From 8292addd5776fd722cf391a137675afea3d82de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Bitteur?= Date: Tue, 14 Jan 2025 19:59:14 +0100 Subject: [PATCH] Redesign of the OCR languages download dialog to fix memory leaks --- app/res/system-actions.xml | 2 +- .../omr/text/tesseract/Languages.java | 385 +++++++++++------- .../omr/text/tesseract/TesseractOCR.java | 20 - .../tesseract/resources/Languages.properties | 11 +- .../resources/Languages_fr.properties | 15 +- .../java/org/audiveris/omr/ui/GuiActions.java | 56 ++- .../omr/ui/resources/GuiActions.properties | 7 +- .../omr/ui/resources/GuiActions_fr.properties | 4 +- 8 files changed, 291 insertions(+), 209 deletions(-) diff --git a/app/res/system-actions.xml b/app/res/system-actions.xml index 85677a8eb..67acf3300 100644 --- a/app/res/system-actions.xml +++ b/app/res/system-actions.xml @@ -87,7 +87,7 @@ - + diff --git a/app/src/main/java/org/audiveris/omr/text/tesseract/Languages.java b/app/src/main/java/org/audiveris/omr/text/tesseract/Languages.java index 193b29bc6..eabd3d262 100644 --- a/app/src/main/java/org/audiveris/omr/text/tesseract/Languages.java +++ b/app/src/main/java/org/audiveris/omr/text/tesseract/Languages.java @@ -26,12 +26,12 @@ import static org.audiveris.omr.text.Language.getSupportedLanguages; import static org.audiveris.omr.text.tesseract.TesseractOCR.LANGUAGE_FILE_EXT; import org.audiveris.omr.ui.GuiActions; +import org.audiveris.omr.ui.OmrGui; import org.audiveris.omr.ui.util.Panel; -import org.audiveris.omr.ui.util.UserOpt; +import org.audiveris.omr.ui.util.WaitingTask; import org.jdesktop.application.Application; import org.jdesktop.application.ResourceMap; -import org.jdesktop.application.Task; import org.kohsuke.github.GHContent; import org.kohsuke.github.GHOrganization; @@ -44,11 +44,13 @@ import com.jgoodies.forms.builder.FormBuilder; import com.jgoodies.forms.layout.FormLayout; -import java.awt.BorderLayout; +import static java.awt.Component.CENTER_ALIGNMENT; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -61,6 +63,8 @@ import java.util.SortedSet; import java.util.stream.Collectors; +import javax.swing.BoxLayout; +import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JDialog; @@ -70,11 +74,9 @@ import javax.swing.JScrollPane; import javax.swing.Scrollable; import javax.swing.SwingConstants; -import javax.swing.SwingUtilities; /** - * Class Languages defines the user dialogues to check and download all possible - * Tesseract languages files. + * Class Languages defines the user dialogues to check and install languages files. * * @author Hervé Bitteur */ @@ -92,8 +94,15 @@ public class Languages //~ Instance fields ---------------------------------------------------------------------------- + /** Component resources. */ private final ResourceMap resources; + /** Remote data (languages, codes). */ + private RemoteData remoteData; + + /** The user interface to browse, select and install languages. */ + private Selector selector; + //~ Constructors ------------------------------------------------------------------------------- /** @@ -106,16 +115,69 @@ public Languages () //~ Methods ------------------------------------------------------------------------------------ - //-------------// - // buildDialog // - //-------------// + //---------------// + // buildSelector // + //---------------// /** - * Build the download dialog, based on the languages remotely available on Github - * Tesseract site. + * Build the languages selector, based on the data remotely available. * - * @return + * @return the selector newly built + */ + private Selector buildSelector () + { + getRemoteData(); + + if ((remoteData != null) && !remoteData.codes.isEmpty()) { + return new Selector(); + } + + return null; + } + + //--------------// + // checkSupport // + //--------------// + /** + * Check the current set of installed languages and prompt the user to install some + * if the set is empty. + */ + public void checkSupport () + { + final SortedSet installed = TesseractOCR.getInstance().getSupportedLanguages(); + if (!installed.isEmpty()) { + logger.info( + "Installed OCR languages: {}", + installed.stream().collect(Collectors.joining(","))); + } else { + // Prompt user + final String install = resources.getString("Check.install"); + final String later = resources.getString("Check.later"); + final Object[] options = { install, later }; + final String message = resources.getString("Check.message"); + + final int choice = JOptionPane.showOptionDialog( + OMR.gui.getFrame(), + message, + resources.getString("Check.title"), + JOptionPane.DEFAULT_OPTION, + JOptionPane.WARNING_MESSAGE, + null, + options, + options[0]); + + if (choice == 0) { + GuiActions.getInstance().installLanguages(null).execute(); + } + } + } + + //--------------------// + // downloadRemoteData // + //--------------------// + /** + * Download the collection of languages names and codes available on Github Tesseract site. */ - public DownloadDialog buildDialog () + private void downloadRemoteData () { try { // Retrieve the list of language codes available on the GitHub/Tesseract site. @@ -129,27 +191,24 @@ public DownloadDialog buildDialog () if (repository == null) { logger.warn("Unknown repository: {}", GITHUB_REPOSITORY); - return null; + return; } - // Retrieve the remote codes - final List remoteContent = repository.getDirectoryContent(""); - final List codes = new ArrayList<>(); + // Retrieve the remote content and codes + remoteData = new RemoteData(); + remoteData.content = repository.getDirectoryContent(""); + remoteData.codes = new ArrayList<>(); - for (GHContent c : remoteContent) { + for (GHContent c : remoteData.content) { final String fileName = c.getName(); if (c.isFile() && fileName.endsWith(LANGUAGE_FILE_EXT)) { - codes.add(fileName.replace(LANGUAGE_FILE_EXT, "")); + remoteData.codes.add(fileName.replace(LANGUAGE_FILE_EXT, "")); logger.debug("code: {} size: {}", fileName, c.getSize()); } } - logger.info("Languages available on Tesseract site: {}", codes.size()); - - if (!codes.isEmpty()) { - return new DownloadDialog(remoteContent, codes); - } + logger.info("Languages available on Tesseract site: {}", remoteData.codes.size()); } catch (IOException ex) { logger.warn("Error getting remote languages.\n {}", ex.getMessage()); @@ -159,47 +218,40 @@ public DownloadDialog buildDialog () logger.warn(" Please make sure you have access to Internet."); } - - return null; } - //--------------// - // checkSupport // - //--------------// + //---------------// + // getRemoteData // + //---------------// /** - * Check the current set of supported languages and prompt the user to download some - * if the set is empty. + * Return the collection of languages remotely available on Github Tesseract site. + * + * @return the RemoteData or null */ - public void checkSupport () + private RemoteData getRemoteData () { - final SortedSet supported = TesseractOCR.getInstance().getSupportedLanguages(); - if (!supported.isEmpty()) { - logger.info( - "Supported OCR languages: {}", - supported.stream().collect(Collectors.joining(","))); - } else { - // Prompt user - final String download = resources.getString("Check.download"); - final String later = resources.getString("Check.later"); - final Object[] options = { download, later }; - final String message = resources.getString("Check.message"); + if (remoteData == null) { + new DownloadRemoteTask().run(); + } - final int choice = JOptionPane.showOptionDialog( - OMR.gui.getFrame(), - message, - resources.getString("Check.title"), - JOptionPane.DEFAULT_OPTION, - JOptionPane.WARNING_MESSAGE, - null, - options, - options[0]); + return remoteData; + } - if (choice == 0) { - // Download - final Task task = GuiActions.getInstance().downloadLanguages(null); - SwingUtilities.invokeLater( () -> task.execute()); - } + //-------------// + // getSelector // + //-------------// + /** + * Report the UI dialog to browse, select and install OCR languages. + * + * @return the selector + */ + public Selector getSelector () + { + if (selector == null) { + selector = buildSelector(); } + + return selector; } //~ Static Methods ----------------------------------------------------------------------------- @@ -218,72 +270,164 @@ public static Languages getInstance () //~ Inner Classes ------------------------------------------------------------------------------ - //----------------// - // DownloadDialog // - //----------------// - public class DownloadDialog + //--------------------// + // DownloadRemoteTask // + //--------------------// + /** + * Task to download the lists of languages names and codes from Tesseract GitHub site. + */ + private class DownloadRemoteTask + extends WaitingTask + { + DownloadRemoteTask () + { + super(OmrGui.getApplication(), resources.getString("downloadTask.message")); + } + + @Override + protected Languages.RemoteData doInBackground () + throws Exception + { + downloadRemoteData(); + return remoteData; + } + } + + //------------// + // RemoteData // + //------------// + /** + * The collection of names and codes remotely available on Github Tesseract site. + */ + public static class RemoteData + { + /** List of languages files names. */ + public List content; + + /** List of 3-letter codes. */ + public List codes; + } + + //-----------------// + // ScrollablePanel // + //-----------------// + private static class ScrollablePanel + extends JPanel + implements Scrollable { - /** Content of the GitHub Tesseract repository to interact with. */ - private final List remoteContent; + @Override + public Dimension getPreferredScrollableViewportSize () + { + return getPreferredSize(); + } + + @Override + public int getScrollableBlockIncrement (Rectangle visibleRect, + int orientation, + int direction) + { + return (orientation == SwingConstants.HORIZONTAL) // + ? visibleRect.width + : visibleRect.height; + } + + @Override + public boolean getScrollableTracksViewportHeight () + { + return false; + } + + @Override + public boolean getScrollableTracksViewportWidth () + { + return true; + } - /** The dialog to browse and download. */ + @Override + public int getScrollableUnitIncrement (Rectangle visibleRect, + int orientation, + int direction) + { + return 40; // Minimum cell height. TODO: Could be improved. + } + } + + //----------// + // Selector // + //----------// + /** + * The UI to browse, select and install languages. + */ + public class Selector + implements ActionListener + { private final JDialog dialog; + private final String boxTip = resources.getString("box.shortDescription"); + /** - * Creates a new DownloadDialog object. - * - * @param remoteContent the list of Github languages - * @param remoteCodes the parallel list of codes + * Creates a new Selector object. */ - public DownloadDialog (List remoteContent, - List remoteCodes) + public Selector () { - this.remoteContent = remoteContent; - - dialog = new JDialog(OMR.gui.getFrame()); + dialog = new JDialog(OMR.gui.getFrame(), true); // True for a modal dialog dialog.setName("LanguagesFrame"); // For SAF life cycle dialog.setPreferredSize(new Dimension(340, 600)); final JComponent framePane = (JComponent) dialog.getContentPane(); - framePane.setLayout(new BorderLayout()); + framePane.setLayout(new BoxLayout(framePane, BoxLayout.Y_AXIS)); - final JPanel panel = defineLayout(remoteCodes); + framePane.add(new JScrollPane(defineLayout())); - final JOptionPane optionPane = new JOptionPane( - new JScrollPane(panel), - JOptionPane.PLAIN_MESSAGE, - JOptionPane.DEFAULT_OPTION, - null, - new Object[] { UserOpt.OK }); - optionPane.addPropertyChangeListener(e -> { - final Object choice = optionPane.getValue(); - if (choice == UserOpt.Cancel || choice == UserOpt.OK) { - dialog.setVisible(false); - dialog.dispose(); + // Closing (via close button) + JButton button = new JButton(); + button.setName("closeButton"); + button.setAlignmentX(CENTER_ALIGNMENT); + button.addActionListener(this); + framePane.add(button); + + // Closing (via close window) + dialog.addWindowListener(new WindowAdapter() + { + @Override + public void windowClosing (WindowEvent e) + { + closeDialog(); } }); resources.injectComponents(dialog); - - dialog.setContentPane(optionPane); dialog.pack(); } - private JPanel defineLayout (List remoteCodes) + @Override + public void actionPerformed (ActionEvent e) { + closeDialog(); + } + + private void closeDialog () + { + dialog.setVisible(false); + dialog.dispose(); + } + + private JPanel defineLayout () + { + final List codes = remoteData.codes; final ScrollablePanel panel = new ScrollablePanel(); // JGoodies columns: code checkbox fullName final String colSpec = "right:50dlu,5dlu,center:10dlu,5dlu,left:200dlu"; final int perLine = 19; - final int height = remoteCodes.size() * perLine; + final int height = codes.size() * perLine; panel.setPreferredSize(new Dimension(320, height)); - final FormLayout layout = new FormLayout(colSpec, Panel.makeRows(remoteCodes.size())); + final FormLayout layout = new FormLayout(colSpec, Panel.makeRows(codes.size())); final FormBuilder builder = FormBuilder.create().layout(layout).panel(panel); int r = 1; - for (String name : remoteCodes) { + for (String name : codes) { final LangLine line = new LangLine(name); line.label.setHorizontalAlignment(SwingConstants.LEFT); builder.addRaw(line.label).xy(1, r); @@ -296,11 +440,11 @@ private JPanel defineLayout (List remoteCodes) } /** - * Download the data file for the provided language code. + * Install the data file for the provided language code. * - * @param code the desired language code + * @param code the language code */ - private void download (String code) + private void install (String code) { try { final Path ocrFolder = TesseractOCR.getInstance().getOcrFolder(); @@ -313,7 +457,7 @@ private void download (String code) final String fileName = code + LANGUAGE_FILE_EXT; - for (GHContent c : remoteContent) { + for (GHContent c : remoteData.content) { if (c.isFile() && c.getName().equals(fileName)) { final URI uri = new URI(c.getDownloadUrl()); final Path targetPath = ocrFolder.resolve(fileName); @@ -321,7 +465,7 @@ private void download (String code) try (InputStream is = uri.toURL().openStream()) { final long size = Files.copy(is, targetPath, REPLACE_EXISTING); - logger.info("Downloaded file: {} size: {}", fileName, size); + logger.info("Installed file: {} size: {}", fileName, size); // Update the current collection of supported codes getSupportedLanguages().addCode(code); @@ -368,6 +512,7 @@ public LangLine (String code) if (fn != null) { box = new JCheckBox(); + box.setToolTipText(boxTip); fullName = new JLabel(fn); label.setEnabled(true); @@ -397,54 +542,10 @@ public void actionPerformed (ActionEvent e) if (getSupportedLanguages().contains(code)) { logger.info("No need to re-download '{}'", code); } else { - download(code); + install(code); } } } } } - - //-----------------// - // ScrollablePanel // - //-----------------// - private static class ScrollablePanel - extends JPanel - implements Scrollable - { - @Override - public Dimension getPreferredScrollableViewportSize () - { - return getPreferredSize(); - } - - @Override - public int getScrollableBlockIncrement (Rectangle visibleRect, - int orientation, - int direction) - { - return (orientation == SwingConstants.HORIZONTAL) // - ? visibleRect.width - : visibleRect.height; - } - - @Override - public boolean getScrollableTracksViewportHeight () - { - return false; - } - - @Override - public boolean getScrollableTracksViewportWidth () - { - return true; - } - - @Override - public int getScrollableUnitIncrement (Rectangle visibleRect, - int orientation, - int direction) - { - return 40; // Minimum cell height. TODO: Could be improved. - } - } } diff --git a/app/src/main/java/org/audiveris/omr/text/tesseract/TesseractOCR.java b/app/src/main/java/org/audiveris/omr/text/tesseract/TesseractOCR.java index 9f404bd66..6811ba63d 100644 --- a/app/src/main/java/org/audiveris/omr/text/tesseract/TesseractOCR.java +++ b/app/src/main/java/org/audiveris/omr/text/tesseract/TesseractOCR.java @@ -141,26 +141,6 @@ private Path findOcrFolder () } } - // // Second, scan OS typical TESSDATA locations, beginning by user Audiveris config folder - // if (WellKnowns.WINDOWS) { - // return scanOcrLocations( - // WellKnowns.CONFIG_FOLDER.toString(), - // Paths.get(System.getenv("ProgramFiles")).resolve("tesseract-ocr").toString(), - // Paths.get(System.getenv("ProgramFiles(x86)")).resolve("tesseract-ocr") - // .toString()); - // } else if (WellKnowns.LINUX) { - // return scanOcrLocations( - // WellKnowns.CONFIG_FOLDER.toString(), - // "/usr/share/tesseract-ocr", // Debian, Ubuntu and derivatives - // "/usr/share", // OpenSUSE - // "/usr/share/tesseract");// Fedora - // } else if (WellKnowns.MAC_OS_X) { - // return scanOcrLocations( - // WellKnowns.CONFIG_FOLDER.toString(), - // "/opt/local/share", // Macports - // "/usr/local/opt/tesseract/share"); // Homebrew - // } - logger.warn(ocrNotFoundMsg); return null; diff --git a/app/src/main/java/org/audiveris/omr/text/tesseract/resources/Languages.properties b/app/src/main/java/org/audiveris/omr/text/tesseract/resources/Languages.properties index c8c008f9a..78b545365 100644 --- a/app/src/main/java/org/audiveris/omr/text/tesseract/resources/Languages.properties +++ b/app/src/main/java/org/audiveris/omr/text/tesseract/resources/Languages.properties @@ -12,10 +12,15 @@ # Resources for Languages # ----------------------- -LanguagesFrame.title = Download of OCR languages +LanguagesFrame.title = Installation of OCR languages + +downloadTask.message = Collecting languages from Tesseract site... +box.shortDescription = Tick this box to install the language on-the-fly + +closeButton.text = Close Check.title = OCR languages Check.message = Your collection of languages is empty!\ -
Would you like to download some? -Check.download = Download +
Would you like to install some? +Check.install = Install Check.later = Later \ No newline at end of file diff --git a/app/src/main/java/org/audiveris/omr/text/tesseract/resources/Languages_fr.properties b/app/src/main/java/org/audiveris/omr/text/tesseract/resources/Languages_fr.properties index d27929a35..a3285fb55 100644 --- a/app/src/main/java/org/audiveris/omr/text/tesseract/resources/Languages_fr.properties +++ b/app/src/main/java/org/audiveris/omr/text/tesseract/resources/Languages_fr.properties @@ -12,10 +12,15 @@ # Resources for Languages # ----------------------- -LanguagesFrame.title = T\u00e9l\u00e9chargement de langages OCR +LanguagesFrame.title = Installation de langues OCR -Check.title = Langages OCR -Check.message = Votre collection de langages est vide !\ -
Voulez-vous en t\u00e9l\u00e9charger ? -Check.download = T\u00e9l\u00e9charger +downloadTask.message = Collecte des langues en cours +box.shortDescription = Cochez cette case pour installer la langue \u00e0 la vol\u00e9e + +closeButton.text = Fermer + +Check.title = Langues OCR +Check.message = Votre collection de langues est vide !\ +
Voulez-vous en installer ? +Check.install = Installer Check.later = Plus tard \ No newline at end of file diff --git a/app/src/main/java/org/audiveris/omr/ui/GuiActions.java b/app/src/main/java/org/audiveris/omr/ui/GuiActions.java index 39cafc65f..ef628bb74 100644 --- a/app/src/main/java/org/audiveris/omr/ui/GuiActions.java +++ b/app/src/main/java/org/audiveris/omr/ui/GuiActions.java @@ -211,33 +211,33 @@ public void defineShapeColors (ActionEvent e) ShapeColorChooser.showFrame(); } - //-------------------// - // downloadLanguages // - //-------------------// + //------// + // exit // + //------// /** - * Download OCR languages from github. + * Action to exit the application * * @param e the event which triggered this action - * @return the task to launch */ @Action - public Task downloadLanguages (ActionEvent e) + public void exit (ActionEvent e) { - return new DownloadLanguagesTask(); + OmrGui.getApplication().exit(); } - //------// - // exit // - //------// + //------------------// + // installLanguages // + //------------------// /** - * Action to exit the application + * Install OCR languages from github. * * @param e the event which triggered this action + * @return the SAF task */ @Action - public void exit (ActionEvent e) + public InstallLanguagesTask installLanguages (ActionEvent e) { - OmrGui.getApplication().exit(); + return new InstallLanguagesTask(); } //--------------------// @@ -440,31 +440,23 @@ private static class Constants "URL of Audiveris manual"); } - //-----------------------// - // DownloadLanguagesTask // - //-----------------------// - private static class DownloadLanguagesTask - extends WaitingTask + //----------------------// + // InstallLanguagesTask // + //----------------------// + public static class InstallLanguagesTask + extends VoidTask { - DownloadLanguagesTask () - { - super(OmrGui.getApplication(), resources.getString("downloadLanguagesTask.message")); - } - @Override - protected Languages.DownloadDialog doInBackground () + protected Void doInBackground () throws Exception { - ///final Languages.DownloadDialog dialog = Languages.getInstance().buildDialog(); - return Languages.getInstance().buildDialog(); - } + final Languages.Selector dld = Languages.getInstance().getSelector(); - @Override - protected void succeeded (Languages.DownloadDialog dialog) - { - if (dialog != null) { - OmrGui.getApplication().show(dialog.getComponent()); + if (dld != null) { + OmrGui.getApplication().show(dld.getComponent()); } + + return null; } } diff --git a/app/src/main/java/org/audiveris/omr/ui/resources/GuiActions.properties b/app/src/main/java/org/audiveris/omr/ui/resources/GuiActions.properties index dfdedef17..f28d220cd 100644 --- a/app/src/main/java/org/audiveris/omr/ui/resources/GuiActions.properties +++ b/app/src/main/java/org/audiveris/omr/ui/resources/GuiActions.properties @@ -25,9 +25,9 @@ clearLog.Action.icon = ${icons.root}/actions/editcut.png # Tools -downloadLanguages.Action.text = Download languages... -downloadLanguages.Action.shortDescription = Download additional OCR languages -downloadLanguages.Action.icon = ${icons.root}/actions/agt_reload.png +installLanguages.Action.text = Install languages... +installLanguages.Action.shortDescription = Install additional OCR languages +installLanguages.Action.icon = ${icons.root}/actions/agt_reload.png browseGlobalSamples.Action.text = Browse global repository... browseGlobalSamples.Action.shortDescription = Verify the training samples @@ -94,5 +94,4 @@ showAbout.Action.icon = ${icons.root}/actions/documentinfo.png launchingGlobalSampleBrowser = Launching global sample browser... launchingLocalSampleBrowser = Launching local sample browser... -downloadLanguagesTask.message = Collecting languages from Tesseract site... constantsTask.message = Collecting application constants... diff --git a/app/src/main/java/org/audiveris/omr/ui/resources/GuiActions_fr.properties b/app/src/main/java/org/audiveris/omr/ui/resources/GuiActions_fr.properties index b2507b6b7..8bc98571d 100644 --- a/app/src/main/java/org/audiveris/omr/ui/resources/GuiActions_fr.properties +++ b/app/src/main/java/org/audiveris/omr/ui/resources/GuiActions_fr.properties @@ -57,8 +57,8 @@ showAbout.Action.shortDescription = Affichage des informations du programme # Tool -downloadLanguages.Action.text = T\u00e9l\u00e9chargement de langages... -downloadLanguages.Action.shortDescription = T\u00e9l\u00e9chargement de nouveaux langages OCR +installLanguages.Action.text = Installation de langues... +installLanguages.Action.shortDescription = Installation de nouvelles langues OCR browseGlobalSamples.Action.text = Inspecter les \u00e9chantillons globaux... browseGlobalSamples.Action.shortDescription = Afficher le r\u00e9pertoire global d'\u00e9chantillons