diff --git a/.github/workflows/gradle.yaml b/.github/workflows/gradle.yaml index 49ae1f4..1fddf0a 100644 --- a/.github/workflows/gradle.yaml +++ b/.github/workflows/gradle.yaml @@ -24,18 +24,16 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v3 with: - java-version: '17' + java-version: '21' distribution: 'temurin' - name: Validate Gradle wrapper - uses: gradle/wrapper-validation-action@v1 - - name: Build with Gradle - uses: gradle/gradle-build-action@v2 - with: - arguments: build - - uses: actions/upload-artifact@v3 + uses: gradle/actions/wrapper-validation@v3 + - name: Build with gradle + run: ./gradlew build + - uses: actions/upload-artifact@v4 with: name: javadoc-viewer-jar path: build/libs diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 9e8eaf3..6dd6881 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -11,13 +11,13 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v3 with: - java-version: '17' + java-version: '21' distribution: 'temurin' - name: Validate Gradle wrapper - uses: gradle/wrapper-validation-action@v1 + uses: gradle/actions/wrapper-validation@v3 - name: Publish snapshot uses: gradle/gradle-build-action@v2 with: @@ -25,7 +25,7 @@ jobs: env: MAVEN_USER: ${{ secrets.MAVEN_USER }} MAVEN_PASS: ${{ secrets.MAVEN_PASS }} - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: javadoc-viewer-release-jar path: build/libs diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml index e0773db..07e242c 100644 --- a/.github/workflows/publish-snapshot.yml +++ b/.github/workflows/publish-snapshot.yml @@ -10,13 +10,13 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v3 with: - java-version: '17' + java-version: '21' distribution: 'temurin' - name: Validate Gradle wrapper - uses: gradle/wrapper-validation-action@v1 + uses: gradle/actions/wrapper-validation@v3 - name: Publish snapshot uses: gradle/gradle-build-action@v2 with: @@ -24,7 +24,7 @@ jobs: env: MAVEN_USER: ${{ secrets.MAVEN_USER }} MAVEN_PASS: ${{ secrets.MAVEN_PASS }} - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: javadoc-viewer-snapshot-jar path: build/libs diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 03f2f51..b7aceb7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] -java = "17" +java = "21" slf4j = "2.0.7" -javafx = "20" +javafx = "21.0.6" javafxPlugin = "0.1.0" [libraries] diff --git a/javadocviewer/src/main/java/qupath/ui/javadocviewer/core/Javadoc.java b/javadocviewer/src/main/java/qupath/ui/javadocviewer/core/Javadoc.java index d9d9bfd..b6ee07c 100644 --- a/javadocviewer/src/main/java/qupath/ui/javadocviewer/core/Javadoc.java +++ b/javadocviewer/src/main/java/qupath/ui/javadocviewer/core/Javadoc.java @@ -17,8 +17,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Objects; -import java.util.Optional; import java.util.Scanner; import java.util.concurrent.CompletableFuture; import java.util.regex.Matcher; @@ -31,73 +29,72 @@ /** * A Javadoc specified by a {@link URI} and containing {@link JavadocElement JavadocElements}. * Elements are populated by looking at the {@link #INDEX_ALL_PAGE} page of the Javadoc. + * + * @param uri the URI of this Javadoc + * @param elements an unmodifiable view of the elements of this Javadoc */ -public class Javadoc { +public record Javadoc(URI uri, List elements) { private static final Logger logger = LoggerFactory.getLogger(Javadoc.class); + private static final String INDEX_PAGE = "index.html"; + private static final String INDEX_ALL_PAGE = "index-all.html"; private static final Pattern ENTRY_PATTERN = Pattern.compile("
(.*?)
"); private static final Pattern URI_PATTERN = Pattern.compile("href=\"(.+?)\""); private static final Pattern NAME_PATTERN = Pattern.compile("(?:)?(.*?)(?:)?"); private static final Pattern CATEGORY_PATTERN = Pattern.compile(" - (.+?) "); - private static final String INDEX_PAGE = "index.html"; - private static final String INDEX_ALL_PAGE = "index-all.html"; - private final URI uri; - private final List elements; + private static final int REQUEST_TIMEOUT_SECONDS = 10; - private Javadoc(URI uri, List elements) { + /** + * Create a Javadoc from a URI and Javadoc elements. Take a look at {@link #create(URI)} + * to create a Javadoc only from a URI. + * + * @param uri the URI pointing to this Javadoc + * @param elements the elements of this Javadoc + */ + public Javadoc(URI uri, List elements) { this.uri = uri; this.elements = Collections.unmodifiableList(elements); } /** * Asynchronously attempt to create a Javadoc from the specified URI. + *

+ * Note that exception handling is left to the caller (the returned CompletableFuture may + * complete exceptionally if the elements of the Javadocs cannot be retrieved for example). * - * @param uri the URI of the Javadoc - * @return a CompletableFuture with the created Javadoc, or an empty Optional if the creation failed + * @param uri the URI of the Javadoc + * @return a CompletableFuture (that may complete exceptionally) with the created Javadoc */ - public static CompletableFuture> create(URI uri) { - return getIndexAllPage(uri).thenApply(indexAllPage -> indexAllPage.map(page -> new Javadoc( + public static CompletableFuture create(URI uri) { + return getIndexAllPage(uri).thenApply(indexAllPage -> new Javadoc( uri, parseJavadocIndexPage( uri.toString().substring(0, uri.toString().lastIndexOf('/') + 1), - page + indexAllPage ) - ))); + )); } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Javadoc javadoc = (Javadoc) o; - return Objects.equals(uri, javadoc.uri); - } - - @Override - public int hashCode() { - return Objects.hashCode(uri); - } - - @Override - public String toString() { - return "Javadoc{" + - "uri=" + uri + - ", elements=" + elements + - '}'; - } - - /** - * @return the URI of this Javadoc - */ - public URI getUri() { - return uri; - } + private static CompletableFuture getIndexAllPage(URI javadocIndexURI) { + String link = javadocIndexURI.toString().replace(INDEX_PAGE, INDEX_ALL_PAGE); + URI indexAllURI; + try { + indexAllURI = new URI(link); + } catch (URISyntaxException e) { + return CompletableFuture.failedFuture(e); + } - /** - * @return an unmodifiable view of the elements of this Javadoc - */ - public List getElements() { - return elements; + if (Utils.doesUrilinkToWebsite(indexAllURI)) { + return getIndexAllPageContentFromHttp(indexAllURI); + } else { + return CompletableFuture.supplyAsync(() -> { + if (indexAllURI.getScheme().contains("jar")) { + return getIndexAllPageContentFromJar(indexAllURI); + } else { + return getIndexAllPageContentFromNonJar(indexAllURI); + } + }); + } } private static List parseJavadocIndexPage(String javadocURI, String indexHTMLPage) { @@ -119,16 +116,18 @@ private static List parseJavadocIndexPage(String javadocURI, Str name = nameMatcher.group(1) + "." + name; } String link = javadocURI + uriMatcher.group(1); + String category = categoryMatcher.group(1); + + name = correctNameIfConstructor(name, category); try { - URI uri = new URI(link); elements.add(new JavadocElement( - uri, + new URI(link), name, - categoryMatcher.group(1) + category )); } catch (URISyntaxException e) { - logger.debug(String.format("Cannot create URI %s of Javadoc element", link), e); + logger.debug("Cannot create URI {} of Javadoc element", link, e); } } } @@ -137,85 +136,73 @@ private static List parseJavadocIndexPage(String javadocURI, Str return elements; } - private static CompletableFuture> getIndexAllPage(URI javadocIndexURI) { - String link = javadocIndexURI.toString().replace(INDEX_PAGE, INDEX_ALL_PAGE); - URI indexAllURI; - try { - indexAllURI = new URI(link); - } catch (URISyntaxException e) { - logger.debug(String.format("Cannot create URI %s of index page", link), e); - return CompletableFuture.completedFuture(Optional.empty()); - } + private static CompletableFuture getIndexAllPageContentFromHttp(URI uri) { + HttpClient httpClient = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.ALWAYS) + .build(); - if (indexAllURI.getScheme().contains("http")) { - return getIndexAllPageFromHttp(indexAllURI); - } else { - return CompletableFuture.supplyAsync(() -> { - if (indexAllURI.getScheme().contains("jar")) { - return getIndexAllPageFromJar(indexAllURI); - } else { - return getIndexAllPageFromDirectory(indexAllURI); - } - }); - } - } + logger.debug("Sending GET request to {} to read the index-all page content...", uri); - private static CompletableFuture> getIndexAllPageFromHttp(URI uri) { - return HttpClient.newHttpClient().sendAsync( + return httpClient.sendAsync( HttpRequest.newBuilder() .uri(uri) - .timeout(Duration.of(10, ChronoUnit.SECONDS)) + .timeout(Duration.of(REQUEST_TIMEOUT_SECONDS, ChronoUnit.SECONDS)) .GET() .build(), HttpResponse.BodyHandlers.ofString() - ).handle((response, error) -> { - if (response == null || error != null) { - if (error != null) { - logger.debug("Error when retrieving Javadoc index page", error); - } - return Optional.empty(); - } else { - return Optional.ofNullable(response.body()); - } - }); + ).thenApply(response -> { + logger.debug("Got response {} from {}", response, uri); + return response.body(); + }).whenComplete((b, e) -> httpClient.close()); } - private static Optional getIndexAllPageFromJar(URI uri) { + private static String getIndexAllPageContentFromJar(URI uri) { String jarURI = uri.toString().substring( uri.toString().indexOf('/'), uri.toString().lastIndexOf('!') ); + logger.debug("Opening {} jar file to read the index-all page content...", jarURI); try (ZipFile zipFile = new ZipFile(jarURI)) { ZipEntry entry = zipFile.getEntry(INDEX_ALL_PAGE); if (entry == null) { - logger.debug(String.format("%s not found in %s", INDEX_ALL_PAGE, jarURI)); - return Optional.empty(); + throw new IllegalArgumentException(String.format("The provided jar file %s doesn't contain any %s entry", jarURI, INDEX_ALL_PAGE)); } else { try ( - InputStream inputStream = zipFile.getInputStream(zipFile.getEntry(INDEX_ALL_PAGE)); + InputStream inputStream = zipFile.getInputStream(entry); Scanner scanner = new Scanner(inputStream) ) { StringBuilder lines = new StringBuilder(); while (scanner.hasNextLine()) { lines.append(scanner.nextLine()); } - return Optional.of(lines.toString()); + return lines.toString(); } } } catch (IOException e) { - logger.debug(String.format("Error while reading %s", jarURI), e); - return Optional.empty(); + throw new RuntimeException(e); } } - private static Optional getIndexAllPageFromDirectory(URI uri) { + private static String getIndexAllPageContentFromNonJar(URI uri) { + logger.debug("Reading {} file to get the index-all page content...", uri); + try (Stream lines = Files.lines(Paths.get(uri))) { - return Optional.of(lines.collect(Collectors.joining("\n"))); - } catch (Exception e) { - logger.debug(String.format("Error while reading %s", uri), e); - return Optional.empty(); + return lines.collect(Collectors.joining("\n")); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static String correctNameIfConstructor(String name, String category) { + // Constructor are usually written in the following way: "Class.Class(Parameter)" + // This function transforms them into "Class(Parameter)" + if (category.equals("Constructor")) { + int pointIndex = name.indexOf("."); + return name.substring(pointIndex + 1); + } else { + return name; } } } diff --git a/javadocviewer/src/main/java/qupath/ui/javadocviewer/core/JavadocElement.java b/javadocviewer/src/main/java/qupath/ui/javadocviewer/core/JavadocElement.java index 460e706..ecd636a 100644 --- a/javadocviewer/src/main/java/qupath/ui/javadocviewer/core/JavadocElement.java +++ b/javadocviewer/src/main/java/qupath/ui/javadocviewer/core/JavadocElement.java @@ -5,8 +5,8 @@ /** * An element (function, class, enum...) of a Javadoc. * - * @param uri the URI of the Javadoc owning this element - * @param name the name of the element (e.g. the function name) - * @param category the category of the element (e.g. "function") + * @param uri the URI of the Javadoc owning this element + * @param name the name of the element (e.g. the function name) + * @param category the category of the element (e.g. "function" or "class") */ public record JavadocElement(URI uri, String name, String category) {} diff --git a/javadocviewer/src/main/java/qupath/ui/javadocviewer/core/JavadocsFinder.java b/javadocviewer/src/main/java/qupath/ui/javadocviewer/core/JavadocsFinder.java index abec1fd..0686d6c 100644 --- a/javadocviewer/src/main/java/qupath/ui/javadocviewer/core/JavadocsFinder.java +++ b/javadocviewer/src/main/java/qupath/ui/javadocviewer/core/JavadocsFinder.java @@ -12,8 +12,10 @@ import java.nio.file.Paths; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.stream.Stream; import java.util.zip.ZipFile; @@ -34,62 +36,71 @@ private JavadocsFinder() { /** * Asynchronously search for Javadocs in the specified URIs. * - * @param urisToSearch URIs to search for Javadocs. It can be a directory, an HTTP link, - * a link to a jar file... + * @param urisToSearch URIs to search for Javadocs. It can be a directory, an HTTP link, + * a link to a jar file... * @return a CompletableFuture with the list of Javadocs found */ public static CompletableFuture> findJavadocs(URI... urisToSearch) { return CompletableFuture.supplyAsync(() -> Arrays.stream(urisToSearch) - .map(JavadocsFinder::findJavadocUris) + .map(JavadocsFinder::findJavadocUrisFromUri) .flatMap(List::stream) - .map(Javadoc::create) - .map(CompletableFuture::join) - .flatMap(Optional::stream) + .map(uri -> { + try { + return Javadoc.create(uri).get(); + } catch (InterruptedException | ExecutionException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + logger.debug("Error when creating javadoc of {}. Skipping it", uri, e); + + return null; + } + }) + .filter(Objects::nonNull) .distinct() .toList() ); } - private static List findJavadocUris(URI uri) { - if (uri.getScheme() != null && List.of("http", "https").contains(uri.getScheme())) { + private static List findJavadocUrisFromUri(URI uri) { + if (Utils.doesUrilinkToWebsite(uri)) { + logger.debug("URI {} retrieved", uri); return List.of(uri); } else { try { - return findJavadocUris(Paths.get(uri)); + return findJavadocUrisFromPath(Paths.get(uri)); } catch (Exception e) { - logger.debug(String.format("Could not convert URI %s to path", uri), e); + logger.debug("Could not convert URI {} to path", uri, e); return List.of(); } } } - private static List findJavadocUris(Path path) { - if (path == null) { - return List.of(); + private static List findJavadocUrisFromPath(Path path) { + if (Files.isDirectory(path)) { + return findJavadocUrisFromDirectory(path); } else { - logger.debug(String.format("Searching for javadocs in %s (depth=%d)", path, SEARCH_DEPTH)); - - if (Files.isDirectory(path)) { - return findJavadocUrisFromDirectory(path); - } else { - return findJavadocUrisFromFile(path).map(List::of).orElse(List.of()); - } + return findJavadocUrisFromFile(path).map(List::of).orElse(List.of()); } } private static List findJavadocUrisFromDirectory(Path directory) { + logger.debug("Searching for javadocs in {} directory with depth {}", directory, SEARCH_DEPTH); + try (Stream walk = Files.walk(directory, JavadocsFinder.SEARCH_DEPTH)) { return walk .map(JavadocsFinder::findJavadocUrisFromFile) .flatMap(Optional::stream) .toList(); } catch (IOException e) { - logger.debug("Exception while requesting javadoc URIs", e); + logger.debug("Exception while searching for javadoc URIs", e); return List.of(); } } private static Optional findJavadocUrisFromFile(Path path) { + logger.debug("Determining if {} contains Javadoc", path); + File file = path.toFile(); if ( @@ -98,10 +109,11 @@ private static Optional findJavadocUrisFromFile(Path path) { ) { try (Stream lines = Files.lines(path)) { if (lines.anyMatch(l -> l.contains("javadoc"))) { + logger.debug("{} points to a Javadoc index page", path); return Optional.of(path.toUri()); } } catch (IOException e) { - logger.debug(String.format("Error while reading %s", path), e); + logger.debug("Error while reading {}", path, e); } } @@ -112,13 +124,21 @@ private static Optional findJavadocUrisFromFile(Path path) { ) { try (ZipFile zipFile = new ZipFile(file)) { if (zipFile.getEntry(JAVADOC_INDEX_FILE) != null) { - return Optional.of(new URI(String.format("jar:%s!/%s", file.toURI(), JAVADOC_INDEX_FILE))); + String uri = String.format("jar:%s!/%s", file.toURI(), JAVADOC_INDEX_FILE); + + try { + logger.debug("{} is an archive containing Javadoc", uri); + return Optional.of(new URI(uri)); + } catch (URISyntaxException e) { + logger.warn("Error while creating URI {}", uri, e); + } } - } catch (IOException | URISyntaxException e) { - logger.warn(String.format("Error while reading %s", path), e); + } catch (IOException e) { + logger.warn("Error while reading {}", path, e); } } + logger.debug("{} doesn't contain Javadoc", path); return Optional.empty(); } diff --git a/javadocviewer/src/main/java/qupath/ui/javadocviewer/core/Utils.java b/javadocviewer/src/main/java/qupath/ui/javadocviewer/core/Utils.java new file mode 100644 index 0000000..94c2814 --- /dev/null +++ b/javadocviewer/src/main/java/qupath/ui/javadocviewer/core/Utils.java @@ -0,0 +1,26 @@ +package qupath.ui.javadocviewer.core; + +import java.net.URI; +import java.util.List; + +/** + * A collection of utility functions. + */ +class Utils { + + private static final List WEBSITE_SCHEMES = List.of("http", "https"); + + private Utils() { + throw new AssertionError("This class is not instantiable."); + } + + /** + * Indicate whether the provided URI links to a website. + * + * @param uri the URI to check + * @return whether the provided URI links to a website + */ + public static boolean doesUrilinkToWebsite(URI uri) { + return uri.getScheme() != null && WEBSITE_SCHEMES.contains(uri.getScheme()); + } +} diff --git a/javadocviewer/src/main/java/qupath/ui/javadocviewer/gui/components/AutoCompleteTextFieldEntry.java b/javadocviewer/src/main/java/qupath/ui/javadocviewer/gui/components/AutoCompleteTextFieldEntry.java index df99b57..c1b1f6d 100644 --- a/javadocviewer/src/main/java/qupath/ui/javadocviewer/gui/components/AutoCompleteTextFieldEntry.java +++ b/javadocviewer/src/main/java/qupath/ui/javadocviewer/gui/components/AutoCompleteTextFieldEntry.java @@ -3,7 +3,7 @@ /** * An entry to a {@link AutoCompletionTextField}. */ -public interface AutoCompleteTextFieldEntry { +public interface AutoCompleteTextFieldEntry extends Comparable { /** * @return the text that should be displayed by this entry diff --git a/javadocviewer/src/main/java/qupath/ui/javadocviewer/gui/components/AutoCompletionTextField.java b/javadocviewer/src/main/java/qupath/ui/javadocviewer/gui/components/AutoCompletionTextField.java index aff94f2..3182954 100644 --- a/javadocviewer/src/main/java/qupath/ui/javadocviewer/gui/components/AutoCompletionTextField.java +++ b/javadocviewer/src/main/java/qupath/ui/javadocviewer/gui/components/AutoCompletionTextField.java @@ -12,18 +12,24 @@ import javafx.scene.text.TextFlow; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.stream.Stream; /** * A {@link TextField} that provides suggestions on a context menu. + *

* Suggestions are grouped by category and must implement {@link AutoCompleteTextFieldEntry}. + *

+ * Suggestions are sorted according to their order. + *

+ * No more than {@link #MAX_ENTRIES} suggestions are displayed at a time. * - * @param the type of suggestions + * @param the type of suggestions */ public class AutoCompletionTextField extends TextField { - private static final int MAX_ENTRIES = 50; + private static final int MAX_ENTRIES = 100; private static final int MAX_POPUP_HEIGHT = 300; private final ContextMenu entriesPopup = new ContextMenu(); private final List suggestions = new ArrayList<>(); @@ -66,9 +72,15 @@ private void setUpListeners() { } else { String loweredCaseEnteredText = enteredText.toLowerCase(); + Comparator comparator = + Comparator.comparing((AutoCompleteTextFieldEntry e) -> e.getSearchableText().toLowerCase().equals(loweredCaseEnteredText) ? -1 : 1) + .thenComparing(e -> e.getSearchableText().toLowerCase().startsWith(loweredCaseEnteredText) ? -1 : 1) + .thenComparing(AutoCompleteTextFieldEntry::compareTo); + populatePopup( suggestions.stream() .filter(entry -> entry.getSearchableText().toLowerCase().contains(loweredCaseEnteredText)) + .sorted(comparator) .limit(MAX_ENTRIES) .toList(), enteredText @@ -91,11 +103,9 @@ private void populatePopup(List entries, String filter) { entries.stream() .filter(entry -> entry.getCategory().equals(category)) .map(entry -> { - MenuItem menuItem = new CustomMenuItem(createEntryItemText(entry.getName(), filter), true); + MenuItem menuItem = new CustomMenuItem(createEntryItemText(entry, filter), true); menuItem.setOnAction(actionEvent -> { - setText(entry.getName()); - positionCaret(entry.getName().length()); entriesPopup.hide(); entry.onSelected(); }); @@ -109,7 +119,7 @@ private void populatePopup(List entries, String filter) { // Add first item, show popup, and then add other items // This is used to avoid the popup to ignore the anchor position // See https://stackoverflow.com/a/58542568 - entriesPopup.getItems().add(items.get(0)); + entriesPopup.getItems().add(items.getFirst()); entriesPopup.show(this, Side.BOTTOM, 0, 0); entriesPopup.getItems().addAll(items.stream().skip(1).toList()); } @@ -121,8 +131,12 @@ private static Node createCategoryItemText(String category) { return text; } - private static Node createEntryItemText(String text, String filter) { - int filterIndex = text.toLowerCase().indexOf(filter.toLowerCase()); + private Node createEntryItemText(T entry, String filter) { + String searchableText = entry.getSearchableText(); + String text = entry.getName(); + + int searchableTextIndex = text.indexOf(searchableText); + int filterIndex = text.toLowerCase().indexOf(filter.toLowerCase(), searchableTextIndex); Text textBefore = new Text(text.substring(0, filterIndex)); Text textFiltered = new Text(text.substring(filterIndex, filterIndex + filter.length())); diff --git a/javadocviewer/src/main/java/qupath/ui/javadocviewer/gui/viewer/JavadocEntry.java b/javadocviewer/src/main/java/qupath/ui/javadocviewer/gui/viewer/JavadocEntry.java index 2e5f07d..1528b9f 100644 --- a/javadocviewer/src/main/java/qupath/ui/javadocviewer/gui/viewer/JavadocEntry.java +++ b/javadocviewer/src/main/java/qupath/ui/javadocviewer/gui/viewer/JavadocEntry.java @@ -3,19 +3,30 @@ import qupath.ui.javadocviewer.gui.components.AutoCompleteTextFieldEntry; import qupath.ui.javadocviewer.core.JavadocElement; +import java.util.Map; + /** * An {@link AutoCompleteTextFieldEntry} that represents a {@link JavadocElement}. */ class JavadocEntry implements AutoCompleteTextFieldEntry { + private static final Map CATEGORY_ORDER = Map.of( + "Class", 1, + "Interface", 2, + "Enum", 3, + "Constructor", 4, + "Static", 5, + "Method", 6 + ); private final JavadocElement javadocElement; private final Runnable onSelected; + private String searchableText; /** * Create a Javadoc entry from a Javadoc element. * - * @param javadocElement the javadoc element to represent - * @param onSelected a function to call when this element is selected + * @param javadocElement the javadoc element to represent + * @param onSelected a function to call when this element is selected */ public JavadocEntry(JavadocElement javadocElement, Runnable onSelected) { this.javadocElement = javadocElement; @@ -29,13 +40,46 @@ public String getName() { @Override public String getSearchableText() { - int parenthesisIndex = javadocElement.name().indexOf("("); + if (searchableText == null) { + searchableText = switch (javadocElement.category()) { + // expect "some.package.Class". Retain "Class" + case "Class", "Interface" -> javadocElement.name().substring(javadocElement.name().lastIndexOf(".") + 1); + // expects "some.package.Class.Enum" or "Class.Enum.variable". Retain "Class.Enum" or "Enum.variable" + case "Enum" -> { + int lastPointIndex = javadocElement.name().lastIndexOf("."); + if (lastPointIndex > -1) { + int secondLastPointIndex = javadocElement.name().lastIndexOf(".", lastPointIndex-1); + if (secondLastPointIndex > -1) { + yield javadocElement.name().substring(secondLastPointIndex+1); + } + } + yield javadocElement.name(); + } + // expect "Class.function(Parameter)". Retain "function" + case "Static", "Method" -> { + int pointIndex = javadocElement.name().indexOf("."); + int parenthesisIndex = javadocElement.name().indexOf("("); + + if (parenthesisIndex > -1) { + yield javadocElement.name().substring(pointIndex+1, parenthesisIndex); + } else { + yield javadocElement.name().substring(pointIndex+1); + } + } + // expect "Class(Parameter)". Retain "Class" + case "Constructor" -> { + int parenthesisIndex = javadocElement.name().indexOf("("); - if (parenthesisIndex > -1) { - return javadocElement.name().substring(0, parenthesisIndex); - } else { - return javadocElement.name(); + if (parenthesisIndex > -1) { + yield javadocElement.name().substring(0, parenthesisIndex); + } else { + yield javadocElement.name(); + } + } + default -> javadocElement.name(); + }; } + return searchableText; } @Override @@ -47,4 +91,19 @@ public String getCategory() { public void onSelected() { onSelected.run(); } + + @Override + public int compareTo(AutoCompleteTextFieldEntry otherEntry) { + int categoryComparison = CATEGORY_ORDER.getOrDefault(getCategory(), 0) - CATEGORY_ORDER.getOrDefault(otherEntry.getCategory(), 0); + if (categoryComparison != 0) { + return categoryComparison; + } + + return getName().compareTo(otherEntry.getName()); + } + + @Override + public String toString() { + return String.format("Javadoc entry of %s", javadocElement); + } } diff --git a/javadocviewer/src/main/java/qupath/ui/javadocviewer/gui/viewer/JavadocViewer.java b/javadocviewer/src/main/java/qupath/ui/javadocviewer/gui/viewer/JavadocViewer.java index 0721e23..6b63bdf 100644 --- a/javadocviewer/src/main/java/qupath/ui/javadocviewer/gui/viewer/JavadocViewer.java +++ b/javadocviewer/src/main/java/qupath/ui/javadocviewer/gui/viewer/JavadocViewer.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.net.URI; import java.nio.file.Paths; +import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Optional; @@ -35,6 +36,7 @@ public class JavadocViewer extends BorderPane { private static final ResourceBundle resources = ResourceBundle.getBundle("qupath.ui.javadocviewer.strings"); private static final Pattern REDIRECTION_PATTERN = Pattern.compile("window\\.location\\.replace\\(['\"](.*?)['\"]\\)"); + private static final List CATEGORIES_TO_SKIP = List.of("package", "module", "Variable", "Exception", "Annotation", "Element"); private final WebView webView = new WebView(); @FXML private Button back; @@ -50,19 +52,20 @@ public class JavadocViewer extends BorderPane { /** * Create the javadoc viewer. * - * @param stylesheet a property containing a link to a stylesheet which should - * be applied to this viewer. Can be null - * @param urisToSearch URIs to search for Javadocs. See {@link JavadocsFinder#findJavadocs(URI...)} - * @throws IOException when the window creation fails + * @param stylesheet a property containing a link to a stylesheet which should + * be applied to this viewer. Can be null + * @param urisToSearch URIs to search for Javadocs. See {@link JavadocsFinder#findJavadocs(URI...)} + * @throws IOException if the window creation fails */ public JavadocViewer(ReadOnlyStringProperty stylesheet, URI... urisToSearch) throws IOException { - initUI(stylesheet, urisToSearch); + initUI(stylesheet, Arrays.stream(urisToSearch).toList()); setUpListeners(); } /** * Set the search text field to an input query. - * @param input The search query string. + * + * @param input the search query string. */ public void setSearchInput(String input) { autoCompletionTextField.setText(input); @@ -78,7 +81,7 @@ private void onForwardClicked(ActionEvent ignoredEvent) { offset(1); } - private void initUI(ReadOnlyStringProperty stylesheet, URI[] urisToSearch) throws IOException { + private void initUI(ReadOnlyStringProperty stylesheet, List urisToSearch) throws IOException { FXMLLoader loader = new FXMLLoader(JavadocViewer.class.getResource("javadoc_viewer.fxml"), resources); loader.setRoot(this); loader.setController(this); @@ -116,10 +119,9 @@ protected void updateItem(URI item, boolean empty) { } webView.getEngine().loadContent(resources.getString("JavadocViewer.findingJavadocs")); - JavadocsFinder.findJavadocs(urisToSearch).thenAccept(javadocs -> Platform.runLater(() -> { - + JavadocsFinder.findJavadocs(urisToSearch.toArray(new URI[0])).thenAccept(javadocs -> Platform.runLater(() -> { this.uris.getItems().setAll(javadocs.stream() - .map(Javadoc::getUri) + .map(Javadoc::uri) .sorted(Comparator.comparing(JavadocViewer::getName)) .toList() ); @@ -130,18 +132,18 @@ protected void updateItem(URI item, boolean empty) { this.uris.getSelectionModel().select(this.uris.getItems().stream() .filter(u -> getName(u).toLowerCase().contains("qupath")) .findFirst() - .orElse(this.uris.getItems().get(0)) + .orElse(this.uris.getItems().getFirst()) ); } autoCompletionTextField.getSuggestions().addAll(javadocs.stream() - .map(Javadoc::getElements) + .map(Javadoc::elements) .flatMap(List::stream) .map(javadocElement -> new JavadocEntry( javadocElement, () -> webView.getEngine().load(javadocElement.uri().toString()) )) - .sorted(Comparator.comparing(JavadocEntry::getName)) + .filter(javadocEntry -> !CATEGORIES_TO_SKIP.contains(javadocEntry.getCategory())) .toList()); })); } diff --git a/javadocviewer/src/main/java/qupath/ui/javadocviewer/gui/viewer/JavadocViewerCommand.java b/javadocviewer/src/main/java/qupath/ui/javadocviewer/gui/viewer/JavadocViewerCommand.java index 6b81104..a73468a 100644 --- a/javadocviewer/src/main/java/qupath/ui/javadocviewer/gui/viewer/JavadocViewerCommand.java +++ b/javadocviewer/src/main/java/qupath/ui/javadocviewer/gui/viewer/JavadocViewerCommand.java @@ -6,6 +6,8 @@ import java.io.IOException; import java.net.URI; +import java.util.Arrays; +import java.util.List; import java.util.ResourceBundle; /** @@ -17,22 +19,22 @@ public class JavadocViewerCommand implements Runnable { private static final ResourceBundle resources = ResourceBundle.getBundle("qupath.ui.javadocviewer.strings"); private final Stage owner; private final ReadOnlyStringProperty stylesheet; - private final URI[] urisToSearch; + private final List urisToSearch; private Stage stage; private JavadocViewer javadocViewer; /** * Create the command. This will not create the viewer until either the command is run or {@link #getJavadocViewer()} is called. * - * @param owner the stage that should own the viewer window. Can be null - * @param stylesheet a property containing a link to a stylesheet which should - * be applied to the viewer. Can be null - * @param urisToSearch URIs to search for Javadocs. See {@link JavadocViewer#JavadocViewer(ReadOnlyStringProperty, URI...)} + * @param owner the stage that should own the viewer window. Can be null + * @param stylesheet a property containing a link to a stylesheet which should + * be applied to the viewer. Can be null + * @param urisToSearch URIs to search for Javadocs. See {@link JavadocViewer#JavadocViewer(ReadOnlyStringProperty, URI...)} */ public JavadocViewerCommand(Stage owner, ReadOnlyStringProperty stylesheet, URI... urisToSearch) { this.owner = owner; this.stylesheet = stylesheet; - this.urisToSearch = urisToSearch; + this.urisToSearch = Arrays.stream(urisToSearch).toList(); } /** @@ -44,7 +46,7 @@ public JavadocViewerCommand(Stage owner, ReadOnlyStringProperty stylesheet, URI. public JavadocViewer getJavadocViewer() { if (javadocViewer == null) { try { - javadocViewer = new JavadocViewer(stylesheet, urisToSearch); + javadocViewer = new JavadocViewer(stylesheet, urisToSearch.toArray(new URI[0])); } catch (IOException e) { throw new RuntimeException(e); }