diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3edf09d599c..b4fd2c565f2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -332,6 +332,22 @@ jobs: env: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} + requirements_coverage: + name: "Validate requiremenet coverage" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + show-progress: 'false' + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: 21.0.2 + distribution: 'temurin' + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + - run: ./gradlew traceRequirements + # This is https://github.com/marketplace/actions/gradle-wrapper-validation # It ensures that the jar file is from gradle and not by a strange third party. gradlevalidation: diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c307ed39c5..57f964c3101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,8 +26,11 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We enabled creating a new file link manually. [#11017](https://github.com/JabRef/jabref/issues/11017) - We added a toggle button to invert the selected groups. [#9073](https://github.com/JabRef/jabref/issues/9073) - We reintroduced the floating search in the main table. [#4237](https://github.com/JabRef/jabref/issues/4237) +- We improved [cleanup](https://docs.jabref.org/finding-sorting-and-cleaning-entries/cleanupentries) of `arXiv` IDs in distributed in the fields `note`, `version`, `institution`, and `eid` fields. [#11306](https://github.com/JabRef/jabref/issues/11306) +- We added a switch not to store the linked file URL, because it caused troubles at other apps. [#11735](https://github.com/JabRef/jabref/pull/11735) - When starting a new SLR, the selected catalogs now persist within and across JabRef sessions. [koppor#614](https://github.com/koppor/jabref/issues/614) - We added a different background color to the search bar to indicate when the search syntax is wrong. [#11658](https://github.com/JabRef/jabref/pull/11658) +- We added a setting which always adds the literal "Cited on pages" text before each JStyle citation. [#11691](https://github.com/JabRef/jabref/pull/11732) ### Changed @@ -44,6 +47,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - The browse button for a Custom theme now opens in the directory of the current used CSS file. [#11597](https://github.com/JabRef/jabref/pull/11597) - The browse button for a Custom exporter now opens in the directory of the current used exporter file. [#11717](https://github.com/JabRef/jabref/pull/11717) - We improved the display of long messages in the integrity check dialog. [#11619](https://github.com/JabRef/jabref/pull/11619) +- We improved the undo/redo buttons in the main toolbar and main menu to be disabled when there is nothing to undo/redo. [#8807](https://github.com/JabRef/jabref/issues/8807) ### Fixed @@ -65,7 +69,9 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where the full-text search results were incomplete. [#8626](https://github.com/JabRef/jabref/issues/8626) - We fixed an issue where search result highlighting was incorrectly highlighting the boolean operators. [#11595](https://github.com/JabRef/jabref/issues/11595) - We fixed an issue where search result highlighting was broken at complex searches. [#8067](https://github.com/JabRef/jabref/issues/8067) -- We fixed an issue where unescaped braces in the arXiv fetcher were not treated [#11704](https://github.com/JabRef/jabref/issues/11704) +- We fixed an exception when searching for unlinked files. [#11731](https://github.com/JabRef/jabref/issues/11731) +- We fixed an issue where two contradicting notifications were shown when cutting an entry in the main table. [#11724](https://github.com/JabRef/jabref/pull/11724) +- We fixed an issue where unescaped braces in the arXiv fetcher were not treated. [#11704](https://github.com/JabRef/jabref/issues/11704) ### Removed diff --git a/PRIVACY.md b/PRIVACY.md index 853f5371530..d21c0fbbf04 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -38,45 +38,46 @@ These third parties may log additional information besides your IP address and t These third-party services are the following: -| Service | Privacy Policy | -|------------------------------------------------------------------------------------------------------------------------------|----------------| -| [ACM](https://www.acm.org/) | | -| [ACS Publications](https://pubs.acs.org/) | | -| [APS Advancing Physics](https://harvest.aps.org/) | | -| [arXiv.org](https://arxiv.org/) | | -| [Bibliotheksverbund Bayern](https://www.bib-bvb.de/) | | -| [Biodiversity Heritage Library](https://www.biodiversitylibrary.org/) | | -| [Collection of Computer Science Bibliographies](https://en.wikipedia.org/wiki/Collection_of_Computer_Science_Bibliographies) | **currently unavailable**, offline | -| [CrossRef](https://www.crossref.org/) | | -| [dblp](https://dblp.uni-trier.de/) | | -| [DJL (Deep Java Library)](https://djl.ai/) | | -| [Directory of Open Access Books](https://www.doabooks.org/) | | -| [Digitala Vetenskapliga Arkivet](https://www.diva-portal.org/) | | -| [DOI Foundation](https://www.doi.org/) | | -| [Elsevier](https://www.elsevier.com/) | | -| [Google Scholar](https://scholar.google.com/) | | -| [Gemeinsamer Verbundkatalog](https://www.gbv.de/) | | -| [Hugging Face](https://huggingface.co/) | | -| [IACR](https://www.iacr.org/) | | -| [IEEEXplore](https://ieeexplore.ieee.org/Xplore/home.jsp) | | +| Service | Privacy Policy | +|------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------| +| [ACM](https://www.acm.org/) | | +| [ACS Publications](https://pubs.acs.org/) | | +| [APS Advancing Physics](https://harvest.aps.org/) | | +| [arXiv.org](https://arxiv.org/) | | +| [Bibliotheksverbund Bayern](https://www.bib-bvb.de/) | | +| [Biodiversity Heritage Library](https://www.biodiversitylibrary.org/) | | +| [Collection of Computer Science Bibliographies](https://en.wikipedia.org/wiki/Collection_of_Computer_Science_Bibliographies) | **currently unavailable**, offline | +| [CrossRef](https://www.crossref.org/) | | +| [dblp](https://dblp.uni-trier.de/) | | +| [DJL (Deep Java Library)](https://djl.ai/) | | +| [Directory of Open Access Books](https://www.doabooks.org/) | | +| [Digitala Vetenskapliga Arkivet](https://www.diva-portal.org/) | | +| [DOI Foundation](https://www.doi.org/) | | +| [Elsevier](https://www.elsevier.com/) | | +| [Google Gemini](https://ai.google.dev/gemini-api) | | +| [Google Scholar](https://scholar.google.com/) | | +| [Gemeinsamer Verbundkatalog](https://www.gbv.de/) | | +| [Hugging Face](https://huggingface.co/) | | +| [IACR](https://www.iacr.org/) | | +| [IEEEXplore](https://ieeexplore.ieee.org/Xplore/home.jsp) | | | [INSPIRE](https://inspirehep.net/) | | -| [ISIDORE](https://isidore.science/) | | -| [JSTOR](https://www.jstor.org/) | | -| [Library of Congress](https://lccn.loc.gov/) | | -| [Mistral AI](https://mistral.ai/) | | -| [National Library of Medicine](https://www.ncbi.nlm.nih.gov/) | | -| [MathSciNet](http://www.ams.org/mathscinet) | | -| [mEDRA](https://www.medra.org/) | | -| [Mr. DLib](https://mr-dlib.org/) [1] | | -| [OpenAI](https://openai.com/) | | -| [Openlibrary](https://openlibrary.org) | | -| [ResearchGate](https://www.researchgate.net/) | | -| [IETF Datatracker](https://datatracker.ietf.org/) | | -| [Semantic Scholar](https://www.semanticscholar.org/), powered by [Allen Institute for AI](https://allenai.org/) | | -| [Springer Nature](https://dev.springernature.com/) | | -| [The SAO/NASA Astrophysics Data System](https://ui.adsabs.harvard.edu/) | | -| [Unpaywall](https://unpaywall.org/) | | -| [zbMATH Open](https://www.zbmath.org) | | +| [ISIDORE](https://isidore.science/) | | +| [JSTOR](https://www.jstor.org/) | | +| [Library of Congress](https://lccn.loc.gov/) | | +| [Mistral AI](https://mistral.ai/) | | +| [National Library of Medicine](https://www.ncbi.nlm.nih.gov/) | | +| [MathSciNet](http://www.ams.org/mathscinet) | | +| [mEDRA](https://www.medra.org/) | | +| [Mr. DLib](https://mr-dlib.org/) [1] | | +| [OpenAI](https://openai.com/) | | +| [Openlibrary](https://openlibrary.org) | | +| [ResearchGate](https://www.researchgate.net/) | | +| [IETF Datatracker](https://datatracker.ietf.org/) | | +| [Semantic Scholar](https://www.semanticscholar.org/), powered by [Allen Institute for AI](https://allenai.org/) | | +| [Springer Nature](https://dev.springernature.com/) | | +| [The SAO/NASA Astrophysics Data System](https://ui.adsabs.harvard.edu/) | | +| [Unpaywall](https://unpaywall.org/) | | +| [zbMATH Open](https://www.zbmath.org) | | [1]: *Note: The Mr. DLib service is used for the related articles tab in the entry editor and collects also your language, your browser and operating system (*disabled* by default).* diff --git a/build.gradle b/build.gradle index 9bd82d66fb8..ac32717a6be 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,8 @@ plugins { id 'idea' id 'org.openrewrite.rewrite' version '6.20.0' + + id "org.itsallcode.openfasttrace" version "3.0.0" } // Enable following for debugging @@ -236,7 +238,7 @@ dependencies { } implementation 'org.fxmisc.flowless:flowless:0.7.3' implementation 'org.fxmisc.richtext:richtextfx:0.11.3' - implementation (group: 'com.dlsc.gemsfx', name: 'gemsfx', version: '2.43.0') { + implementation (group: 'com.dlsc.gemsfx', name: 'gemsfx', version: '2.48.0') { exclude module: 'javax.inject' // Split package, use only jakarta.inject exclude module: 'commons-lang3' exclude group: 'org.apache.commons.validator' @@ -323,20 +325,21 @@ dependencies { implementation 'com.github.vatbub:mslinks:1.0.6.2' // YAML formatting - implementation 'org.yaml:snakeyaml:2.2' + implementation 'org.yaml:snakeyaml:2.3' // AI - implementation 'dev.langchain4j:langchain4j:0.33.0' + implementation 'dev.langchain4j:langchain4j:0.34.0' // Even though we use jvm-openai for LLM connection, we still need this package for tokenization. - implementation('dev.langchain4j:langchain4j-open-ai:0.33.0') { + implementation('dev.langchain4j:langchain4j-open-ai:0.34.0') { exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8' } - implementation('dev.langchain4j:langchain4j-mistral-ai:0.33.0') - implementation('dev.langchain4j:langchain4j-hugging-face:0.33.0') + implementation('dev.langchain4j:langchain4j-mistral-ai:0.34.0') + implementation('dev.langchain4j:langchain4j-google-ai-gemini:0.34.0') + implementation('dev.langchain4j:langchain4j-hugging-face:0.34.0') implementation 'ai.djl:api:0.29.0' implementation 'ai.djl.pytorch:pytorch-model-zoo:0.29.0' implementation 'ai.djl.huggingface:tokenizers:0.29.0' - implementation 'io.github.stefanbratanov:jvm-openai:0.10.0' + implementation 'io.github.stefanbratanov:jvm-openai:0.11.0' // openai depends on okhttp, which needs kotlin - see https://github.com/square/okhttp/issues/5299 for details implementation ('com.squareup.okhttp3:okhttp:4.12.0') { exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8' @@ -367,7 +370,7 @@ dependencies { xjc group: 'org.glassfish.jaxb', name: 'jaxb-xjc', version: '3.0.2' xjc group: 'org.glassfish.jaxb', name: 'jaxb-runtime', version: '3.0.2' - rewrite(platform("org.openrewrite.recipe:rewrite-recipe-bom:2.17.0")) + rewrite(platform("org.openrewrite.recipe:rewrite-recipe-bom:2.18.0")) rewrite("org.openrewrite.recipe:rewrite-static-analysis") rewrite("org.openrewrite.recipe:rewrite-logging-frameworks") rewrite("org.openrewrite.recipe:rewrite-testing-frameworks") @@ -776,6 +779,7 @@ jlink { uses 'org.eclipse.jgit.transport.SshSessionFactory' uses 'org.eclipse.jgit.lib.GpgSigner' uses 'kong.unirest.core.json.JsonEngine'; + uses 'ai.djl.repository.zoo.ZooProvider'; provides 'org.mariadb.jdbc.tls.TlsSocketPlugin' with 'org.mariadb.jdbc.internal.protocol.tls.DefaultTlsSocketPlugin' provides 'java.sql.Driver' with 'org.postgresql.Driver' provides 'org.mariadb.jdbc.authentication.AuthenticationPlugin' with 'org.mariadb.jdbc.internal.com.send.authentication.CachingSha2PasswordPlugin', @@ -792,7 +796,10 @@ jlink { provides 'java.security.Provider' with 'org.bouncycastle.jce.provider.BouncyCastleProvider', 'org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider' provides 'kong.unirest.core.json.JsonEngine' with 'kong.unirest.modules.gson.GsonEngine'; - + provides 'ai.djl.repository.zoo.ZooProvider' with 'ai.djl.engine.rust.zoo.RsZooProvider', + 'ai.djl.huggingface.zoo.HfZooProvider', + 'ai.djl.pytorch.zoo.PtZooProvider', + 'ai.djl.repository.zoo.DefaultZooProvider'; } jpackage { @@ -898,3 +905,7 @@ jmh { iterations = 10 fork = 2 } + +requirementTracing { + inputDirectories = files('docs', 'src/main/java', 'src/test/java') +} diff --git a/buildres/abbrv.jabref.org b/buildres/abbrv.jabref.org index b69f1d607a5..234cce883ad 160000 --- a/buildres/abbrv.jabref.org +++ b/buildres/abbrv.jabref.org @@ -1 +1 @@ -Subproject commit b69f1d607a57488276f3402bbf610d9129e7f6fb +Subproject commit 234cce883adf9012c2831990d883b198d1d6a939 diff --git a/docs/code-howtos/index.md b/docs/code-howtos/index.md index e8b49cc188f..932dcb04539 100644 --- a/docs/code-howtos/index.md +++ b/docs/code-howtos/index.md @@ -47,7 +47,7 @@ JabRef stores files relative to one of [multiple possible directories](https://d The convert the relative path to an absolute one, there is the `find` method in `FileUtil`: ```java -org.jabref.logic.util.io.FileUtil.find(org.jabref.model.database.BibDatabaseContext, java.lang.String, org.jabref.preferences.FilePreferences) +org.jabref.logic.util.io.FileUtil.find(org.jabref.model.database.BibDatabaseContext, java.lang.String, org.jabref.logic.FilePreferences) ``` `String path` Can be the files name or a relative path to it. The Preferences should only be directly accessed in the GUI. For the usage in logic pass them as parameter @@ -59,7 +59,7 @@ When adding a file to a library, the path should be stored relative to "the best This is implemented in `FileUtil`: ```java -org.jabref.logic.util.io.FileUtil.relativize(java.nio.file.Path, org.jabref.model.database.BibDatabaseContext, org.jabref.preferences.FilePreferences) +org.jabref.logic.util.io.FileUtil.relativize(java.nio.file.Path, org.jabref.model.database.BibDatabaseContext, org.jabref.logic.FilePreferences) ``` ## Setting a Directory for a .bib File diff --git a/docs/contributing.md b/docs/contributing.md index 0e64dfe09b0..d827129b175 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -112,3 +112,5 @@ If you want to indicate that a pull request is not yet complete **before** creat For improving developer's documentation, go on at the [docs/ subdirectory of JabRef's code](https://github.com/JabRef/jabref/tree/main/docs) and edit the file. GitHub offers a good guide at [Editing files in another user's repository](https://help.github.com/en/github/managing-files-in-a-repository/editing-files-in-another-users-repository). + +One can also add [callouts](https://just-the-docs.github.io/just-the-docs-tests/components/callouts/). diff --git a/docs/requirements/ai.md b/docs/requirements/ai.md new file mode 100644 index 00000000000..dfe6be9f004 --- /dev/null +++ b/docs/requirements/ai.md @@ -0,0 +1,16 @@ +--- +parent: Requirements +--- +# AI + +## User Interface + +### Chatting with AI +`req~ai.chat.new-message-based-on-previous~1` + +To enable simple editing and resending of previous messages, Cursor Up should show last message. +This should only happen if the current text field is empty. + +Needs: impl + + diff --git a/docs/requirements/index.md b/docs/requirements/index.md new file mode 100644 index 00000000000..bee1af1c590 --- /dev/null +++ b/docs/requirements/index.md @@ -0,0 +1,49 @@ +--- +nav_order: 7 +has_children: true +--- +# Requirements + +This part of the documentation collects requirements using [OpenFastTrace](https://github.com/itsallcode/openfasttrace). + +## Specifying requirements + +One writes directly below a Markdown heading a requirement identifier. + +Example: + +```markdown +### Example +`req~ai.example~1` +``` + +It is important that there is no empty line directly after the heading. + +{: note} +One needs to add `` to the end of the file, because the id of the requirement needs to follow the heading directly. + +## Linking implementations + +Then, one writes down at the requirement. +Directly at the end, one writes that it requires an implementation: + +```markdown +Needs: impl +``` + +One can also state that there should be detailed design document (`dsn`). +However, typically in JabRef, we go from the requirement directly to the implementation. + +Then, at the implementation, a comment is added this implementation is covered: + +```java +// [impl->req~ai.example~1] +``` + +When executing the gradle task `traceRequirements`, `build/tracing.txt` is generated. +In case of a tracing error, one can inspect this file to see which requirements were not covered. + +## More Information + +- [User manual of OpenFastTrace](https://github.com/itsallcode/openfasttrace/blob/main/doc/user_guide.md) +- We cannot copy and paste real examples here, because of [openfasttrace#280](https://github.com/itsallcode/openfasttrace/issues/280). diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2b189974c29..8e876e1c557 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=5b9c5eb3f9fc2c94abaea57d90bd78747ca117ddbbf96c859d3741181a12bf2a -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +distributionSha256Sum=1541fa36599e12857140465f3c91a97409b4512501c26f9631fb113e392c5bd1 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/scripts/vms/README.md b/scripts/vms/README.md index 5809913510d..a25e8b9d665 100644 --- a/scripts/vms/README.md +++ b/scripts/vms/README.md @@ -55,7 +55,8 @@ Then, everything is removed. | [`fedora`](fedora/) | source | -- | -- | -- | | [`Linux Mint (Cinnamon)`](linux-mint-cinnamon/) | source | Firefox | yes | -- | | [`ubuntu`](ubuntu/) | snap | Firefox | yes | -- | -| [`windows`](windows/) | source | Edge | -- | -- | +| [`windows 10`](windows10/) | source | Edge | -- | -- | +| [`windows 11`](windows11/) | source | Edge | -- | -- | ## Troubleshooting diff --git a/scripts/vms/windows/README.md b/scripts/vms/windows10/README.md similarity index 99% rename from scripts/vms/windows/README.md rename to scripts/vms/windows10/README.md index e0948951dd9..a9efe6fa563 100644 --- a/scripts/vms/windows/README.md +++ b/scripts/vms/windows10/README.md @@ -1,4 +1,4 @@ -# Windows VM +# Windows 10 VM A Windows-based VM to test JabRef. As user, you need to ensure to have the proper Windows license to use this VM. diff --git a/scripts/vms/windows/Vagrantfile b/scripts/vms/windows10/Vagrantfile similarity index 97% rename from scripts/vms/windows/Vagrantfile rename to scripts/vms/windows10/Vagrantfile index 21331b914af..8ccefd513a0 100644 --- a/scripts/vms/windows/Vagrantfile +++ b/scripts/vms/windows10/Vagrantfile @@ -1,7 +1,7 @@ # -*- mode: ruby -*- # vi: set ft=ruby : -NAME = "jabref-windows-sandbox" +NAME = "jabref-windows-10-sandbox" Vagrant.configure("2") do |config| config.vm.box = "gusztavvargadr/windows-10" diff --git a/scripts/vms/windows11/README.md b/scripts/vms/windows11/README.md new file mode 100644 index 00000000000..0a8e1f9a51f --- /dev/null +++ b/scripts/vms/windows11/README.md @@ -0,0 +1,49 @@ +# Windows 11 VM + +A Windows-based VM to test JabRef. +As user, you need to ensure to have the proper Windows license to use this VM. + +In case you have many CPU cores, you can adapt `vb.cpus` in `Vagrantfile` to a higher number. + +One has to install the [JabRef Browser Extension](https://addons.mozilla.org/en-US/firefox/addon/jabref/) manually. + +## Troubleshooting + +### "Waiting for machine to reboot..." + +In case Vagrant reports "Waiting for machine to reboot..." and nothing happens, one has to "power off" the machine, execute `vagrant destory`, and then run `vagrant up` again. + +### `fatal: early EOF` + +```console +jabref-windows-sandbox: Cloning into 'jabref'... +jabref-windows-sandbox: error: RPC failed; curl 92 HTTP/2 stream 5 was not closed cleanly: CANCEL (err 8) +jabref-windows-sandbox: error: 6846 bytes of body are still expected +jabref-windows-sandbox: fetch-pack: unexpected disconnect while reading sideband packet +jabref-windows-sandbox: fatal: early EOF +jabref-windows-sandbox: fatal: fetch-pack: invalid index-pack output +``` + +The `git clone` command did not work. + +Login, open `cmd` and then execute following commands: + +```cmd +git clone --recurse-submodules https://github.com/JabRef/jabref.git +cd jabref +gradlew run +``` + +## Background + +`Vagrantfile` is based on [SeisoLLC/windows-sandbox](https://github.com/SeisoLLC/windows-sandbox/tree/main). + +The most use image seems to be the [Windows 10 image by `gusztavvargadr`](https://portal.cloud.hashicorp.com/vagrant/discover/gusztavvargadr/windows-10). +List of all images at . + +[Chocolatey](https://chocolatey.org/) is used instead of [winget-cli](https://learn.microsoft.com/en-us/windows/package-manager/), because Chocolatey installation does not hit GitHub's rate limits during unattended installation. + +## Atlernatives + +- Atlernative Vagrant images: . +- [Windows Sandbox](https://learn.microsoft.com/en-us/windows/security/application-security/application-isolation/windows-sandbox/windows-sandbox-overview) diff --git a/scripts/vms/windows11/Vagrantfile b/scripts/vms/windows11/Vagrantfile new file mode 100644 index 00000000000..0d9cdf27c81 --- /dev/null +++ b/scripts/vms/windows11/Vagrantfile @@ -0,0 +1,46 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +NAME = "jabref-windows-11-sandbox" + +Vagrant.configure("2") do |config| + # config.vm.box = "Caden/windows-11-pro-jp-22h2-22621.1992" + config.vm.box = "stromweld/windows-11" + + config.vm.define NAME + config.vm.hostname = NAME + + config.vm.provider "virtualbox" do |vb| + vb.name = NAME + + vb.memory = 6000 + vb.cpus = 2 + + vb.customize ['modifyvm', :id, '--clipboard-mode', 'bidirectional'] + vb.gui = true + end + + config.vm.provision "shell", privileged: "true", powershell_elevated_interactive: "true", inline: <<-SHELL + # Install chocolatey + Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) + choco feature enable -n=allowGlobalConfirmation + + choco install libericajdk + choco install git.install -y --params "/GitAndUnixToolsOnPath /WindowsTerminal /WindowsTerminalProfile" + + # Required by AI functionaltiy + choco choco install vcredist140 + + # choco install firefox + # choco install libreoffice-fresh + SHELL + + config.vm.provision "shell", reboot: true + + config.vm.provision "shell", privileged: "false", inline: <<-SHELL + cd \\users\\vagrant + git clone --depth=1 --recurse-submodules https://github.com/JabRef/jabref.git + # cd jabref + # .\\gradlew jar + SHELL +end diff --git a/src/jmh/java/org/jabref/benchmarks/Benchmarks.java b/src/jmh/java/org/jabref/benchmarks/Benchmarks.java index d2e79b24c63..e553c87ceec 100644 --- a/src/jmh/java/org/jabref/benchmarks/Benchmarks.java +++ b/src/jmh/java/org/jabref/benchmarks/Benchmarks.java @@ -16,7 +16,9 @@ import org.jabref.logic.importer.fileformat.BibtexParser; import org.jabref.logic.layout.format.HTMLChars; import org.jabref.logic.layout.format.LatexToUnicodeFormatter; -import org.jabref.logic.util.OS; +import org.jabref.logic.os.OS; +import org.jabref.logic.preferences.CliPreferences; +import org.jabref.logic.preferences.JabRefCliPreferences; import org.jabref.model.database.BibDatabase; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.database.BibDatabaseMode; @@ -29,8 +31,6 @@ import org.jabref.model.groups.KeywordGroup; import org.jabref.model.groups.WordKeywordGroup; import org.jabref.model.metadata.MetaData; -import org.jabref.preferences.JabRefPreferences; -import org.jabref.preferences.PreferencesService; import com.airhacks.afterburner.injection.Injector; import org.openjdk.jmh.Main; @@ -52,7 +52,7 @@ public class Benchmarks { @Setup public void init() throws Exception { - Injector.setModelOrService(PreferencesService.class, JabRefPreferences.getInstance()); + Injector.setModelOrService(CliPreferences.class, JabRefCliPreferences.getInstance()); Random randomizer = new Random(); for (int i = 0; i < 1000; i++) { @@ -89,8 +89,8 @@ private StringWriter getOutputWriter() throws IOException { @Benchmark public ParserResult parse() throws IOException { - PreferencesService preferencesService = Injector.instantiateModelOrService(PreferencesService.class); - BibtexParser parser = new BibtexParser(preferencesService.getImportFormatPreferences()); + CliPreferences preferences = Injector.instantiateModelOrService(CliPreferences.class); + BibtexParser parser = new BibtexParser(preferences.getImportFormatPreferences()); return parser.parse(new StringReader(bibtexString)); } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 65e89455413..dc1fce79618 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -142,8 +142,9 @@ requires org.jooq.jool; - // region: AI + // region AI requires ai.djl.api; + uses ai.djl.repository.zoo.ZooProvider; requires ai.djl.tokenizers; requires jvm.openai; requires langchain4j; @@ -183,5 +184,6 @@ requires mslinks; requires org.antlr.antlr4.runtime; requires org.libreoffice.uno; + requires langchain4j.google.ai.gemini; // endregion } diff --git a/src/main/java/org/jabref/Launcher.java b/src/main/java/org/jabref/Launcher.java index 117c9fd5c1c..1d3cb9cb3e1 100644 --- a/src/main/java/org/jabref/Launcher.java +++ b/src/main/java/org/jabref/Launcher.java @@ -14,6 +14,8 @@ import org.jabref.cli.ArgumentProcessor; import org.jabref.cli.JabRefCLI; import org.jabref.gui.JabRefGUI; +import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.gui.preferences.JabRefGuiPreferences; import org.jabref.gui.util.DefaultDirectoryMonitor; import org.jabref.gui.util.DefaultFileUpdateMonitor; import org.jabref.logic.UiCommand; @@ -24,19 +26,17 @@ import org.jabref.logic.net.ProxyRegisterer; import org.jabref.logic.net.ssl.SSLPreferences; import org.jabref.logic.net.ssl.TrustStoreManager; +import org.jabref.logic.preferences.CliPreferences; import org.jabref.logic.protectedterms.ProtectedTermsLoader; import org.jabref.logic.remote.RemotePreferences; import org.jabref.logic.remote.client.RemoteClient; import org.jabref.logic.util.BuildInfo; +import org.jabref.logic.util.Directories; import org.jabref.logic.util.HeadlessExecutorService; -import org.jabref.logic.util.OS; import org.jabref.migrations.PreferencesMigrations; import org.jabref.model.entry.BibEntryTypesManager; import org.jabref.model.util.DirectoryMonitor; import org.jabref.model.util.FileUpdateMonitor; -import org.jabref.preferences.JabRefPreferences; -import org.jabref.preferences.PreferencesService; -import org.jabref.preferences.ai.AiApiKeyProvider; import com.airhacks.afterburner.injection.Injector; import org.apache.commons.cli.ParseException; @@ -62,9 +62,9 @@ public static void main(String[] args) { Injector.setModelOrService(BuildInfo.class, new BuildInfo()); // Initialize preferences - final JabRefPreferences preferences = JabRefPreferences.getInstance(); - Injector.setModelOrService(PreferencesService.class, preferences); - Injector.setModelOrService(AiApiKeyProvider.class, preferences); + final JabRefGuiPreferences preferences = JabRefGuiPreferences.getInstance(); + Injector.setModelOrService(CliPreferences.class, preferences); + Injector.setModelOrService(GuiPreferences.class, preferences); // Early exit in case another instance is already running if (!handleMultipleAppInstances(args, preferences.getRemotePreferences())) { @@ -97,6 +97,7 @@ public static void main(String[] args) { args, ArgumentProcessor.Mode.INITIAL_START, preferences, + preferences, fileUpdateMonitor, entryTypesManager); argumentProcessor.processArguments(); @@ -139,7 +140,7 @@ private static void initLogging(String[] args) { } // addLogToDisk - Path directory = OS.getNativeDesktop().getLogDirectory(); + Path directory = Directories.getLogDirectory(); try { Files.createDirectories(directory); } catch (IOException e) { @@ -206,7 +207,7 @@ private static void configureSSL(SSLPreferences sslPreferences) { } private static void clearOldSearchIndices() { - Path currentIndexPath = OS.getNativeDesktop().getFulltextIndexBaseDirectory(); + Path currentIndexPath = Directories.getFulltextIndexBaseDirectory(); Path appData = currentIndexPath.getParent(); try { diff --git a/src/main/java/org/jabref/cli/ArgumentProcessor.java b/src/main/java/org/jabref/cli/ArgumentProcessor.java index 1ac2d50da67..64edddac7a4 100644 --- a/src/main/java/org/jabref/cli/ArgumentProcessor.java +++ b/src/main/java/org/jabref/cli/ArgumentProcessor.java @@ -14,8 +14,8 @@ import java.util.prefs.BackingStoreException; import org.jabref.gui.externalfiles.AutoSetFileLinksUtil; -import org.jabref.gui.undo.NamedCompound; -import org.jabref.gui.util.CurrentThreadTaskExecutor; +import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.logic.FilePreferences; import org.jabref.logic.JabRefException; import org.jabref.logic.UiCommand; import org.jabref.logic.bibtex.FieldPreferences; @@ -42,9 +42,12 @@ import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; import org.jabref.logic.net.URLDownload; +import org.jabref.logic.os.OS; +import org.jabref.logic.preferences.CliPreferences; import org.jabref.logic.search.DatabaseSearcher; +import org.jabref.logic.search.SearchPreferences; import org.jabref.logic.shared.prefs.SharedDatabasePreferences; -import org.jabref.logic.util.OS; +import org.jabref.logic.util.CurrentThreadTaskExecutor; import org.jabref.logic.util.io.FileUtil; import org.jabref.logic.xmp.XmpPreferences; import org.jabref.model.database.BibDatabase; @@ -56,9 +59,6 @@ import org.jabref.model.strings.StringUtil; import org.jabref.model.util.DummyFileUpdateMonitor; import org.jabref.model.util.FileUpdateMonitor; -import org.jabref.preferences.FilePreferences; -import org.jabref.preferences.PreferencesService; -import org.jabref.preferences.SearchPreferences; import com.airhacks.afterburner.injection.Injector; import com.google.common.base.Throwables; @@ -74,7 +74,8 @@ public enum Mode { INITIAL_START, REMOTE_START } private final Mode startupMode; - private final PreferencesService preferencesService; + private final CliPreferences cliPreferences; + private final GuiPreferences guiPreferences; private final FileUpdateMonitor fileUpdateMonitor; private final BibEntryTypesManager entryTypesManager; @@ -84,31 +85,32 @@ public enum Mode { INITIAL_START, REMOTE_START } /** * First call the constructor, then call {@link #processArguments()}. * Afterward, you can access the {@link #getUiCommands()}. + * + * @implNote both cli and gui preferences are passed to make the dependency to GUI parts explicit */ public ArgumentProcessor(String[] args, Mode startupMode, - PreferencesService preferencesService, + CliPreferences cliPreferences, + GuiPreferences guiPreferences, FileUpdateMonitor fileUpdateMonitor, BibEntryTypesManager entryTypesManager) throws org.apache.commons.cli.ParseException { this.cli = new JabRefCLI(args); this.startupMode = startupMode; - this.preferencesService = preferencesService; + this.cliPreferences = cliPreferences; + this.guiPreferences = guiPreferences; this.fileUpdateMonitor = fileUpdateMonitor; this.entryTypesManager = entryTypesManager; } /** - * Will open a file (like importFile), but will also request JabRef to focus on this database + * Will open a file (like {@link #importFile(String)}, but will also request JabRef to focus on this database. * - * @param argument See importFile. * @return ParserResult with setToOpenTab(true) */ - private Optional importToOpenBase(String argument) { - Optional result = importFile(argument); - + private Optional importToOpenBase(String importArguments) { + Optional result = importFile(importArguments); result.ifPresent(ParserResult::setToOpenTab); - return result; } @@ -125,9 +127,13 @@ private Optional importBibtexToOpenBase(String argument, ImportFor } } - private Optional importFile(String argument) { - LOGGER.debug("Importing file {}", argument); - String[] data = argument.split(","); + /** + * + * @param importArguments Format: fileName[,format] + */ + private Optional importFile(String importArguments) { + LOGGER.debug("Importing file {}", importArguments); + String[] data = importArguments.split(","); String address = data[0]; Path file; @@ -166,9 +172,9 @@ private Optional importFile(String argument) { private Optional importFile(Path file, String importFormat) { try { ImportFormatReader importFormatReader = new ImportFormatReader( - preferencesService.getImporterPreferences(), - preferencesService.getImportFormatPreferences(), - preferencesService.getCitationKeyPatternPreferences(), + cliPreferences.getImporterPreferences(), + cliPreferences.getImportFormatPreferences(), + cliPreferences.getCitationKeyPatternPreferences(), fileUpdateMonitor ); @@ -200,7 +206,7 @@ public void processArguments() { } if ((startupMode == Mode.INITIAL_START) && cli.isHelp()) { - JabRefCLI.printUsage(preferencesService); + JabRefCLI.printUsage(cliPreferences); guiNeeded = false; return; } @@ -249,11 +255,11 @@ public void processArguments() { if (!loaded.isEmpty()) { writeMetadataToPdf(loaded, cli.getWriteMetadataToPdf(), - preferencesService.getXmpPreferences(), - preferencesService.getFilePreferences(), - preferencesService.getLibraryPreferences().getDefaultBibDatabaseMode(), + cliPreferences.getXmpPreferences(), + cliPreferences.getFilePreferences(), + cliPreferences.getLibraryPreferences().getDefaultBibDatabaseMode(), Injector.instantiateModelOrService(BibEntryTypesManager.class), - preferencesService.getFieldPreferences(), + cliPreferences.getFieldPreferences(), Injector.instantiateModelOrService(JournalAbbreviationRepository.class), cli.isWriteXmpToPdf() || cli.isWriteMetadataToPdf(), cli.isEmbedBibFileInPdf() || cli.isWriteMetadataToPdf()); @@ -271,7 +277,7 @@ public void processArguments() { if (cli.isPreferencesExport()) { try { - preferencesService.exportPreferences(Path.of(cli.getPreferencesExport())); + cliPreferences.exportPreferences(Path.of(cli.getPreferencesExport())); } catch (JabRefException ex) { LOGGER.error("Cannot export preferences", ex); } @@ -295,7 +301,7 @@ public void processArguments() { } private void writeMetadataToPdf(List loaded, - String filesAndCitekeys, + String filesAndCiteKeys, XmpPreferences xmpPreferences, FilePreferences filePreferences, BibDatabaseMode databaseMode, @@ -314,7 +320,7 @@ private void writeMetadataToPdf(List loaded, XmpPdfExporter xmpPdfExporter = new XmpPdfExporter(xmpPreferences); EmbeddedBibFilePdfExporter embeddedBibFilePdfExporter = new EmbeddedBibFilePdfExporter(databaseMode, entryTypesManager, fieldPreferences); - if ("all".equals(filesAndCitekeys)) { + if ("all".equals(filesAndCiteKeys)) { for (BibEntry entry : databaseContext.getEntries()) { writeMetadataToPDFsOfEntry( databaseContext, @@ -332,7 +338,7 @@ private void writeMetadataToPdf(List loaded, List citeKeys = new ArrayList<>(); List pdfs = new ArrayList<>(); - for (String fileOrCiteKey : filesAndCitekeys.split(",")) { + for (String fileOrCiteKey : filesAndCiteKeys.split(",")) { if (fileOrCiteKey.toLowerCase(Locale.ROOT).endsWith(".pdf")) { pdfs.add(fileOrCiteKey); } else { @@ -368,7 +374,7 @@ private void writeMetadataToPDFsOfEntry(BibDatabaseContext databaseContext, EmbeddedBibFilePdfExporter embeddedBibFilePdfExporter, JournalAbbreviationRepository abbreviationRepository, boolean writeXMP, - boolean embeddBibfile) { + boolean embedBibfile) { try { if (writeXMP) { if (xmpPdfExporter.exportToAllFilesOfEntry(databaseContext, filePreferences, entry, List.of(entry), abbreviationRepository)) { @@ -377,7 +383,7 @@ private void writeMetadataToPDFsOfEntry(BibDatabaseContext databaseContext, System.err.printf("Cannot write XMP metadata on any linked files of %s. Make sure there is at least one linked file and the path is correct.%n", citeKey); } } - if (embeddBibfile) { + if (embedBibfile) { if (embeddedBibFilePdfExporter.exportToAllFilesOfEntry(databaseContext, filePreferences, entry, List.of(entry), abbreviationRepository)) { System.out.printf("Successfully embedded metadata on at least one linked file of %s%n", citeKey); } else { @@ -456,12 +462,13 @@ private boolean exportMatches(List loaded) { ParserResult pr = loaded.getLast(); BibDatabaseContext databaseContext = pr.getDatabaseContext(); - SearchPreferences searchPreferences = preferencesService.getSearchPreferences(); + SearchPreferences searchPreferences = cliPreferences.getSearchPreferences(); SearchQuery query = new SearchQuery(searchTerm, searchPreferences.getSearchFlags()); List matches; try { - matches = new DatabaseSearcher(query, databaseContext, new CurrentThreadTaskExecutor(), preferencesService.getFilePreferences()).getMatches(); + // extract current thread task executor from luceneManager + matches = new DatabaseSearcher(query, databaseContext, new CurrentThreadTaskExecutor(), cliPreferences.getFilePreferences()).getMatches(); } catch (IOException e) { LOGGER.error("Error occurred when searching", e); return false; @@ -493,7 +500,7 @@ private boolean exportMatches(List loaded) { } else { // export new database ExporterFactory exporterFactory = ExporterFactory.create( - preferencesService, + cliPreferences, Injector.instantiateModelOrService(BibEntryTypesManager.class)); Optional exporter = exporterFactory.getExporterByName(formatName); if (exporter.isEmpty()) { @@ -553,7 +560,7 @@ private List importAndOpenFiles() { try { pr = OpenDatabase.loadDatabase( Path.of(aLeftOver), - preferencesService.getImportFormatPreferences(), + cliPreferences.getImportFormatPreferences(), fileUpdateMonitor); // In contrast to org.jabref.gui.LibraryTab.onDatabaseLoadingSucceed, we do not execute OpenDatabaseAction.performPostOpenActions(result, dialogService); } catch (IOException ex) { @@ -593,7 +600,7 @@ private List importAndOpenFiles() { } if (!cli.isBlank() && cli.isBibtexImport()) { - importBibtexToOpenBase(cli.getBibtexImport(), preferencesService.getImportFormatPreferences()).ifPresent(loaded::add); + importBibtexToOpenBase(cli.getBibtexImport(), cliPreferences.getImportFormatPreferences()).ifPresent(loaded::add); } return loaded; @@ -629,12 +636,12 @@ private void saveDatabase(BibDatabase newBase, String subName) { try (AtomicFileWriter fileWriter = new AtomicFileWriter(Path.of(subName), StandardCharsets.UTF_8)) { BibWriter bibWriter = new BibWriter(fileWriter, OS.NEWLINE); SelfContainedSaveConfiguration saveConfiguration = (SelfContainedSaveConfiguration) new SelfContainedSaveConfiguration() - .withReformatOnSave(preferencesService.getLibraryPreferences().shouldAlwaysReformatOnSave()); + .withReformatOnSave(cliPreferences.getLibraryPreferences().shouldAlwaysReformatOnSave()); BibDatabaseWriter databaseWriter = new BibtexDatabaseWriter( bibWriter, saveConfiguration, - preferencesService.getFieldPreferences(), - preferencesService.getCitationKeyPatternPreferences(), + cliPreferences.getFieldPreferences(), + cliPreferences.getCitationKeyPatternPreferences(), entryTypesManager); databaseWriter.saveDatabase(new BibDatabaseContext(newBase)); @@ -670,10 +677,10 @@ private void exportFile(List loaded, String[] data) { BibDatabaseContext databaseContext = parserResult.getDatabaseContext(); databaseContext.setDatabasePath(path); List fileDirForDatabase = databaseContext - .getFileDirectories(preferencesService.getFilePreferences()); + .getFileDirectories(cliPreferences.getFilePreferences()); System.out.println(Localization.lang("Exporting %0", data[0])); ExporterFactory exporterFactory = ExporterFactory.create( - preferencesService, + cliPreferences, Injector.instantiateModelOrService(BibEntryTypesManager.class)); Optional exporter = exporterFactory.getExporterByName(data[1]); if (exporter.isEmpty()) { @@ -696,8 +703,8 @@ private void exportFile(List loaded, String[] data) { private void importPreferences() { try { - preferencesService.importPreferences(Path.of(cli.getPreferencesImport())); - Injector.setModelOrService(BibEntryTypesManager.class, preferencesService.getCustomEntryTypesRepository()); + cliPreferences.importPreferences(Path.of(cli.getPreferencesImport())); + Injector.setModelOrService(BibEntryTypesManager.class, cliPreferences.getCustomEntryTypesRepository()); } catch (JabRefException ex) { LOGGER.error("Cannot import preferences", ex); } @@ -707,7 +714,7 @@ private void resetPreferences(String value) { if ("all".equals(value.trim())) { try { System.out.println(Localization.lang("Setting all preferences to default values.")); - preferencesService.clear(); + cliPreferences.clear(); new SharedDatabasePreferences().clear(); } catch (BackingStoreException e) { System.err.println(Localization.lang("Unable to clear preferences.")); @@ -717,7 +724,7 @@ private void resetPreferences(String value) { String[] keys = value.split(","); for (String key : keys) { try { - preferencesService.deleteKey(key.trim()); + cliPreferences.deleteKey(key.trim()); System.out.println(Localization.lang("Resetting preference key '%0'", key.trim())); } catch (IllegalArgumentException e) { System.out.println(e.getMessage()); @@ -729,12 +736,18 @@ private void resetPreferences(String value) { private void automaticallySetFileLinks(List loaded) { for (ParserResult parserResult : loaded) { BibDatabase database = parserResult.getDatabase(); - LOGGER.info(Localization.lang("Automatically setting file links")); + LOGGER.info("Automatically setting file links for {}", + parserResult.getDatabaseContext().getDatabasePath() + .map(Path::getFileName) + .map(Path::toString).orElse("UNKNOWN")); + AutoSetFileLinksUtil util = new AutoSetFileLinksUtil( parserResult.getDatabaseContext(), - preferencesService.getFilePreferences(), - preferencesService.getAutoLinkPreferences()); - util.linkAssociatedFiles(database.getEntries(), new NamedCompound("")); + guiPreferences.getExternalApplicationsPreferences(), + cliPreferences.getFilePreferences(), + cliPreferences.getAutoLinkPreferences()); + + util.linkAssociatedFiles(database.getEntries(), (linkedFile, bibEntry) -> bibEntry.addFile(linkedFile)); } } @@ -746,7 +759,7 @@ private void regenerateCitationKeys(List loaded) { CitationKeyGenerator keyGenerator = new CitationKeyGenerator( parserResult.getDatabaseContext(), - preferencesService.getCitationKeyPatternPreferences()); + cliPreferences.getCitationKeyPatternPreferences()); for (BibEntry entry : database.getEntries()) { keyGenerator.generateAndSetKey(entry); } @@ -771,8 +784,8 @@ private Optional fetch(String fetchCommand) { String query = split[1]; Set fetchers = WebFetchers.getSearchBasedFetchers( - preferencesService.getImportFormatPreferences(), - preferencesService.getImporterPreferences()); + cliPreferences.getImportFormatPreferences(), + cliPreferences.getImporterPreferences()); Optional selectedFetcher = fetchers.stream() .filter(fetcher -> fetcher.getName().equalsIgnoreCase(engine)) .findFirst(); diff --git a/src/main/java/org/jabref/cli/JabRefCLI.java b/src/main/java/org/jabref/cli/JabRefCLI.java index 4d36f61158b..0cd74a0bba4 100644 --- a/src/main/java/org/jabref/cli/JabRefCLI.java +++ b/src/main/java/org/jabref/cli/JabRefCLI.java @@ -8,12 +8,12 @@ import org.jabref.logic.exporter.ExporterFactory; import org.jabref.logic.importer.ImportFormatReader; import org.jabref.logic.l10n.Localization; +import org.jabref.logic.os.OS; +import org.jabref.logic.preferences.CliPreferences; import org.jabref.logic.util.BuildInfo; -import org.jabref.logic.util.OS; import org.jabref.model.entry.BibEntryTypesManager; import org.jabref.model.strings.StringUtil; import org.jabref.model.util.DummyFileUpdateMonitor; -import org.jabref.preferences.PreferencesService; import com.airhacks.afterburner.injection.Injector; import org.apache.commons.cli.CommandLine; @@ -309,13 +309,13 @@ public void displayVersion() { System.out.println(getVersionInfo()); } - public static void printUsage(PreferencesService preferencesService) { + public static void printUsage(CliPreferences preferences) { String header = ""; ImportFormatReader importFormatReader = new ImportFormatReader( - preferencesService.getImporterPreferences(), - preferencesService.getImportFormatPreferences(), - preferencesService.getCitationKeyPatternPreferences(), + preferences.getImporterPreferences(), + preferences.getImportFormatPreferences(), + preferences.getCitationKeyPatternPreferences(), new DummyFileUpdateMonitor() ); List> importFormats = importFormatReader @@ -326,7 +326,7 @@ public static void printUsage(PreferencesService preferencesService) { String importFormatsList = "%s:%n%s%n".formatted(importFormatsIntro, alignStringTable(importFormats)); ExporterFactory exporterFactory = ExporterFactory.create( - preferencesService, + preferences, Injector.instantiateModelOrService(BibEntryTypesManager.class)); List> exportFormats = exporterFactory .getExporters().stream() diff --git a/src/main/java/org/jabref/gui/ClipBoardManager.java b/src/main/java/org/jabref/gui/ClipBoardManager.java index f135aeab52e..3e1455908e0 100644 --- a/src/main/java/org/jabref/gui/ClipBoardManager.java +++ b/src/main/java/org/jabref/gui/ClipBoardManager.java @@ -18,11 +18,11 @@ import org.jabref.architecture.AllowedToUseAwt; import org.jabref.logic.bibtex.BibEntryWriter; import org.jabref.logic.bibtex.FieldWriter; +import org.jabref.logic.preferences.CliPreferences; import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.BibEntryTypesManager; import org.jabref.model.entry.BibtexString; -import org.jabref.preferences.PreferencesService; import com.airhacks.afterburner.injection.Injector; import org.slf4j.Logger; @@ -167,11 +167,11 @@ public void setContent(List entries, BibEntryTypesManager entryTypesMa } private String serializeEntries(List entries, BibEntryTypesManager entryTypesManager) throws IOException { - PreferencesService preferencesService = Injector.instantiateModelOrService(PreferencesService.class); + CliPreferences preferences = Injector.instantiateModelOrService(CliPreferences.class); // BibEntry is not Java serializable. Thus, we need to do the serialization manually // At reading of the clipboard in JabRef, we parse the plain string in all cases, so we don't need to flag we put BibEntries here // Furthermore, storing a string also enables other applications to work with the data - BibEntryWriter writer = new BibEntryWriter(new FieldWriter(preferencesService.getFieldPreferences()), entryTypesManager); + BibEntryWriter writer = new BibEntryWriter(new FieldWriter(preferences.getFieldPreferences()), entryTypesManager); return writer.serializeAll(entries, BibDatabaseMode.BIBTEX); } } diff --git a/src/main/java/org/jabref/preferences/GuiPreferences.java b/src/main/java/org/jabref/gui/CoreGuiPreferences.java similarity index 61% rename from src/main/java/org/jabref/preferences/GuiPreferences.java rename to src/main/java/org/jabref/gui/CoreGuiPreferences.java index af79cc49a99..23212a62ed1 100644 --- a/src/main/java/org/jabref/preferences/GuiPreferences.java +++ b/src/main/java/org/jabref/gui/CoreGuiPreferences.java @@ -1,22 +1,13 @@ -package org.jabref.preferences; - -import java.nio.file.Path; -import java.util.List; +package org.jabref.gui; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; -import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; -import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; - -import org.jabref.logic.util.io.FileHistory; -public class GuiPreferences { +public class CoreGuiPreferences { private final DoubleProperty positionX; private final DoubleProperty positionY; private final DoubleProperty sizeX; @@ -26,36 +17,22 @@ public class GuiPreferences { private final DoubleProperty sidePaneWidth; - // the last libraries that were open when jabref closes and should be reopened on startup - private final ObservableList lastFilesOpened; - - private final ObjectProperty lastFocusedFile; - - // observable list last files opened in the file menu - private final FileHistory fileHistory; - private final StringProperty lastSelectedIdBasedFetcher; - public GuiPreferences(double positionX, - double positionY, - double sizeX, - double sizeY, - boolean windowMaximised, - List lastFilesOpened, - Path lastFocusedFile, - FileHistory fileHistory, - String lastSelectedIdBasedFetcher, - double sidePaneWidth) { + public CoreGuiPreferences(double positionX, + double positionY, + double sizeX, + double sizeY, + boolean windowMaximised, + String lastSelectedIdBasedFetcher, + double sidePaneWidth) { this.positionX = new SimpleDoubleProperty(positionX); this.positionY = new SimpleDoubleProperty(positionY); this.sizeX = new SimpleDoubleProperty(sizeX); this.sizeY = new SimpleDoubleProperty(sizeY); this.windowMaximised = new SimpleBooleanProperty(windowMaximised); - this.lastFilesOpened = FXCollections.observableArrayList(lastFilesOpened); - this.lastFocusedFile = new SimpleObjectProperty<>(lastFocusedFile); this.lastSelectedIdBasedFetcher = new SimpleStringProperty(lastSelectedIdBasedFetcher); this.sidePaneWidth = new SimpleDoubleProperty(sidePaneWidth); - this.fileHistory = fileHistory; } public double getPositionX() { @@ -118,30 +95,6 @@ public void setWindowMaximised(boolean windowMaximised) { this.windowMaximised.set(windowMaximised); } - public ObservableList getLastFilesOpened() { - return lastFilesOpened; - } - - public void setLastFilesOpened(List files) { - lastFilesOpened.setAll(files); - } - - public Path getLastFocusedFile() { - return lastFocusedFile.get(); - } - - public ObjectProperty lastFocusedFileProperty() { - return lastFocusedFile; - } - - public void setLastFocusedFile(Path lastFocusedFile) { - this.lastFocusedFile.set(lastFocusedFile); - } - - public FileHistory getFileHistory() { - return fileHistory; - } - public String getLastSelectedIdBasedFetcher() { return lastSelectedIdBasedFetcher.get(); } diff --git a/src/main/java/org/jabref/gui/DialogService.java b/src/main/java/org/jabref/gui/DialogService.java index 03c505068a8..7248be47511 100644 --- a/src/main/java/org/jabref/gui/DialogService.java +++ b/src/main/java/org/jabref/gui/DialogService.java @@ -22,6 +22,7 @@ import org.jabref.gui.util.DirectoryDialogConfiguration; import org.jabref.gui.util.FileDialogConfiguration; import org.jabref.logic.importer.FetcherException; +import org.jabref.logic.util.NotificationService; import org.controlsfx.control.textfield.CustomPasswordField; import org.controlsfx.dialog.ProgressDialog; @@ -29,7 +30,7 @@ /** * This interface provides methods to create dialogs and show them to the user. */ -public interface DialogService { +public interface DialogService extends NotificationService { /** * This will create and display new {@link ChoiceDialog} of type T with a default choice and a collection of possible choices @@ -251,13 +252,6 @@ Optional showCustomButtonDialogAndWait(Alert.AlertType type, String */ Optional showBackgroundProgressDialogAndWait(String title, String content, StateManager stateManager); - /** - * Notify the user in a non-blocking way (i.e., in form of toast in a snackbar). - * - * @param message the message to show. - */ - void notify(String message); - /** * Shows a new file save dialog. The method doesn't return until the * displayed file save dialog is dismissed. The return value specifies the diff --git a/src/main/java/org/jabref/gui/FallbackExceptionHandler.java b/src/main/java/org/jabref/gui/FallbackExceptionHandler.java deleted file mode 100644 index 2f42d2a00a1..00000000000 --- a/src/main/java/org/jabref/gui/FallbackExceptionHandler.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.jabref.gui; - -import org.jabref.gui.util.UiTaskExecutor; - -import com.airhacks.afterburner.injection.Injector; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Catch and log any unhandled exceptions. - */ -public class FallbackExceptionHandler implements Thread.UncaughtExceptionHandler { - - private static final Logger LOGGER = LoggerFactory.getLogger(FallbackExceptionHandler.class); - - public static void installExceptionHandler() { - Thread.setDefaultUncaughtExceptionHandler(new FallbackExceptionHandler()); - } - - @Override - public void uncaughtException(Thread thread, Throwable exception) { - LOGGER.error("Uncaught exception occurred in {}", thread, exception); - UiTaskExecutor.runInJavaFXThread(() -> { - DialogService dialogService = Injector.instantiateModelOrService(DialogService.class); - dialogService.showErrorDialogAndWait("Uncaught exception occurred in " + thread, exception); - }); - } -} diff --git a/src/main/java/org/jabref/gui/JabRefDialogService.java b/src/main/java/org/jabref/gui/JabRefDialogService.java index 9d907840e43..c3e3ce962d9 100644 --- a/src/main/java/org/jabref/gui/JabRefDialogService.java +++ b/src/main/java/org/jabref/gui/JabRefDialogService.java @@ -41,7 +41,7 @@ import org.jabref.gui.help.ErrorConsoleAction; import org.jabref.gui.icon.IconTheme; -import org.jabref.gui.util.BackgroundTask; +import org.jabref.gui.theme.ThemeManager; import org.jabref.gui.util.BaseDialog; import org.jabref.gui.util.BaseWindow; import org.jabref.gui.util.DirectoryDialogConfiguration; @@ -392,7 +392,7 @@ public Optional showBackgroundProgressDialogAndWait(String title TaskProgressView> taskProgressView = new TaskProgressView<>(); EasyBind.bindContent(taskProgressView.getTasks(), stateManager.getRunningBackgroundTasks()); taskProgressView.setRetainTasks(false); - taskProgressView.setGraphicFactory(BackgroundTask::getIcon); + taskProgressView.setGraphicFactory(task -> ThemeManager.getDownloadIconTitleMap.getOrDefault(task.getTitle(), null)); Label message = new Label(content); diff --git a/src/main/java/org/jabref/gui/JabRefGUI.java b/src/main/java/org/jabref/gui/JabRefGUI.java index 3e09d3f5c39..4ba82c1fed5 100644 --- a/src/main/java/org/jabref/gui/JabRefGUI.java +++ b/src/main/java/org/jabref/gui/JabRefGUI.java @@ -14,16 +14,17 @@ import javafx.stage.Stage; import javafx.stage.WindowEvent; +import org.jabref.gui.ai.chatting.chathistory.ChatHistoryService; import org.jabref.gui.frame.JabRefFrame; import org.jabref.gui.help.VersionWorker; import org.jabref.gui.icon.IconTheme; import org.jabref.gui.keyboard.KeyBindingRepository; import org.jabref.gui.keyboard.TextInputKeyBindings; import org.jabref.gui.openoffice.OOBibBaseConnect; +import org.jabref.gui.preferences.GuiPreferences; import org.jabref.gui.remote.CLIMessageHandler; import org.jabref.gui.theme.ThemeManager; import org.jabref.gui.undo.CountingUndoManager; -import org.jabref.gui.util.TaskExecutor; import org.jabref.gui.util.UiTaskExecutor; import org.jabref.logic.UiCommand; import org.jabref.logic.ai.AiService; @@ -32,15 +33,14 @@ import org.jabref.logic.remote.RemotePreferences; import org.jabref.logic.remote.server.RemoteListenerServerManager; import org.jabref.logic.util.BuildInfo; +import org.jabref.logic.util.FallbackExceptionHandler; import org.jabref.logic.util.HeadlessExecutorService; +import org.jabref.logic.util.TaskExecutor; import org.jabref.logic.util.WebViewStore; import org.jabref.model.entry.BibEntryTypesManager; import org.jabref.model.strings.StringUtil; import org.jabref.model.util.DirectoryMonitor; import org.jabref.model.util.FileUpdateMonitor; -import org.jabref.preferences.GuiPreferences; -import org.jabref.preferences.JabRefPreferences; -import org.jabref.preferences.ai.AiApiKeyProvider; import com.airhacks.afterburner.injection.Injector; import com.tobiasdiez.easybind.EasyBind; @@ -56,11 +56,12 @@ public class JabRefGUI extends Application { private static final Logger LOGGER = LoggerFactory.getLogger(JabRefGUI.class); private static List uiCommands; - private static JabRefPreferences preferencesService; + private static GuiPreferences preferences; private static FileUpdateMonitor fileUpdateMonitor; // AI Service handles chat messages etc. Therefore, it is tightly coupled to the GUI. private static AiService aiService; + private static ChatHistoryService chatHistoryService; private static StateManager stateManager; private static ThemeManager themeManager; @@ -75,10 +76,10 @@ public class JabRefGUI extends Application { private Stage mainStage; public static void setup(List uiCommands, - JabRefPreferences preferencesService, + GuiPreferences preferences, FileUpdateMonitor fileUpdateMonitor) { JabRefGUI.uiCommands = uiCommands; - JabRefGUI.preferencesService = preferencesService; + JabRefGUI.preferences = preferences; JabRefGUI.fileUpdateMonitor = fileUpdateMonitor; } @@ -86,7 +87,12 @@ public static void setup(List uiCommands, public void start(Stage stage) { this.mainStage = stage; - FallbackExceptionHandler.installExceptionHandler(); + FallbackExceptionHandler.installExceptionHandler((exception, thread) -> { + UiTaskExecutor.runInJavaFXThread(() -> { + DialogService dialogService = Injector.instantiateModelOrService(DialogService.class); + dialogService.showErrorDialogAndWait("Uncaught exception occurred in " + thread, exception); + }); + }); initialize(); @@ -94,8 +100,9 @@ public void start(Stage stage) { mainStage, dialogService, fileUpdateMonitor, - preferencesService, + preferences, aiService, + chatHistoryService, stateManager, countingUndoManager, Injector.instantiateModelOrService(BibEntryTypesManager.class), @@ -114,12 +121,12 @@ public void start(Stage stage) { } BuildInfo buildInfo = Injector.instantiateModelOrService(BuildInfo.class); - EasyBind.subscribe(preferencesService.getInternalPreferences().versionCheckEnabledProperty(), enabled -> { + EasyBind.subscribe(preferences.getInternalPreferences().versionCheckEnabledProperty(), enabled -> { if (enabled) { new VersionWorker(buildInfo.version, dialogService, taskExecutor, - preferencesService) + preferences) .checkForNewVersionDelayed(); } }); @@ -136,10 +143,10 @@ public void initialize() { JabRefGUI.stateManager = new StateManager(); Injector.setModelOrService(StateManager.class, stateManager); - Injector.setModelOrService(KeyBindingRepository.class, preferencesService.getKeyBindingRepository()); + Injector.setModelOrService(KeyBindingRepository.class, preferences.getKeyBindingRepository()); JabRefGUI.themeManager = new ThemeManager( - preferencesService.getWorkspacePreferences(), + preferences.getWorkspacePreferences(), fileUpdateMonitor, Runnable::run); Injector.setModelOrService(ThemeManager.class, themeManager); @@ -148,6 +155,7 @@ public void initialize() { Injector.setModelOrService(UndoManager.class, countingUndoManager); Injector.setModelOrService(CountingUndoManager.class, countingUndoManager); + // our Default task executor is the UITaskExecutor which can use the fx thread JabRefGUI.taskExecutor = new UiTaskExecutor(); Injector.setModelOrService(TaskExecutor.class, taskExecutor); @@ -158,24 +166,28 @@ public void initialize() { Injector.setModelOrService(ClipBoardManager.class, clipBoardManager); JabRefGUI.aiService = new AiService( - preferencesService.getAiPreferences(), - preferencesService.getFilePreferences(), - preferencesService.getCitationKeyPatternPreferences(), - Injector.instantiateModelOrService(AiApiKeyProvider.class), + preferences.getAiPreferences(), + preferences.getFilePreferences(), + preferences.getCitationKeyPatternPreferences(), dialogService, taskExecutor); Injector.setModelOrService(AiService.class, aiService); + + JabRefGUI.chatHistoryService = new ChatHistoryService( + preferences.getCitationKeyPatternPreferences(), + dialogService); + Injector.setModelOrService(ChatHistoryService.class, chatHistoryService); } private void setupProxy() { - if (!preferencesService.getProxyPreferences().shouldUseProxy() - || !preferencesService.getProxyPreferences().shouldUseAuthentication()) { + if (!preferences.getProxyPreferences().shouldUseProxy() + || !preferences.getProxyPreferences().shouldUseAuthentication()) { return; } - if (preferencesService.getProxyPreferences().shouldPersistPassword() - && StringUtil.isNotBlank(preferencesService.getProxyPreferences().getPassword())) { - ProxyRegisterer.register(preferencesService.getProxyPreferences()); + if (preferences.getProxyPreferences().shouldPersistPassword() + && StringUtil.isNotBlank(preferences.getProxyPreferences().getPassword())) { + ProxyRegisterer.register(preferences.getProxyPreferences()); return; } @@ -185,8 +197,8 @@ private void setupProxy() { Localization.lang("Password")); if (password.isPresent()) { - preferencesService.getProxyPreferences().setPassword(password.get()); - ProxyRegisterer.register(preferencesService.getProxyPreferences()); + preferences.getProxyPreferences().setPassword(password.get()); + ProxyRegisterer.register(preferences.getProxyPreferences()); } else { LOGGER.warn("No proxy password specified"); } @@ -195,24 +207,24 @@ private void setupProxy() { private void openWindow() { LOGGER.debug("Initializing frame"); - GuiPreferences guiPreferences = preferencesService.getGuiPreferences(); - LOGGER.debug("Reading from prefs: isMaximized {}", guiPreferences.isWindowMaximised()); + CoreGuiPreferences coreGuiPreferences = preferences.getGuiPreferences(); + LOGGER.debug("Reading from prefs: isMaximized {}", coreGuiPreferences.isWindowMaximised()); mainStage.setMinWidth(580); mainStage.setMinHeight(330); // maximized target state is stored, because "saveWindowState" saves x and y only if not maximized - boolean windowMaximised = guiPreferences.isWindowMaximised(); + boolean windowMaximised = coreGuiPreferences.isWindowMaximised(); LOGGER.debug("Screens: {}", Screen.getScreens()); debugLogWindowState(mainStage); if (isWindowPositionInBounds()) { LOGGER.debug("The JabRef window is inside screen bounds."); - mainStage.setX(guiPreferences.getPositionX()); - mainStage.setY(guiPreferences.getPositionY()); - mainStage.setWidth(guiPreferences.getSizeX()); - mainStage.setHeight(guiPreferences.getSizeY()); + mainStage.setX(coreGuiPreferences.getPositionX()); + mainStage.setY(coreGuiPreferences.getPositionY()); + mainStage.setWidth(coreGuiPreferences.getSizeX()); + mainStage.setHeight(coreGuiPreferences.getSizeY()); LOGGER.debug("NOT saving window positions"); } else { LOGGER.info("The JabRef window is outside of screen bounds. Position and size will be corrected to 1024x768. Primary screen will be used."); @@ -237,7 +249,7 @@ private void openWindow() { scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> TextInputKeyBindings.call( scene, event, - preferencesService.getKeyBindingRepository())); + preferences.getKeyBindingRepository())); mainStage.setTitle(JabRefFrame.FRAME_TITLE); mainStage.getIcons().addAll(IconTheme.getLogoSetFX()); @@ -259,7 +271,7 @@ public void onShowing(WindowEvent event) { // Open last edited databases if (uiCommands.stream().noneMatch(UiCommand.BlankWorkspace.class::isInstance) - && preferencesService.getWorkspacePreferences().shouldOpenLastEdited()) { + && preferences.getWorkspacePreferences().shouldOpenLastEdited()) { mainFrame.openLastEditedDatabases(); } } @@ -272,12 +284,12 @@ public void onCloseRequest(WindowEvent event) { public void onHiding(WindowEvent event) { saveWindowState(); - preferencesService.flush(); + preferences.flush(); Platform.exit(); } private void saveWindowState() { - GuiPreferences preferences = preferencesService.getGuiPreferences(); + CoreGuiPreferences preferences = JabRefGUI.preferences.getGuiPreferences(); if (!mainStage.isMaximized()) { preferences.setPositionX(mainStage.getX()); preferences.setPositionY(mainStage.getY()); @@ -309,19 +321,19 @@ private void debugLogWindowState(Stage mainStage) { * Tests if the window coordinates are inside any screen */ private boolean isWindowPositionInBounds() { - GuiPreferences guiPreferences = preferencesService.getGuiPreferences(); + CoreGuiPreferences coreGuiPreferences = preferences.getGuiPreferences(); if (LOGGER.isDebugEnabled()) { Screen.getScreens().forEach(screen -> LOGGER.debug("Screen bounds: {}", screen.getBounds())); } - return lowerLeftIsInBounds(guiPreferences) && upperRightIsInBounds(guiPreferences); + return lowerLeftIsInBounds(coreGuiPreferences) && upperRightIsInBounds(coreGuiPreferences); } - private boolean lowerLeftIsInBounds(GuiPreferences guiPreferences) { + private boolean lowerLeftIsInBounds(CoreGuiPreferences coreGuiPreferences) { // Windows/PowerToys somehow removes 10 pixels to the left; they are re-added - double leftX = guiPreferences.getPositionX() + 10.0; - double bottomY = guiPreferences.getPositionY() + guiPreferences.getSizeY(); + double leftX = coreGuiPreferences.getPositionX() + 10.0; + double bottomY = coreGuiPreferences.getPositionY() + coreGuiPreferences.getSizeY(); LOGGER.debug("left x: {}, bottom y: {}", leftX, bottomY); boolean inBounds = Screen.getScreens().stream().anyMatch((screen -> screen.getBounds().contains(leftX, bottomY))); @@ -329,11 +341,11 @@ private boolean lowerLeftIsInBounds(GuiPreferences guiPreferences) { return inBounds; } - private boolean upperRightIsInBounds(GuiPreferences guiPreferences) { + private boolean upperRightIsInBounds(CoreGuiPreferences coreGuiPreferences) { // The upper right corner is checked as there are most probably the window controls. // Windows/PowerToys somehow adds 10 pixels to the right and top of the screen, they are removed - double rightX = guiPreferences.getPositionX() + guiPreferences.getSizeX() - 10.0; - double topY = guiPreferences.getPositionY(); + double rightX = coreGuiPreferences.getPositionX() + coreGuiPreferences.getSizeX() - 10.0; + double topY = coreGuiPreferences.getPositionY(); LOGGER.debug("right x: {}, top y: {}", rightX, topY); boolean inBounds = Screen.getScreens().stream().anyMatch((screen -> screen.getBounds().contains(rightX, topY))); @@ -343,13 +355,13 @@ private boolean upperRightIsInBounds(GuiPreferences guiPreferences) { // Background tasks public void startBackgroundTasks() { - RemotePreferences remotePreferences = preferencesService.getRemotePreferences(); + RemotePreferences remotePreferences = preferences.getRemotePreferences(); BibEntryTypesManager bibEntryTypesManager = Injector.instantiateModelOrService(BibEntryTypesManager.class); if (remotePreferences.useRemoteServer()) { remoteListenerServerManager.openAndStart( new CLIMessageHandler( mainFrame, - preferencesService, + preferences, fileUpdateMonitor, bibEntryTypesManager), remotePreferences.getPort()); diff --git a/src/main/java/org/jabref/gui/LibraryTab.java b/src/main/java/org/jabref/gui/LibraryTab.java index 0542c7023fe..93901045848 100644 --- a/src/main/java/org/jabref/gui/LibraryTab.java +++ b/src/main/java/org/jabref/gui/LibraryTab.java @@ -1,5 +1,6 @@ package org.jabref.gui; +import java.io.IOException; import java.nio.file.Path; import java.util.Collections; import java.util.List; @@ -46,12 +47,14 @@ import org.jabref.gui.dialogs.AutosaveUiManager; import org.jabref.gui.entryeditor.EntryEditor; import org.jabref.gui.exporter.SaveDatabaseAction; +import org.jabref.gui.externalfiles.ImportHandler; import org.jabref.gui.fieldeditors.LinkedFileViewModel; import org.jabref.gui.importer.actions.OpenDatabaseAction; import org.jabref.gui.linkedfile.DeleteFileAction; import org.jabref.gui.maintable.BibEntryTableViewModel; import org.jabref.gui.maintable.MainTable; import org.jabref.gui.maintable.MainTableDataModel; +import org.jabref.gui.preferences.GuiPreferences; import org.jabref.gui.undo.CountingUndoManager; import org.jabref.gui.undo.NamedCompound; import org.jabref.gui.undo.RedoAction; @@ -59,19 +62,20 @@ import org.jabref.gui.undo.UndoableFieldChange; import org.jabref.gui.undo.UndoableInsertEntries; import org.jabref.gui.undo.UndoableRemoveEntries; -import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.OptionalObjectProperty; -import org.jabref.gui.util.TaskExecutor; import org.jabref.gui.util.UiTaskExecutor; -import org.jabref.logic.ai.AiService; import org.jabref.logic.citationstyle.CitationStyleCache; +import org.jabref.logic.importer.FetcherClientException; +import org.jabref.logic.importer.FetcherException; +import org.jabref.logic.importer.FetcherServerException; import org.jabref.logic.importer.ParserResult; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; import org.jabref.logic.pdf.FileAnnotationCache; import org.jabref.logic.search.LuceneManager; import org.jabref.logic.shared.DatabaseLocation; -import org.jabref.logic.util.UpdateField; +import org.jabref.logic.util.BackgroundTask; +import org.jabref.logic.util.TaskExecutor; import org.jabref.logic.util.io.FileUtil; import org.jabref.model.FieldChange; import org.jabref.model.database.BibDatabase; @@ -82,6 +86,7 @@ import org.jabref.model.entry.Author; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.entry.BibtexString; import org.jabref.model.entry.LinkedFile; import org.jabref.model.entry.event.EntriesEventSource; import org.jabref.model.entry.event.FieldChangedEvent; @@ -93,7 +98,6 @@ import org.jabref.model.util.DirectoryMonitor; import org.jabref.model.util.DirectoryMonitorManager; import org.jabref.model.util.FileUpdateMonitor; -import org.jabref.preferences.PreferencesService; import com.airhacks.afterburner.injection.Injector; import com.google.common.eventbus.Subscribe; @@ -117,8 +121,7 @@ private enum PanelMode { MAIN_TABLE, MAIN_TABLE_AND_ENTRY_EDITOR } private final LibraryTabContainer tabContainer; private final CountingUndoManager undoManager; private final DialogService dialogService; - private final PreferencesService preferencesService; - private final AiService aiService; + private final GuiPreferences preferences; private final FileUpdateMonitor fileUpdateMonitor; private final StateManager stateManager; private final BibEntryTypesManager entryTypesManager; @@ -162,6 +165,8 @@ private enum PanelMode { MAIN_TABLE, MAIN_TABLE_AND_ENTRY_EDITOR } private final ClipBoardManager clipBoardManager; private final TaskExecutor taskExecutor; private final DirectoryMonitorManager directoryMonitorManager; + + private ImportHandler importHandler; private LuceneManager luceneManager; /** @@ -173,8 +178,7 @@ private enum PanelMode { MAIN_TABLE, MAIN_TABLE_AND_ENTRY_EDITOR } private LibraryTab(BibDatabaseContext bibDatabaseContext, LibraryTabContainer tabContainer, DialogService dialogService, - PreferencesService preferencesService, - AiService aiService, + GuiPreferences preferences, StateManager stateManager, FileUpdateMonitor fileUpdateMonitor, BibEntryTypesManager entryTypesManager, @@ -186,8 +190,7 @@ private LibraryTab(BibDatabaseContext bibDatabaseContext, this.bibDatabaseContext = Objects.requireNonNull(bibDatabaseContext); this.undoManager = undoManager; this.dialogService = dialogService; - this.preferencesService = Objects.requireNonNull(preferencesService); - this.aiService = Objects.requireNonNull(aiService); + this.preferences = Objects.requireNonNull(preferences); this.stateManager = Objects.requireNonNull(stateManager); this.fileUpdateMonitor = fileUpdateMonitor; this.entryTypesManager = entryTypesManager; @@ -218,10 +221,18 @@ private void initializeComponentsAndListeners(boolean isDummyContext) { bibDatabaseContext.getMetaData().registerListener(this); this.selectedGroupsProperty = new SimpleListProperty<>(stateManager.getSelectedGroups(bibDatabaseContext)); - this.tableModel = new MainTableDataModel(getBibDatabaseContext(), preferencesService, taskExecutor, stateManager, getLuceneManager(), selectedGroupsProperty(), searchQueryProperty(), resultSizeProperty()); + this.tableModel = new MainTableDataModel(getBibDatabaseContext(), preferences, taskExecutor, stateManager, getLuceneManager(), selectedGroupsProperty(), searchQueryProperty(), resultSizeProperty()); new CitationStyleCache(bibDatabaseContext); - annotationCache = new FileAnnotationCache(bibDatabaseContext, preferencesService.getFilePreferences()); + annotationCache = new FileAnnotationCache(bibDatabaseContext, preferences.getFilePreferences()); + importHandler = new ImportHandler( + bibDatabaseContext, + preferences, + fileUpdateMonitor, + undoManager, + stateManager, + dialogService, + taskExecutor); setupMainPanel(); setupAutoCompletion(); @@ -234,7 +245,7 @@ private void initializeComponentsAndListeners(boolean isDummyContext) { // ensure that all entry changes mark the panel as changed this.bibDatabaseContext.getDatabase().registerListener(this); - this.getDatabase().registerListener(new UpdateTimestampListener(preferencesService)); + this.getDatabase().registerListener(new UpdateTimestampListener(preferences)); this.entryEditor = createEntryEditor(); @@ -292,7 +303,7 @@ private void onDatabaseLoadingStarted() { } private void onDatabaseLoadingSucceed(ParserResult result) { - OpenDatabaseAction.performPostOpenActions(result, dialogService, preferencesService); + OpenDatabaseAction.performPostOpenActions(result, dialogService, preferences); if (result.getChangedOnMigration()) { this.markBaseChanged(); } @@ -305,7 +316,7 @@ private void onDatabaseLoadingSucceed(ParserResult result) { } public void createLuceneManager() { - luceneManager = new LuceneManager(bibDatabaseContext, taskExecutor, preferencesService.getFilePreferences()); + luceneManager = new LuceneManager(bibDatabaseContext, taskExecutor, preferences.getFilePreferences()); stateManager.setLuceneManager(bibDatabaseContext, luceneManager); } @@ -353,17 +364,17 @@ private void setDatabaseContext(BibDatabaseContext bibDatabaseContext) { public void installAutosaveManagerAndBackupManager() { if (isDatabaseReadyForAutoSave(bibDatabaseContext)) { AutosaveManager autosaveManager = AutosaveManager.start(bibDatabaseContext); - autosaveManager.registerListener(new AutosaveUiManager(this, dialogService, preferencesService, entryTypesManager)); + autosaveManager.registerListener(new AutosaveUiManager(this, dialogService, preferences, entryTypesManager)); } - if (isDatabaseReadyForBackup(bibDatabaseContext) && preferencesService.getFilePreferences().shouldCreateBackup()) { - BackupManager.start(this, bibDatabaseContext, Injector.instantiateModelOrService(BibEntryTypesManager.class), preferencesService); + if (isDatabaseReadyForBackup(bibDatabaseContext) && preferences.getFilePreferences().shouldCreateBackup()) { + BackupManager.start(this, bibDatabaseContext, Injector.instantiateModelOrService(BibEntryTypesManager.class), preferences); } } private boolean isDatabaseReadyForAutoSave(BibDatabaseContext context) { return ((context.getLocation() == DatabaseLocation.SHARED) || ((context.getLocation() == DatabaseLocation.LOCAL) - && preferencesService.getLibraryPreferences().shouldAutoSave())) + && preferences.getLibraryPreferences().shouldAutoSave())) && context.getDatabasePath().isPresent(); } @@ -379,7 +390,7 @@ private boolean isDatabaseReadyForBackup(BibDatabaseContext context) { * Example: *jabref-authors.bib – testbib */ public void updateTabTitle(boolean isChanged) { - boolean isAutosaveEnabled = preferencesService.getLibraryPreferences().shouldAutoSave(); + boolean isAutosaveEnabled = preferences.getLibraryPreferences().shouldAutoSave(); DatabaseLocation databaseLocation = bibDatabaseContext.getLocation(); Optional file = bibDatabaseContext.getDatabasePath(); @@ -449,63 +460,6 @@ public SuggestionProviders getSuggestionProviders() { return suggestionProviders; } - /** - * Removes the selected entries and files linked to selected entries from the database - * - * @param mode If DELETE_ENTRY the user will get asked if he really wants to delete the entries, and it will be localized as "deleted". If true the action will be localized as "cut" - */ - public void delete(StandardActions mode) { - delete(mode, mainTable.getSelectedEntries()); - } - - /** - * Removes the selected entries and files linked to selected entries from the database - * - * @param mode If DELETE_ENTRY the user will get asked if he really wants to delete the entries, and it will be localized as "deleted". If true the action will be localized as "cut" - */ - private void delete(StandardActions mode, List entries) { - if (entries.isEmpty()) { - return; - } - if (mode == StandardActions.DELETE_ENTRY && !showDeleteConfirmationDialog(entries.size())) { - return; - } - - // Delete selected entries - getUndoManager().addEdit(new UndoableRemoveEntries(bibDatabaseContext.getDatabase(), entries, mode == StandardActions.CUT)); - bibDatabaseContext.getDatabase().removeEntries(entries); - - if (mode != StandardActions.CUT) { - List linkedFileList = entries.stream() - .flatMap(entry -> entry.getFiles().stream()) - .distinct() - .toList(); - - if (!linkedFileList.isEmpty()) { - List viewModels = linkedFileList.stream() - .map(linkedFile -> linkedFile.toModel(null, bibDatabaseContext, null, null, preferencesService)) - .collect(Collectors.toList()); - - new DeleteFileAction(dialogService, preferencesService.getFilePreferences(), bibDatabaseContext, viewModels).execute(); - } - } - - ensureNotShowingBottomPanel(entries); - - this.changedProperty.setValue(true); - switch (mode) { - case StandardActions.CUT -> dialogService.notify(Localization.lang("Cut %0 entry(ies)", entries.size())); - case StandardActions.DELETE_ENTRY -> dialogService.notify(Localization.lang("Deleted %0 entry(ies)", entries.size())); - } - - // prevent the main table from loosing focus - mainTable.requestFocus(); - } - - public void delete(BibEntry entry) { - delete(StandardActions.DELETE_ENTRY, Collections.singletonList(entry)); - } - public void registerUndoableChanges(List changes) { NamedCompound ce = new NamedCompound(Localization.lang("Save actions")); for (FieldChange change : changes) { @@ -517,31 +471,6 @@ public void registerUndoableChanges(List changes) { } } - public void insertEntry(final BibEntry bibEntry) { - if (bibEntry != null) { - insertEntries(Collections.singletonList(bibEntry)); - } - } - - public void insertEntries(final List entries) { - if (!entries.isEmpty()) { - bibDatabaseContext.getDatabase().insertEntries(entries); - - // Set owner and timestamp - UpdateField.setAutomaticFields(entries, - preferencesService.getOwnerPreferences(), - preferencesService.getTimestampPreferences()); - // Create an UndoableInsertEntries object. - getUndoManager().addEdit(new UndoableInsertEntries(bibDatabaseContext.getDatabase(), entries)); - - this.changedProperty.setValue(true); // The database just changed. - if (preferencesService.getEntryEditorPreferences().shouldOpenOnNewEntry()) { - showAndEdit(entries.getFirst()); - } - clearAndSelect(entries.getFirst()); - } - } - public void editEntryAndFocusField(BibEntry entry, Field field) { showAndEdit(entry); Platform.runLater(() -> { @@ -556,14 +485,14 @@ private void createMainTable() { this, tabContainer, bibDatabaseContext, - preferencesService, + preferences, dialogService, stateManager, - preferencesService.getKeyBindingRepository(), + preferences.getKeyBindingRepository(), clipBoardManager, entryTypesManager, taskExecutor, - fileUpdateMonitor); + importHandler); // Add the listener that binds selection to state manager (TODO: should be replaced by proper JavaFX binding as soon as table is implemented in JavaFX) // content binding between StateManager#getselectedEntries and mainTable#getSelectedEntries does not work here as it does not trigger the ActionHelper#needsEntriesSelected checker for the menubar mainTable.addSelectionListener(event -> { @@ -610,7 +539,7 @@ public void setupMainPanel() { * Set up autocompletion for this database */ private void setupAutoCompletion() { - AutoCompletePreferences autoCompletePreferences = preferencesService.getAutoCompletePreferences(); + AutoCompletePreferences autoCompletePreferences = preferences.getAutoCompletePreferences(); if (autoCompletePreferences.shouldAutoComplete()) { suggestionProviders = new SuggestionProviders( getDatabase(), @@ -640,7 +569,7 @@ public void showAndEdit(BibEntry entry) { if (!splitPane.getItems().contains(entryEditor)) { splitPane.getItems().addLast(entryEditor); mode = PanelMode.MAIN_TABLE_AND_ENTRY_EDITOR; - splitPane.setDividerPositions(preferencesService.getEntryEditorPreferences().getDividerPosition()); + splitPane.setDividerPositions(preferences.getEntryEditorPreferences().getDividerPosition()); } // We use != instead of equals because of performance reasons @@ -724,7 +653,7 @@ public BibDatabase getDatabase() { * @return true if user confirm to delete entry */ private boolean showDeleteConfirmationDialog(int numberOfEntries) { - if (preferencesService.getWorkspacePreferences().shouldConfirmDelete()) { + if (preferences.getWorkspacePreferences().shouldConfirmDelete()) { String title = Localization.lang("Delete entry"); String message = Localization.lang("Really delete the selected entry?"); String okButton = Localization.lang("Delete entry"); @@ -742,7 +671,7 @@ private boolean showDeleteConfirmationDialog(int numberOfEntries) { okButton, cancelButton, Localization.lang("Do not ask again"), - optOut -> preferencesService.getWorkspacePreferences().setConfirmDelete(!optOut)); + optOut -> preferences.getWorkspacePreferences().setConfirmDelete(!optOut)); } else { return true; } @@ -753,7 +682,7 @@ private boolean showDeleteConfirmationDialog(int numberOfEntries) { */ private void saveDividerLocation(Number position) { if (mode == PanelMode.MAIN_TABLE_AND_ENTRY_EDITOR) { - preferencesService.getEntryEditorPreferences().setDividerPosition(position.doubleValue()); + preferences.getEntryEditorPreferences().setDividerPosition(position.doubleValue()); } } @@ -807,7 +736,7 @@ private boolean confirmClose() { if (buttonType.equals(saveChanges)) { try { - SaveDatabaseAction saveAction = new SaveDatabaseAction(this, dialogService, preferencesService, Injector.instantiateModelOrService(BibEntryTypesManager.class)); + SaveDatabaseAction saveAction = new SaveDatabaseAction(this, dialogService, preferences, Injector.instantiateModelOrService(BibEntryTypesManager.class)); if (saveAction.save()) { return true; } @@ -822,7 +751,7 @@ private boolean confirmClose() { } if (buttonType.equals(discardChanges)) { - BackupManager.discardBackup(bibDatabaseContext, preferencesService.getFilePreferences().getBackupDirectory()); + BackupManager.discardBackup(bibDatabaseContext, preferences.getFilePreferences().getBackupDirectory()); return true; } @@ -871,8 +800,8 @@ private void onClosed(Event event) { } try { BackupManager.shutdown(bibDatabaseContext, - preferencesService.getFilePreferences().getBackupDirectory(), - preferencesService.getFilePreferences().shouldCreateBackup()); + preferences.getFilePreferences().getBackupDirectory(), + preferences.getFilePreferences().shouldCreateBackup()); } catch (RuntimeException e) { LOGGER.error("Problem when shutting down backup manager", e); } @@ -943,26 +872,157 @@ public void resetChangeMonitor() { fileUpdateMonitor, taskExecutor, dialogService, - preferencesService, + preferences, databaseNotificationPane, undoManager, stateManager)); } - public void copy() { - mainTable.copy(); + public void insertEntry(final BibEntry bibEntry) { + insertEntries(List.of(bibEntry)); } - public void paste() { - mainTable.paste(); + public void insertEntries(final List entries) { + if (!entries.isEmpty()) { + importHandler.importCleanedEntries(entries); + + // Create an UndoableInsertEntries object. + getUndoManager().addEdit(new UndoableInsertEntries(bibDatabaseContext.getDatabase(), entries)); + + markBaseChanged(); + if (preferences.getEntryEditorPreferences().shouldOpenOnNewEntry()) { + showAndEdit(entries.getFirst()); + } + clearAndSelect(entries.getFirst()); + } + } + + public void copyEntry() { + int entriesCopied = doCopyEntry(getSelectedEntries()); + if (entriesCopied >= 0) { + dialogService.notify(Localization.lang("Copied %0 entry(ies)", entriesCopied)); + } else { + dialogService.notify(Localization.lang("Copy failed", entriesCopied)); + } + } + + private int doCopyEntry(List selectedEntries) { + if (!selectedEntries.isEmpty()) { + List stringConstants = bibDatabaseContext.getDatabase().getUsedStrings(selectedEntries); + try { + if (stringConstants.isEmpty()) { + clipBoardManager.setContent(selectedEntries, entryTypesManager); + } else { + clipBoardManager.setContent(selectedEntries, entryTypesManager, stringConstants); + } + return selectedEntries.size(); + } catch (IOException e) { + LOGGER.error("Error while copying selected entries to clipboard.", e); + return -1; + } + } + + return 0; + } + + public void pasteEntry() { + List entriesToAdd; + String content = ClipBoardManager.getContents(); + entriesToAdd = importHandler.handleBibTeXData(content); + if (entriesToAdd.isEmpty()) { + entriesToAdd = handleNonBibTeXStringData(content); + } + if (entriesToAdd.isEmpty()) { + return; + } + + importHandler.importEntriesWithDuplicateCheck(bibDatabaseContext, entriesToAdd); + } + + private List handleNonBibTeXStringData(String data) { + try { + return this.importHandler.handleStringData(data); + } catch ( + FetcherException exception) { + if (exception instanceof FetcherClientException) { + dialogService.showInformationDialogAndWait(Localization.lang("Look up identifier"), Localization.lang("No data was found for the identifier")); + } else if (exception instanceof FetcherServerException) { + dialogService.showInformationDialogAndWait(Localization.lang("Look up identifier"), Localization.lang("Server not available")); + } else { + dialogService.showErrorDialogAndWait(exception); + } + return List.of(); + } } public void dropEntry(List entriesToAdd) { - mainTable.dropEntry(entriesToAdd); + importHandler.importEntriesWithDuplicateCheck(bibDatabaseContext, entriesToAdd); } - public void cut() { - mainTable.cut(); + public void cutEntry() { + int entriesCopied = doCopyEntry(getSelectedEntries()); + int entriesDeleted = doDeleteEntry(StandardActions.CUT, mainTable.getSelectedEntries()); + + if (entriesCopied == entriesDeleted) { + dialogService.notify(Localization.lang("Cut %0 entry(ies)", entriesCopied)); + } else { + dialogService.notify(Localization.lang("Cut failed", entriesCopied)); + undoManager.undo(); + clipBoardManager.setContent(""); + } + } + + /** + * Removes the selected entries and files linked to selected entries from the database + */ + public void deleteEntry() { + int entriesDeleted = doDeleteEntry(StandardActions.DELETE_ENTRY, mainTable.getSelectedEntries()); + dialogService.notify(Localization.lang("Deleted %0 entry(ies)", entriesDeleted)); + } + + public void deleteEntry(BibEntry entry) { + doDeleteEntry(StandardActions.DELETE_ENTRY, Collections.singletonList(entry)); + } + + /** + * Removes the selected entries and files linked to selected entries from the database + * + * @param mode If DELETE_ENTRY the user will get asked if he really wants to delete the entries, and it will be localized as "deleted". If true the action will be localized as "cut" + */ + private int doDeleteEntry(StandardActions mode, List entries) { + if (entries.isEmpty()) { + return 0; + } + if (mode == StandardActions.DELETE_ENTRY && !showDeleteConfirmationDialog(entries.size())) { + return -1; + } + + // Delete selected entries + getUndoManager().addEdit(new UndoableRemoveEntries(bibDatabaseContext.getDatabase(), entries, mode == StandardActions.CUT)); + bibDatabaseContext.getDatabase().removeEntries(entries); + + if (mode != StandardActions.CUT) { + List linkedFileList = entries.stream() + .flatMap(entry -> entry.getFiles().stream()) + .distinct() + .toList(); + + if (!linkedFileList.isEmpty()) { + List viewModels = linkedFileList.stream() + .map(linkedFile -> LinkedFileViewModel.fromLinkedFile(linkedFile, null, bibDatabaseContext, null, null, preferences)) + .collect(Collectors.toList()); + + new DeleteFileAction(dialogService, preferences.getFilePreferences(), bibDatabaseContext, viewModels).execute(); + } + } + + ensureNotShowingBottomPanel(entries); + markBaseChanged(); + + // prevent the main table from loosing focus + mainTable.requestFocus(); + + return entries.size(); } public boolean isModified() { @@ -992,8 +1052,7 @@ public void resetChangedProperties() { public static LibraryTab createLibraryTab(BackgroundTask dataLoadingTask, Path file, DialogService dialogService, - PreferencesService preferencesService, - AiService aiService, + GuiPreferences preferences, StateManager stateManager, LibraryTabContainer tabContainer, FileUpdateMonitor fileUpdateMonitor, @@ -1008,8 +1067,7 @@ public static LibraryTab createLibraryTab(BackgroundTask dataLoadi context, tabContainer, dialogService, - preferencesService, - aiService, + preferences, stateManager, fileUpdateMonitor, entryTypesManager, @@ -1030,8 +1088,7 @@ public static LibraryTab createLibraryTab(BackgroundTask dataLoadi public static LibraryTab createLibraryTab(BibDatabaseContext databaseContext, LibraryTabContainer tabContainer, DialogService dialogService, - PreferencesService preferencesService, - AiService aiService, + GuiPreferences preferences, StateManager stateManager, FileUpdateMonitor fileUpdateMonitor, BibEntryTypesManager entryTypesManager, @@ -1044,8 +1101,7 @@ public static LibraryTab createLibraryTab(BibDatabaseContext databaseContext, databaseContext, tabContainer, dialogService, - preferencesService, - aiService, + preferences, stateManager, fileUpdateMonitor, entryTypesManager, @@ -1065,7 +1121,7 @@ public void listen(EntriesAddedEvent addedEntriesEvent) { } // Automatically add new entries to the selected group (or set of groups) - if (preferencesService.getGroupsPreferences().shouldAutoAssignGroup()) { + if (preferences.getGroupsPreferences().shouldAutoAssignGroup()) { stateManager.getSelectedGroups(bibDatabaseContext).forEach( selectedGroup -> selectedGroup.addEntriesToGroup(addedEntriesEvent.getBibEntries())); } diff --git a/src/main/java/org/jabref/gui/StateManager.java b/src/main/java/org/jabref/gui/StateManager.java index 42fd8d9f1bc..cf73b00da94 100644 --- a/src/main/java/org/jabref/gui/StateManager.java +++ b/src/main/java/org/jabref/gui/StateManager.java @@ -22,11 +22,11 @@ import org.jabref.gui.edit.automaticfiededitor.LastAutomaticFieldEditorEdit; import org.jabref.gui.search.SearchType; import org.jabref.gui.sidepane.SidePaneType; -import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.CustomLocalDragboard; import org.jabref.gui.util.DialogWindowState; import org.jabref.gui.util.OptionalObjectProperty; import org.jabref.logic.search.LuceneManager; +import org.jabref.logic.util.BackgroundTask; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.groups.GroupTreeNode; diff --git a/src/main/java/org/jabref/gui/UpdateTimestampListener.java b/src/main/java/org/jabref/gui/UpdateTimestampListener.java index 2ccc4a72d3c..b975773dee8 100644 --- a/src/main/java/org/jabref/gui/UpdateTimestampListener.java +++ b/src/main/java/org/jabref/gui/UpdateTimestampListener.java @@ -1,9 +1,9 @@ package org.jabref.gui; +import org.jabref.logic.preferences.CliPreferences; import org.jabref.model.entry.event.EntriesEventSource; import org.jabref.model.entry.event.EntryChangedEvent; import org.jabref.model.entry.field.StandardField; -import org.jabref.preferences.PreferencesService; import com.google.common.eventbus.Subscribe; @@ -11,19 +11,19 @@ * Updates the timestamp of changed entries if the feature is enabled */ class UpdateTimestampListener { - private final PreferencesService preferencesService; + private final CliPreferences preferences; - UpdateTimestampListener(PreferencesService preferencesService) { - this.preferencesService = preferencesService; + UpdateTimestampListener(CliPreferences preferences) { + this.preferences = preferences; } @Subscribe public void listen(EntryChangedEvent event) { // The event source needs to be checked, since the timestamp is always updated on every change. The cleanup formatter is an exception to that behaviour, // since it just should move the contents from the timestamp field to modificationdate or creationdate. - if (preferencesService.getTimestampPreferences().shouldAddModificationDate() && event.getEntriesEventSource() != EntriesEventSource.CLEANUP_TIMESTAMP) { + if (preferences.getTimestampPreferences().shouldAddModificationDate() && event.getEntriesEventSource() != EntriesEventSource.CLEANUP_TIMESTAMP) { event.getBibEntry().setField(StandardField.MODIFICATIONDATE, - preferencesService.getTimestampPreferences().now()); + preferences.getTimestampPreferences().now()); } } } diff --git a/src/main/java/org/jabref/preferences/WorkspacePreferences.java b/src/main/java/org/jabref/gui/WorkspacePreferences.java similarity index 99% rename from src/main/java/org/jabref/preferences/WorkspacePreferences.java rename to src/main/java/org/jabref/gui/WorkspacePreferences.java index 57d97a971b6..2948be7582e 100644 --- a/src/main/java/org/jabref/preferences/WorkspacePreferences.java +++ b/src/main/java/org/jabref/gui/WorkspacePreferences.java @@ -1,4 +1,4 @@ -package org.jabref.preferences; +package org.jabref.gui; import java.util.List; diff --git a/src/main/java/org/jabref/gui/actions/ActionHelper.java b/src/main/java/org/jabref/gui/actions/ActionHelper.java index 20376f2f3e2..437d38d9bfe 100644 --- a/src/main/java/org/jabref/gui/actions/ActionHelper.java +++ b/src/main/java/org/jabref/gui/actions/ActionHelper.java @@ -12,13 +12,13 @@ import javafx.scene.control.TabPane; import org.jabref.gui.StateManager; +import org.jabref.logic.preferences.CliPreferences; import org.jabref.logic.shared.DatabaseLocation; import org.jabref.logic.util.io.FileUtil; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.LinkedFile; import org.jabref.model.entry.field.Field; -import org.jabref.preferences.PreferencesService; import com.tobiasdiez.easybind.EasyBind; import com.tobiasdiez.easybind.EasyBinding; @@ -66,7 +66,7 @@ public static BooleanExpression isAnyFieldSetForSelectedEntry(List fields return BooleanExpression.booleanExpression(fieldsAreSet); } - public static BooleanExpression isFilePresentForSelectedEntry(StateManager stateManager, PreferencesService preferencesService) { + public static BooleanExpression isFilePresentForSelectedEntry(StateManager stateManager, CliPreferences preferences) { ObservableList selectedEntries = stateManager.getSelectedEntries(); Binding fileIsPresent = EasyBind.valueAt(selectedEntries, 0).mapOpt(entry -> { List files = entry.getFiles(); @@ -79,7 +79,7 @@ public static BooleanExpression isFilePresentForSelectedEntry(StateManager state Optional filename = FileUtil.find( stateManager.getActiveDatabase().get(), files.getFirst().getLink(), - preferencesService.getFilePreferences()); + preferences.getFilePreferences()); return filename.isPresent(); } else { return false; diff --git a/src/main/java/org/jabref/gui/ai/ClearEmbeddingsAction.java b/src/main/java/org/jabref/gui/ai/ClearEmbeddingsAction.java index 6b76d7af67d..91b3e926269 100644 --- a/src/main/java/org/jabref/gui/ai/ClearEmbeddingsAction.java +++ b/src/main/java/org/jabref/gui/ai/ClearEmbeddingsAction.java @@ -5,10 +5,10 @@ import org.jabref.gui.DialogService; import org.jabref.gui.StateManager; import org.jabref.gui.actions.SimpleCommand; -import org.jabref.gui.util.BackgroundTask; -import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.ai.AiService; import org.jabref.logic.l10n.Localization; +import org.jabref.logic.util.BackgroundTask; +import org.jabref.logic.util.TaskExecutor; import org.jabref.model.entry.LinkedFile; import static org.jabref.gui.actions.ActionHelper.needsDatabase; diff --git a/src/main/java/org/jabref/logic/ai/chatting/chathistory/ChatHistoryService.java b/src/main/java/org/jabref/gui/ai/chatting/chathistory/ChatHistoryService.java similarity index 93% rename from src/main/java/org/jabref/logic/ai/chatting/chathistory/ChatHistoryService.java rename to src/main/java/org/jabref/gui/ai/chatting/chathistory/ChatHistoryService.java index acbde14c3a3..872a263c7a1 100644 --- a/src/main/java/org/jabref/logic/ai/chatting/chathistory/ChatHistoryService.java +++ b/src/main/java/org/jabref/gui/ai/chatting/chathistory/ChatHistoryService.java @@ -1,4 +1,4 @@ -package org.jabref.logic.ai.chatting.chathistory; +package org.jabref.gui.ai.chatting.chathistory; import java.util.Comparator; import java.util.HashSet; @@ -11,9 +11,13 @@ import javafx.collections.ObservableList; import org.jabref.gui.StateManager; +import org.jabref.logic.ai.chatting.chathistory.ChatHistoryStorage; +import org.jabref.logic.ai.chatting.chathistory.storages.MVStoreChatHistoryStorage; import org.jabref.logic.ai.util.CitationKeyCheck; import org.jabref.logic.citationkeypattern.CitationKeyGenerator; import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences; +import org.jabref.logic.util.Directories; +import org.jabref.logic.util.NotificationService; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.event.FieldChangedEvent; @@ -29,7 +33,8 @@ /** * Main class for getting and storing chat history for entries and groups. - * Use this class in the logic and UI. + * Use this class in logic and UI. + * It currently resides in the UI package because it relies on the {@link StateManager} to get the open databases and to find the correct {@link BibDatabaseContext} based on an entry. *

* The returned chat history is a {@link ObservableList}. So chat history exists for every possible * {@link BibEntry} and {@link AbstractGroup}. The chat history is stored in runtime. @@ -51,6 +56,8 @@ public class ChatHistoryService implements AutoCloseable { private static final Logger LOGGER = LoggerFactory.getLogger(ChatHistoryService.class); + private static final String CHAT_HISTORY_FILE_NAME = "chat-histories.mv"; + private final StateManager stateManager = Injector.instantiateModelOrService(StateManager.class); private final CitationKeyPatternPreferences citationKeyPatternPreferences; @@ -72,6 +79,12 @@ private record ChatHistoryManagementRecord(Optional bibDatab return o1 == o2 ? 0 : o1.getGroup().getName().compareTo(o2.getGroup().getName()); }); + public ChatHistoryService(CitationKeyPatternPreferences citationKeyPatternPreferences, NotificationService notificationService) { + this.citationKeyPatternPreferences = citationKeyPatternPreferences; + this.implementation = new MVStoreChatHistoryStorage(Directories.getAiFilesDirectory().resolve(CHAT_HISTORY_FILE_NAME), notificationService); + configureHistoryTransfer(); + } + public ChatHistoryService(CitationKeyPatternPreferences citationKeyPatternPreferences, ChatHistoryStorage implementation) { this.citationKeyPatternPreferences = citationKeyPatternPreferences; diff --git a/src/main/java/org/jabref/gui/ai/components/aichat/AiChatComponent.fxml b/src/main/java/org/jabref/gui/ai/components/aichat/AiChatComponent.fxml index fa24208105e..a479ace35e6 100644 --- a/src/main/java/org/jabref/gui/ai/components/aichat/AiChatComponent.fxml +++ b/src/main/java/org/jabref/gui/ai/components/aichat/AiChatComponent.fxml @@ -1,15 +1,16 @@ - - - - - - - + + + + + + + - + + chatHistory aiService, dialogService, aiPreferences, - filePreferences, + externalApplicationsPreferences, taskExecutor ), 800, diff --git a/src/main/java/org/jabref/gui/ai/components/aichat/chathistory/ChatHistoryComponent.fxml b/src/main/java/org/jabref/gui/ai/components/aichat/chathistory/ChatHistoryComponent.fxml index b204fc467de..20836e0e3bb 100644 --- a/src/main/java/org/jabref/gui/ai/components/aichat/chathistory/ChatHistoryComponent.fxml +++ b/src/main/java/org/jabref/gui/ai/components/aichat/chathistory/ChatHistoryComponent.fxml @@ -1,9 +1,8 @@ - - - + + - - - - - - + + + + + + + + + diff --git a/src/main/java/org/jabref/gui/ai/components/aichat/chatprompt/ChatPromptComponent.fxml b/src/main/java/org/jabref/gui/ai/components/aichat/chatprompt/ChatPromptComponent.fxml index b933fc2b7bf..04c53eaab88 100644 --- a/src/main/java/org/jabref/gui/ai/components/aichat/chatprompt/ChatPromptComponent.fxml +++ b/src/main/java/org/jabref/gui/ai/components/aichat/chatprompt/ChatPromptComponent.fxml @@ -1,11 +1,7 @@ - - - - - - + + req~ai.chat.new-message-based-on-previous~1] if ((currentUserMessageScroll.get() < history.get().size() - 1) && (userPromptTextArea.getText().isEmpty() || showingHistoryMessage.get())) { // 1. We should not go up the maximum number of user messages. // 2. We can scroll history only on two conditions: diff --git a/src/main/java/org/jabref/gui/ai/components/privacynotice/AiPrivacyNoticeGuardedComponent.java b/src/main/java/org/jabref/gui/ai/components/privacynotice/AiPrivacyNoticeGuardedComponent.java index c8af00148ce..a286be359be 100644 --- a/src/main/java/org/jabref/gui/ai/components/privacynotice/AiPrivacyNoticeGuardedComponent.java +++ b/src/main/java/org/jabref/gui/ai/components/privacynotice/AiPrivacyNoticeGuardedComponent.java @@ -4,9 +4,9 @@ import org.jabref.gui.DialogService; import org.jabref.gui.ai.components.aichat.AiChatGuardedComponent; +import org.jabref.gui.frame.ExternalApplicationsPreferences; import org.jabref.gui.util.DynamicallyChangeableNode; -import org.jabref.preferences.FilePreferences; -import org.jabref.preferences.ai.AiPreferences; +import org.jabref.logic.ai.AiPreferences; /** * A class that guards a component, before AI privacy policy is accepted. @@ -14,12 +14,12 @@ */ public abstract class AiPrivacyNoticeGuardedComponent extends DynamicallyChangeableNode { private final AiPreferences aiPreferences; - private final FilePreferences filePreferences; + private final ExternalApplicationsPreferences externalApplicationsPreferences; private final DialogService dialogService; - public AiPrivacyNoticeGuardedComponent(AiPreferences aiPreferences, FilePreferences filePreferences, DialogService dialogService) { + public AiPrivacyNoticeGuardedComponent(AiPreferences aiPreferences, ExternalApplicationsPreferences externalApplicationsPreferences, DialogService dialogService) { this.aiPreferences = aiPreferences; - this.filePreferences = filePreferences; + this.externalApplicationsPreferences = externalApplicationsPreferences; this.dialogService = dialogService; aiPreferences.enableAiProperty().addListener(observable -> rebuildUi()); @@ -33,7 +33,7 @@ public final void rebuildUi() { new PrivacyNoticeComponent( aiPreferences, this::rebuildUi, - filePreferences, + externalApplicationsPreferences, dialogService ) ); diff --git a/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.fxml b/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.fxml index 91984a61731..2f439beac31 100644 --- a/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.fxml +++ b/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.fxml @@ -1,10 +1,14 @@ - - - - - + + + + + + + + + @@ -28,7 +32,7 @@ - + @@ -37,16 +41,25 @@ - + + + + + + + + + + - + diff --git a/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.java b/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.java index cb257d4cf80..9681379d60d 100644 --- a/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.java +++ b/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.java @@ -9,9 +9,11 @@ import javafx.scene.text.TextFlow; import org.jabref.gui.DialogService; -import org.jabref.gui.desktop.JabRefDesktop; -import org.jabref.preferences.FilePreferences; -import org.jabref.preferences.ai.AiPreferences; +import org.jabref.gui.desktop.os.NativeDesktop; +import org.jabref.gui.frame.ExternalApplicationsPreferences; +import org.jabref.logic.ai.AiDefaultPreferences; +import org.jabref.logic.ai.AiPreferences; +import org.jabref.model.ai.AiProvider; import com.airhacks.afterburner.views.ViewLoader; import org.slf4j.Logger; @@ -22,19 +24,20 @@ public class PrivacyNoticeComponent extends ScrollPane { @FXML private TextFlow openAiPrivacyTextFlow; @FXML private TextFlow mistralAiPrivacyTextFlow; + @FXML private TextFlow geminiPrivacyTextFlow; @FXML private TextFlow huggingFacePrivacyTextFlow; @FXML private Text embeddingModelText; private final AiPreferences aiPreferences; private final Runnable onIAgreeButtonClickCallback; private final DialogService dialogService; - private final FilePreferences filePreferences; + private final ExternalApplicationsPreferences externalApplicationsPreferences; - public PrivacyNoticeComponent(AiPreferences aiPreferences, Runnable onIAgreeButtonClickCallback, FilePreferences filePreferences, DialogService dialogService) { + public PrivacyNoticeComponent(AiPreferences aiPreferences, Runnable onIAgreeButtonClickCallback, ExternalApplicationsPreferences externalApplicationsPreferences, DialogService dialogService) { this.aiPreferences = aiPreferences; this.onIAgreeButtonClickCallback = onIAgreeButtonClickCallback; + this.externalApplicationsPreferences = externalApplicationsPreferences; this.dialogService = dialogService; - this.filePreferences = filePreferences; ViewLoader.view(this) .root(this) @@ -43,9 +46,10 @@ public PrivacyNoticeComponent(AiPreferences aiPreferences, Runnable onIAgreeButt @FXML private void initialize() { - initPrivacyHyperlink(openAiPrivacyTextFlow, "https://openai.com/policies/privacy-policy/"); - initPrivacyHyperlink(mistralAiPrivacyTextFlow, "https://mistral.ai/terms/#privacy-policy"); - initPrivacyHyperlink(huggingFacePrivacyTextFlow, "https://huggingface.co/privacy"); + initPrivacyHyperlink(openAiPrivacyTextFlow, AiProvider.OPEN_AI); + initPrivacyHyperlink(mistralAiPrivacyTextFlow, AiProvider.MISTRAL_AI); + initPrivacyHyperlink(geminiPrivacyTextFlow, AiProvider.GEMINI); + initPrivacyHyperlink(huggingFacePrivacyTextFlow, AiProvider.HUGGING_FACE); String newEmbeddingModelText = embeddingModelText.getText().replaceAll("%0", aiPreferences.getEmbeddingModel().sizeInfo()); embeddingModelText.setText(newEmbeddingModelText); @@ -56,20 +60,19 @@ private void initialize() { embeddingModelText.wrappingWidthProperty().bind(this.widthProperty()); } - private void initPrivacyHyperlink(TextFlow textFlow, String link) { + private void initPrivacyHyperlink(TextFlow textFlow, AiProvider aiProvider) { if (textFlow.getChildren().isEmpty() || !(textFlow.getChildren().getFirst() instanceof Text text)) { return; } - String[] stringArray = text.getText().split("%0"); + String replacedText = text.getText().replaceAll("%0", aiProvider.getLabel()).replace("%1", ""); - if (stringArray.length != 2) { - return; - } + replacedText = replacedText.endsWith(".") ? replacedText.substring(0, replacedText.length() - 1) : replacedText; + text.setText(replacedText); text.wrappingWidthProperty().bind(this.widthProperty()); - text.setText(stringArray[0]); + String link = AiDefaultPreferences.PROVIDERS_PRIVACY_POLICIES.get(aiProvider); Hyperlink hyperlink = new Hyperlink(link); hyperlink.setWrapText(true); hyperlink.setFont(text.getFont()); @@ -79,11 +82,11 @@ private void initPrivacyHyperlink(TextFlow textFlow, String link) { textFlow.getChildren().add(hyperlink); - Text postText = new Text(stringArray[1]); - postText.setFont(text.getFont()); - postText.wrappingWidthProperty().bind(this.widthProperty()); + Text dot = new Text("."); + dot.setFont(text.getFont()); + dot.wrappingWidthProperty().bind(this.widthProperty()); - textFlow.getChildren().add(postText); + textFlow.getChildren().add(dot); } @FXML @@ -99,7 +102,7 @@ private void onDjlPrivacyPolicyClick() { private void openBrowser(String link) { try { - JabRefDesktop.openBrowser(link, filePreferences); + NativeDesktop.openBrowser(link, externalApplicationsPreferences); } catch (IOException e) { LOGGER.error("Error opening the browser to the Privacy Policy page of the AI provider.", e); dialogService.showErrorDialogAndWait(e); diff --git a/src/main/java/org/jabref/gui/ai/components/summary/SummaryComponent.java b/src/main/java/org/jabref/gui/ai/components/summary/SummaryComponent.java index 3a164c2739c..ed8e66980e0 100644 --- a/src/main/java/org/jabref/gui/ai/components/summary/SummaryComponent.java +++ b/src/main/java/org/jabref/gui/ai/components/summary/SummaryComponent.java @@ -7,6 +7,8 @@ import org.jabref.gui.DialogService; import org.jabref.gui.ai.components.privacynotice.AiPrivacyNoticeGuardedComponent; import org.jabref.gui.ai.components.util.errorstate.ErrorStateComponent; +import org.jabref.gui.frame.ExternalApplicationsPreferences; +import org.jabref.logic.ai.AiPreferences; import org.jabref.logic.ai.AiService; import org.jabref.logic.ai.processingstatus.ProcessingInfo; import org.jabref.logic.ai.summarization.Summary; @@ -18,8 +20,6 @@ import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.LinkedFile; -import org.jabref.preferences.FilePreferences; -import org.jabref.preferences.ai.AiPreferences; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,11 +37,11 @@ public SummaryComponent(BibDatabaseContext bibDatabaseContext, BibEntry entry, AiService aiService, AiPreferences aiPreferences, - FilePreferences filePreferences, + ExternalApplicationsPreferences externalApplicationsPreferences, CitationKeyPatternPreferences citationKeyPatternPreferences, DialogService dialogService ) { - super(aiPreferences, filePreferences, dialogService); + super(aiPreferences, externalApplicationsPreferences, dialogService); this.bibDatabaseContext = bibDatabaseContext; this.entry = entry; diff --git a/src/main/java/org/jabref/gui/ai/components/summary/SummaryShowingComponent.fxml b/src/main/java/org/jabref/gui/ai/components/summary/SummaryShowingComponent.fxml index db7698eccf6..f248bccab17 100644 --- a/src/main/java/org/jabref/gui/ai/components/summary/SummaryShowingComponent.fxml +++ b/src/main/java/org/jabref/gui/ai/components/summary/SummaryShowingComponent.fxml @@ -1,10 +1,11 @@ - - - - - + + + + + +