Java tutorial
/* KDXplore provides KDDart Data Exploration and Management Copyright (C) 2015,2016,2017 Diversity Arrays Technology, Pty Ltd. KDXplore may be redistributed and may be modified under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. KDXplore is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with KDXplore. If not, see <http://www.gnu.org/licenses/>. */ package com.diversityarrays.kdxplore; import java.awt.AlphaComposite; import java.awt.BorderLayout; import java.awt.CardLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Cursor; import java.awt.Desktop; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Rectangle; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import javax.imageio.ImageIO; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.Box; import javax.swing.ImageIcon; import javax.swing.JCheckBox; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JSeparator; import javax.swing.JSplitPane; import javax.swing.JTabbedPane; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import javax.swing.border.EmptyBorder; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.apache.commons.collections15.Bag; import org.apache.commons.collections15.Closure; import org.apache.commons.collections15.bag.HashBag; import com.diversityarrays.dalclient.DALClient; import com.diversityarrays.kdxplore.config.KdxploreConfig; import com.diversityarrays.kdxplore.prefs.ExplorerProperties; import com.diversityarrays.kdxplore.prefs.KdxplorePreferenceEditor; import com.diversityarrays.kdxplore.prefs.KdxplorePreferences; import com.diversityarrays.kdxplore.services.AppInitContext; import com.diversityarrays.kdxplore.services.BackupProvider; import com.diversityarrays.kdxplore.services.KdxApp; import com.diversityarrays.kdxplore.services.KdxApp.AfterUpdateResult; import com.diversityarrays.kdxplore.services.KdxApp.DevelopmentState; import com.diversityarrays.kdxplore.services.KdxAppService; import com.diversityarrays.kdxplore.services.KdxPluginInfo; import com.diversityarrays.ui.BrandingImageComponent; import com.diversityarrays.ui.LoginUrlsProvider; import com.diversityarrays.ui.PropertiesLoginUrlsProvider; import com.diversityarrays.update.UpdateCheckContext; import com.diversityarrays.update.UpdateCheckRequest; import com.diversityarrays.update.UpdateCheckRequest.CheckStatus; import com.diversityarrays.update.UpdateDialog; import com.diversityarrays.util.Check; import com.diversityarrays.util.ConnectDisconnectActions; import com.diversityarrays.util.DALClientProvider; import com.diversityarrays.util.DefaultDALClientProvider; import com.diversityarrays.util.DefaultUncaughtExceptionHandler; import com.diversityarrays.util.Either; import com.diversityarrays.util.ImageId; import com.diversityarrays.util.KDClientUtils; import com.diversityarrays.util.MsgBox; import com.diversityarrays.util.PrintStreamMessageLogger; import com.diversityarrays.util.ReportIssueAction; import com.diversityarrays.util.RunMode; import com.diversityarrays.util.VerticalLabelUI; import net.pearcan.application.ApplicationFolder; import net.pearcan.ui.GuiUtil; import net.pearcan.ui.desktop.DefaultDesktopSupport; import net.pearcan.ui.desktop.DefaultFrameWindowOpener; import net.pearcan.ui.desktop.DesktopSupport; import net.pearcan.ui.desktop.MacApplication; import net.pearcan.ui.desktop.MacApplicationException; import net.pearcan.ui.desktop.WindowOpener; import net.pearcan.ui.widget.MessagesPanel; import net.pearcan.util.BackgroundRunner; import net.pearcan.util.MemoryUsageMonitor; import net.pearcan.util.MessagePrinter; import net.pearcan.util.Util; public class KDXploreFrame extends JFrame { private static final String OFFLINE_DATA_APP_SERVICE_CLASS_NAME = "com.diversityarrays.kdxplore.offline.OfflineDataAppService"; // This is how often we will check for updates (if user hasn't exited) // ms/sec sec/hr hr/day # days private static final long EVERY_5_DAYS = 1000L * 3600 * 24 * 5; private static final String TAG = KDXploreFrame.class.getSimpleName(); static private final Dimension MINIMUM_SIZE = new Dimension(800, 600); private static final String CARD_KDXAPPS = "kdxapps"; //$NON-NLS-1$ private final String displayVersion; private MessagesPanel messagesPanel = new MessagesPanel(Msg.HDG_MESSAGES(), true); private AboutPage aboutPage; private Action aboutAction = new AbstractAction(Msg.ACTION_ABOUT()) { @Override public void actionPerformed(ActionEvent ev) { if (aboutPage != null) { aboutPage.toFront(); return; } aboutPage = new AboutPage(Msg.TITLE_PREFIX_ABOUT(getTitle()), KDXploreFrame.this, applicationFolder.getApplicationName(), displayVersion); aboutPage.addWindowListener(new WindowAdapter() { @Override public void windowClosed(WindowEvent e) { aboutPage.removeWindowListener(this); aboutPage = null; } }); aboutPage.setLocationRelativeTo(KDXploreFrame.this); aboutPage.setVisible(true); } }; private final String onlineHelpUrl; private Action onlineHelpAction = new AbstractAction(Msg.ACTION_ONLINE_HELP()) { @Override public void actionPerformed(ActionEvent e) { if (Check.isEmpty(onlineHelpUrl)) { MsgBox.warn(KDXploreFrame.this, "Sorry - no help URL available", getTitle()); return; } Desktop dt = Desktop.getDesktop(); try { dt.browse(new URI(onlineHelpUrl)); } catch (IOException | URISyntaxException e1) { MsgBox.error(KDXploreFrame.this, "Unable to open online help at\n" + onlineHelpUrl, //$NON-NLS-1$ getTitle()); } } }; private final DefaultFrameWindowOpener frameWindowOpener; private CardLayout cardLayout = new CardLayout(); private JPanel cardPanel = new JPanel(cardLayout); private JTabbedPane kdxAppTabs; private final BackgroundPanel backgroundPanel = new BackgroundPanel(); private Image iconImageBig; private Image iconImageSmall; private final Image iconImage; private final ImageIcon kdxploreIcon; private final String baseTitle; private final LoginUrlsProvider loginUrlsProvider; private final DALClientProvider clientProvider; private final ConnectDisconnectActions connectDisconnectActions; private JSplitPane splitPane; private final ApplicationFolder applicationFolder; private final File userDataFolder; private final Closure<UpdateCheckContext> updateChecker; private LogoImagesLabel logoImagesLabel; private final PrintStreamMessageLogger messageLogger = new PrintStreamMessageLogger( messagesPanel.getPrintStream()); private final int versionCode; private final String version; static private final Consumer<DALClientProvider> NO_OP_CONSUMER = new Consumer<DALClientProvider>() { @Override public void accept(DALClientProvider t) { } }; private final Function<DALClientProvider, Consumer<DALClientProvider>> connectIntercept = new Function<DALClientProvider, Consumer<DALClientProvider>>() { @Override public Consumer<DALClientProvider> apply(DALClientProvider provider) { Consumer<DALClientProvider> result; if (provider.getCanChangeUrl()) { // Just let it continue result = NO_OP_CONSUMER; } else { // We will allow change temporarily. provider.setCanChangeUrl(true); result = new Consumer<DALClientProvider>() { @Override public void accept(DALClientProvider provider) { provider.setCanChangeUrl(false); } }; } return result; } }; KDXploreFrame(ApplicationFolder appFolder, String title, int versionCode, String version, Closure<UpdateCheckContext> updateChecker) throws IOException { super(title); this.applicationFolder = appFolder; this.baseTitle = title; this.versionCode = versionCode; this.version = version; this.updateChecker = updateChecker; KdxploreConfig config = KdxploreConfig.getInstance(); this.onlineHelpUrl = config.getOnlineHelpUrl(); String supportEmail = config.getSupportEmail(); if (Check.isEmpty(supportEmail)) { supportEmail = "someone@somewhere"; //$NON-NLS-1$ } DefaultUncaughtExceptionHandler eh = new DefaultUncaughtExceptionHandler(this, appFolder.getApplicationName() + "_Error", //$NON-NLS-1$ supportEmail, version + "(" + versionCode + ")"); //$NON-NLS-1$ //$NON-NLS-2$ Thread.setDefaultUncaughtExceptionHandler(eh); MsgBox.DEFAULT_PROBLEM_REPORTER = eh; this.userDataFolder = applicationFolder.getUserDataFolder(); this.loginUrlsProvider = new PropertiesLoginUrlsProvider(CommandArgs.getPropertiesFile(applicationFolder)); displayVersion = RunMode.getRunMode().isDeveloper() ? version + "-dev" //$NON-NLS-1$ : version; List<? extends Image> iconImages = loadIconImages(); iconImage = iconImageBig != null ? iconImageBig : iconImageSmall; kdxploreIcon = new ImageIcon(iconImageSmall != null ? iconImageSmall : iconImageBig); setIconImages(iconImages); if (Util.isMacOS()) { try { System.setProperty("apple.laf.useScreenMenuBar", "true"); //$NON-NLS-1$ //$NON-NLS-2$ macapp = new MacApplication(null); macapp.setAboutHandler(aboutAction); macapp.setQuitHandler(exitAction); if (iconImage != null) { macapp.setDockIconImage(iconImage); } macapp.setPreferencesHandler(settingsAction); macapp.setAboutHandler(aboutAction); macapp.setQuitHandler(exitAction); } catch (MacApplicationException e) { macapp = null; Shared.Log.w(TAG, "isMacOS", e); //$NON-NLS-1$ } } if (iconImage != null) { this.setIconImage(iconImage); } clientProvider = new DefaultDALClientProvider(this, loginUrlsProvider, createBrandingImageComponent()); clientProvider.setCanChangeUrl(false); clientProvider.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { handleClientChanged(); } }); connectDisconnectActions = new ConnectDisconnectActions(clientProvider, Msg.TOOLTIP_CONNECT_TO_DATABASE(), Msg.TOOLTIP_DISCONNECT_FROM_DATABASE()); connectDisconnectActions.setConnectIntercept(connectIntercept); desktopSupport = new DefaultDesktopSupport(KDXploreFrame.this, baseTitle); frameWindowOpener = new DefaultFrameWindowOpener(desktopSupport) { @Override public Image getIconImage() { return iconImage; } }; frameWindowOpener.setOpenOnSameScreenAs(KDXploreFrame.this); setGlassPane(desktopSupport.getBlockingPane()); setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); addWindowListener(windowListener); initialiseKdxApps(); JMenuBar mb = buildMenuBar(); if (iconImage != null) { backgroundPanel.setBackgroundImage(iconImage); } else { backgroundPanel.setBackgroundImage(KDClientUtils.getImage(ImageId.DART_LOGO_128x87)); } backgroundPanel.setBackground(Color.decode("0xccffff")); //$NON-NLS-1$ splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, cardPanel, messagesPanel); splitPane.setResizeWeight(0.8); // = = = = = = Container cp = getContentPane(); cp.add(splitPane, BorderLayout.CENTER); statusInfoLine.setHorizontalAlignment(SwingConstants.LEFT); logoImagesLabel = new LogoImagesLabel(); JPanel bottom = new JPanel(new BorderLayout()); bottom.add(statusInfoLine, BorderLayout.CENTER); bottom.add(logoImagesLabel, BorderLayout.EAST); cp.add(bottom, BorderLayout.SOUTH); addWindowListener(new WindowAdapter() { @Override public void windowOpened(WindowEvent e) { removeWindowListener(this); logoImagesLabel.startCycling(); } }); showCard(CARD_KDXAPPS); setJMenuBar(mb); pack(); Dimension size = getSize(); boolean changed = false; if (size.width < MINIMUM_SIZE.width) { size.width = MINIMUM_SIZE.width; changed = true; } if (size.height < MINIMUM_SIZE.height) { size.height = MINIMUM_SIZE.height; changed = true; } if (changed) { setSize(size); } setLocationRelativeTo(null); final MemoryUsageMonitor mum = new MemoryUsageMonitor(); mum.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { updateStatusLineWithMemoryUsage(mum.getMemoryUsage()); } }); } private String updateSiteMessage; private void updateStatusLineWithMemoryUsage(String usage) { String updateMsg = updateSiteMessage; if (Check.isEmpty(updateMsg)) { statusInfoLine.setText(usage); } else { statusInfoLine.setText(usage + " [" + updateMsg + "]"); } } // Backup KdxApps are those that return true for canBackupDatabase(). // They are separate from active Apps because some may NOT have a user interface // in the normal case. private final List<BackupProvider> backupProviders = new ArrayList<>(); // Active KdxApps are those that are 'wanted' and also have a uiComponent. private final List<KdxApp> allKdxApps = new ArrayList<>(); private final Map<Component, KdxApp> appByComponent = new HashMap<>(); private ChangeListener kdxAppTabsChangeListener = new ChangeListener() { KdxApp previousApp = null; @Override public void stateChanged(ChangeEvent e) { Component comp = kdxAppTabs.getSelectedComponent(); if (comp != null) { KdxApp app = appByComponent.get(comp); if (app != null) { getRootPane().setDefaultButton(app.getDefaultButton()); JMenuBar mbar = getJMenuBar(); boolean mbarChanged = false; if (previousApp != null) { List<JMenu> menus = previousApp.getAppMenus(); if (!Check.isEmpty(menus)) { for (JMenu m : menus) { mbar.remove(m); mbarChanged = true; } } previousApp.setActive(false); } app.setActive(true); List<JMenu> menus = app.getAppMenus(); if (!Check.isEmpty(menus)) { int pos = 0; if (mbar.getMenuCount() > 0) { pos = 1; } for (JMenu m : menus) { mbar.add(m, pos); ++pos; mbarChanged = true; } } if (mbarChanged) { mbar.repaint(); } previousApp = app; } } } }; private void initialiseKdxApps() throws IOException { String[] classNames = KdxploreConfig.getInstance().getMainPluginClassNames(); if (classNames != null && classNames.length > 0) { List<String> classNamesToLoad = new ArrayList<>(); Collections.addAll(classNamesToLoad, classNames); if (!classNamesToLoad.contains(OFFLINE_DATA_APP_SERVICE_CLASS_NAME)) { classNamesToLoad.add(0, OFFLINE_DATA_APP_SERVICE_CLASS_NAME); classNames = classNamesToLoad.toArray(new String[classNamesToLoad.size()]); } } Map<KdxApp, Component> componentByApp = collectKdxApps(classNames); appByComponent.clear(); for (KdxApp app : componentByApp.keySet()) { Component comp = componentByApp.get(app); if (comp != null) { appByComponent.put(comp, app); } } allKdxApps.clear(); allKdxApps.addAll(componentByApp.keySet()); // Initialise the apps in initialisation order. allKdxApps.sort(Comparator.comparing(KdxApp::getInitialisationOrder)); // And while we're initialising them we collect // those that can perform a databaseBackup (i.e. have a BackupProvider). backupProviders.clear(); List<KdxApp> wantedAppsWithUi = new ArrayList<>(); for (KdxApp app : allKdxApps) { BackupProvider bp = app.getBackupProvider(); if (bp != null) { backupProviders.add(bp); } /** * See {@link com.diversityarrays.kdxplore.prefs.KdxplorePreferences#SHOW_ALL_APPS} */ if (appIsWanted(app)) { try { app.initialiseAppBeforeUpdateCheck(appInitContext); } catch (Exception e) { String msg = Msg.MSG_KDXAPP_INIT_PROBLEM(app.getAppName()); Shared.Log.w(TAG, msg, e); messagesPanel.println(msg); messagesPanel.println(e.getMessage()); } } if (appIsWanted(app) && null != componentByApp.get(app)) { wantedAppsWithUi.add(app); } } // - - - - - - - - - - - - - - - - - - - - - // Display the apps in display order. wantedAppsWithUi.sort(Comparator.comparing(KdxApp::getDisplayOrder)); backupProviders.sort(Comparator.comparing(BackupProvider::getDisplayOrder)); switch (wantedAppsWithUi.size()) { case 0: JLabel label = new JLabel(Msg.MSG_NO_KDXPLORE_APPS_AVAILABLE()); label.setHorizontalAlignment(JLabel.CENTER); cardPanel.add(label, CARD_KDXAPPS); break; case 1: KdxApp kdxApp = wantedAppsWithUi.get(0); Component uiComponent = componentByApp.get(kdxApp); Component appComponent = makeComponentForTab(kdxApp, uiComponent); cardPanel.add(appComponent, CARD_KDXAPPS); getRootPane().setDefaultButton(kdxApp.getDefaultButton()); String msg = Msg.MSG_SHOWING_KDXAPP(kdxApp.getAppName()); messagesPanel.println(msg); System.err.println(msg + " uiClass=" //$NON-NLS-1$ + uiComponent.getClass().getName()); break; default: kdxAppTabs = new JTabbedPane(JTabbedPane.LEFT); cardPanel.add(kdxAppTabs, CARD_KDXAPPS); Bag<String> tabsSeen = new HashBag<>(); for (KdxApp app : wantedAppsWithUi) { Component ui = componentByApp.get(app); String tabName = app.getAppName(); DevelopmentState devState = app.getDevelopmentState(); switch (devState) { case ALPHA: tabName = tabName + " (\u03b1)"; // TODO move to UnicodeChars break; case BETA: tabName = tabName + " (\u03b2)"; // TODO move to UnicodeChars break; case PRODUCTION: break; default: tabName = tabName + " " + devState.name(); break; } tabsSeen.add(tabName); int count = tabsSeen.getCount(tabName); if (count > 1) { tabName = tabName + "_" + count; //$NON-NLS-1$ } Component tabComponent = makeComponentForTab(app, ui); kdxAppTabs.addTab(tabName, tabComponent); if (macapp == null) { int index = kdxAppTabs.indexOfTab(tabName); if (index >= 0) { JLabel tabLabel = new JLabel(tabName); tabLabel.setBorder(new EmptyBorder(2, 2, 2, 2)); tabLabel.setUI(new VerticalLabelUI(VerticalLabelUI.UPWARDS)); kdxAppTabs.setTabComponentAt(index, tabLabel); } } messagesPanel.println(Msg.MSG_SHOWING_KDXAPP(tabName)); } kdxAppTabs.addChangeListener(kdxAppTabsChangeListener); kdxAppTabs.setSelectedIndex(0); break; } } private Component makeComponentForTab(KdxApp kdxApp, Component uiComponent) { Component appComponent = uiComponent; DevelopmentState devState = kdxApp.getDevelopmentState(); if (devState.getShouldShowHeading()) { JLabel heading = new JLabel(devState.getHeadingText(), JLabel.CENTER); heading.setOpaque(true); heading.setForeground(devState.getHeadingFontColor()); heading.setBackground(Color.LIGHT_GRAY); heading.setFont(heading.getFont().deriveFont(Font.BOLD)); JPanel p = new JPanel(new BorderLayout()); p.add(heading, BorderLayout.NORTH); //Color.DARK_GRAY, Font.BOLD, Color.LIGHT_GRAY, Color.GRAY // p.add(GuiUtil.createLabelSeparator(devState.getHeadingText(), fontColor, Font.BOLD, Color.LIGHT_GRAY, Color.GRAY), // BorderLayout.NORTH); p.add(uiComponent, BorderLayout.CENTER); appComponent = p; } return appComponent; } private Set<String> appNamesToHide = null; private boolean appIsWanted(KdxApp app) { if (appNamesToHide == null) { appNamesToHide = new HashSet<>(); if (!KdxplorePreferences.getInstance().getShowAllApps()) { appNamesToHide.addAll(ExplorerProperties.getInstance().getAppNamesToHide()); } } return !appNamesToHide.contains(app.getAppName()); } private Map<KdxApp, Component> collectKdxApps(String[] classNames) throws IOException { Map<KdxApp, Component> result = new HashMap<>(); BiConsumer<String, Either<Throwable, KdxAppService>> onServiceFound = new BiConsumer<String, Either<Throwable, KdxAppService>>() { @Override public void accept(String className, Either<Throwable, KdxAppService> either) { Throwable error = null; if (either.isRight()) { KdxAppService kdxAppService = either.right(); if (kdxAppService != null) { try { KdxApp kdxApp = kdxAppService.createKdxApp(pluginInfo); Component uiComponent = kdxApp.getUIComponent(); result.put(kdxApp, uiComponent); } catch (Exception | NoClassDefFoundError e) { error = e; } } } else { error = either.left(); } if (error != null) { String msg = Msg.MSG_PROBLEM_GETTING_KDXAPP(className); Shared.Log.w(TAG, msg, error); messagesPanel.println(msg); messagesPanel.println(error); } } }; Shared.detectServices(KdxAppService.class, onServiceFound, classNames); return result; } static class LogoImagesLabel extends JLabel implements Runnable { static class LogoImageData { public final String menuName; public final ImageIcon imageIcon; public final String url; LogoImageData(String menuName, String imageResourceName, String url) { this.menuName = menuName; this.url = url; ImageIcon icon = null; InputStream is = LogoImagesLabel.class.getResourceAsStream(imageResourceName); if (is != null) { try { icon = new ImageIcon(ImageIO.read(is)); } catch (IOException e) { Shared.Log.w("LogoImageData", //$NON-NLS-1$ "ctor: Missing resource: " + imageResourceName, e); //$NON-NLS-1$ } finally { try { is.close(); } catch (IOException ignore) { } } } imageIcon = icon; } } static private final LogoImageData[] LOGOS = { new LogoImageData("Diversity Arrays Technology", //$NON-NLS-1$ "DArT-53x30.png", //$NON-NLS-1$ "http://www.diversityarrays.com/"), //$NON-NLS-1$ new LogoImageData("www.kddart.org", //$NON-NLS-1$ "KDDart-53x30.png", //$NON-NLS-1$ "http://www.kddart.org/"), //$NON-NLS-1$ }; private boolean started = false; private JPopupMenu popupMenu; private final MouseListener mouseListener = new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (popupMenu == null) { popupMenu = new JPopupMenu(); for (LogoImageData lid : LOGOS) { Action action = new AbstractAction(lid.menuName) { @Override public void actionPerformed(ActionEvent e) { try { Util.openUrl(lid.url); } catch (IOException ignore) { } } }; popupMenu.add(action); } } popupMenu.show(LogoImagesLabel.this, e.getX(), e.getY()); } }; static private final long IMAGE_MILLIS = 10000; static private final long OPACITY_MILLIS = 80; private int showingIndex; private int overlayIndex = 0; private final List<BufferedImage> overlays = new ArrayList<>(); LogoImagesLabel() { for (float op = 1.0f; op >= 0.0f; op -= 0.1f) { BufferedImage bi = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); Graphics2D g = (Graphics2D) bi.getGraphics(); AlphaComposite transparent = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, op); g.setComposite(transparent); g.setColor(Color.WHITE); g.fillRect(0, 0, 1, 1); g.dispose(); overlays.add(bi); } overlayIndex = overlays.size() - 1; showNext(0); addMouseListener(mouseListener); setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); setToolTipText(Msg.MSG_CLICK_TO_VISIT_WEBSITE()); } private void showNext(int index) { showingIndex = index; setIcon(LOGOS[showingIndex].imageIcon); } public void startCycling() { if (!started && LOGOS.length > 1) { started = true; Thread t = new Thread(this); t.setDaemon(true); t.start(); } } @Override public void run() { try { Thread.sleep(IMAGE_MILLIS / 10); } catch (InterruptedException ignore) { } while (true) { try { overlayIndex = (overlayIndex + 1) % overlays.size(); if (overlayIndex == 0) { Thread.sleep(IMAGE_MILLIS); int index = (showingIndex + 1) % LOGOS.length; showNext(index); } else { repaint(); Thread.sleep(OPACITY_MILLIS); } } catch (InterruptedException ignore) { } } } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); if (overlays != null) { BufferedImage bi = overlays.get(overlayIndex); Rectangle bounds = getBounds(); g.drawImage(bi, 0, 0, bounds.width, bounds.height, null); } } } private BrandingImageComponent createBrandingImageComponent() { BrandingImageComponent bic = null; if (iconImage != null) { bic = new BrandingImageComponent(iconImage); bic.setBorder(new EmptyBorder(4, 4, 4, 4)); bic.setBackground(Color.decode("#ffe74a")); //$NON-NLS-1$ } return bic; } private List<? extends Image> loadIconImages() { List<Image> result = new ArrayList<>(); iconImageBig = KDClientUtils.getImage(ImageId.KDXPLORE_48); if (iconImageBig != null) { result.add(iconImageBig); } iconImageSmall = KDClientUtils.getImage(ImageId.KDXPLORE_24); if (iconImageSmall != null) { result.add(iconImageSmall); } return result; } private void showCard(String name) { cardLayout.show(cardPanel, name); } static private class BackgroundPanel extends JPanel { private Image backgroundImage; private int backgroundWidth = -1; private int backgroundHeight = -1; public BackgroundPanel() { super(new BorderLayout()); setOpaque(false); } public void setBackgroundImage(Image img) { backgroundImage = img; if (backgroundImage == null) { backgroundWidth = -1; backgroundHeight = -1; } else { backgroundWidth = backgroundImage.getWidth(null); backgroundHeight = backgroundImage.getHeight(null); } } @Override public void paint(Graphics g) { if (backgroundImage != null) { Rectangle vr = getVisibleRect(); Color save = g.getColor(); g.setColor(getBackground()); g.fillRect(0, 0, vr.width, vr.height); g.setColor(save); if (backgroundWidth > 0 && backgroundHeight > 0) { int x = Math.max(0, (vr.width - backgroundWidth) / 2); int y = Math.max(0, (vr.height - backgroundHeight) / 2); g.drawImage(backgroundImage, x, y, null); } else { g.drawImage(backgroundImage, vr.x, vr.y, vr.width, vr.height, this); } } super.paint(g); } } private void handleClientChanged() { if (clientProvider.isClientAvailable()) { DALClient client = clientProvider.getDALClient(); StringBuilder sb = new StringBuilder(baseTitle); sb.append(": ").append(client.getBaseUrl()) //$NON-NLS-1$ .append(" ").append(client.getUserName()); //$NON-NLS-1$ if (client.isInAdminGroup()) { sb.append(" **"); //$NON-NLS-1$ } sb.append('/').append(client.getGroupName()); String newTitle = sb.toString(); setTitle(newTitle); messagesPanel.println(Msg.MSG_LOGGED_IN(newTitle)); messagesPanel.println("DAL: " + clientProvider.getClientVersion()); //$NON-NLS-1$ } else { messagesPanel.println(Msg.MSG_LOGGED_OUT()); setTitle(baseTitle); } } private Action settingsAction = new AbstractAction(Msg.ACTION_PREFERENCES()) { @Override public void actionPerformed(ActionEvent e) { JFrame frame = frameWindowOpener.getWindowByIdentifier(KdxplorePreferences.class); if (frame == null) { frame = frameWindowOpener.addDesktopObject(new KdxplorePreferenceEditor( Msg.TITLE_KDXPLORE_PREFERENCES(applicationFolder.getApplicationName()))); frame.setSize(800, 600); frame.setLocationRelativeTo(null); } else { frame.toFront(); } } }; private Action updateAction = new AbstractAction(Msg.ACTION_CHECK_FOR_UPDATES()) { @Override public void actionPerformed(ActionEvent e) { if (updateChecker == null || Check.isEmpty(KdxploreConfig.getInstance().getUpdatebaseUrl())) { MsgBox.warn(KDXploreFrame.this, Msg.MSG_NO_UPDATE_AVAILABLE(), getTitle()); } else { String title = Msg.TITLE_UPDATE_CHECK(getTitle()); String url = KdxploreConfig.getInstance().getUpdatebaseUrl(); if (Check.isEmpty(url)) { MsgBox.warn(KDXploreFrame.this, Msg.MSG_NO_UPDATE_URL(), title); } else { UpdateCheckContext ctx = createUpdateCheckContext(url); updateChecker.execute(ctx); } } } }; private final DesktopSupport desktopSupport; private Action exitAction = new AbstractAction(Msg.ACTION_EXIT()) { @Override public void actionPerformed(ActionEvent e) { // boolean quit = JOptionPane.YES_OPTION == JOptionPane // .showConfirmDialog(KDXploreFrame.this, "Are you sure?", // "Quit", JOptionPane.YES_NO_OPTION); boolean quit = true; if (quit) { if (macapp != null) { System.out.println(e.getClass()); } System.exit(0); } } }; private MacApplication macapp; private JMenuBar buildMenuBar() { RunMode runMode = RunMode.getRunMode(); JMenuBar mb = new JMenuBar(); mb.add(createMainMenu(runMode)); if (!backupProviders.isEmpty()) { String toolsMenuLabel = Msg.MENU_TOOLS(); JMenu toolsMenu = new JMenu(toolsMenuLabel); mb.add(toolsMenu); for (Action a : pluginInfo.getToolsMenuActions()) { toolsMenu.add(a); } Action backupAction = new AbstractAction(Msg.MENUITEM_BACKUP_DB()) { @Override public void actionPerformed(ActionEvent e) { doBackupDatabase(); } }; backupAction.putValue(Action.SMALL_ICON, KDClientUtils.getIcon(ImageId.DB_BACKUP)); toolsMenu.add(backupAction); boolean seen = false; for (BackupProvider bp : backupProviders) { Action a = bp.getOfflineDataAction(); if (a != null) { if (!seen) { seen = true; toolsMenu.add(new JSeparator()); } toolsMenu.add(a); } } } mb.add(frameWindowOpener.getWindowsMenu()); mb.add(createHelpMenu()); return mb; } private JMenu createHelpMenu() { JMenu helpMenu = new JMenu(Msg.MENU_HELP()); helpMenu.add(aboutAction); if (Desktop.isDesktopSupported()) { helpMenu.add(onlineHelpAction); helpMenu.add(new ReportIssueAction(this)); } return helpMenu; } private JMenu createMainMenu(RunMode runMode) { JMenu mainMenu = new JMenu(Msg.MENU_FILE()); mainMenu.add(connectDisconnectActions.connectAction); mainMenu.add(connectDisconnectActions.disconnectAction); mainMenu.addSeparator(); KDClientUtils.initAction(ImageId.SETTINGS_24, settingsAction, Msg.TOOLTIP_CHANGE_PREFS(), true); mainMenu.add(settingsAction); mainMenu.addSeparator(); mainMenu.add(updateAction); mainMenu.addSeparator(); mainMenu.add(exitAction); return mainMenu; } private JLabel statusInfoLine = new JLabel(); private WindowAdapter windowListener = new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { JFrame[] openWindows = frameWindowOpener.getWrappingWindows(); int nWindows = openWindows.length; if (nWindows <= 0) { if (RunMode.getRunMode().isDeveloper()) { System.exit(0); } boolean closeWithoutAsking = KdxplorePreferences.getInstance().getCloseKdxploreWithoutAsking(); if (closeWithoutAsking) { System.exit(0); } JCheckBox dontAskAgain = new JCheckBox(Msg.OPTION_CLOSE_WITHOUT_ASKING_IN_FUTURE()); Box box = Box.createVerticalBox(); box.add(new JLabel(Msg.MSG_DO_YOU_REALLY_WANT_TO_CLOSE())); box.add(dontAskAgain); int answer = JOptionPane.showConfirmDialog(KDXploreFrame.this, box, getTitle(), JOptionPane.YES_NO_OPTION); if (dontAskAgain.isSelected()) { KdxplorePreferences.getInstance().saveCloseKdxploreWithoutAsking(true); } if (JOptionPane.YES_OPTION == answer) { System.exit(0); } } else if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(KDXploreFrame.this, Msg.MSG_YOU_STILL_HAVE_ACTIVITIES_ARE_YOU_SURE(nWindows), getTitle(), JOptionPane.YES_NO_OPTION)) { try { for (JFrame f : openWindows) { f.dispose(); } } finally { System.exit(0); } } else { GuiUtil.restoreFrame(openWindows[0]); } } @Override public void windowOpened(WindowEvent e) { splitPane.setDividerLocation(0.8); KdxploreConfig config = KdxploreConfig.getInstance(); String url = config.getUpdatebaseUrl(); if (url != null && !url.isEmpty()) { beginUpdateCheck(url); } else { doPostUpdateCheckAppInit(); } } @Override public void windowClosed(WindowEvent e) { clientProvider.logout(); // Note: any errors here we don't try to recover from. for (KdxApp app : allKdxApps) { app.shutdown(); } KdxplorePreferences.getInstance().saveOnShutdown(); } }; private final AppInitContext appInitContext = new AppInitContext() { final Map<String, Object> map = new HashMap<>(); @Override public JFrame getKdxploreFrame() { return KDXploreFrame.this; } @SuppressWarnings("unchecked") @Override public <T> T getContextValue(Class<T> tclass, String key) { T result = null; Object tmp = map.get(key); if (tmp != null) { if (tclass.isAssignableFrom(tmp.getClass())) { result = (T) tmp; } } return result; } @Override public <T> void setContextValue(Class<T> tclass, String key, T value) { T tmp = getContextValue(tclass, key); if (tmp != null) { if (!tclass.isAssignableFrom(tmp.getClass())) { throw new IllegalStateException("Incompatible context value, expecting: " + tclass.getName() //$NON-NLS-1$ + " got: " + tmp.getClass().getName()); //$NON-NLS-1$ } } map.put(key, value); } }; private final KdxPluginInfo pluginInfo = new KdxPluginInfo() { @Override public JFrame getKdxploreFrame() { return KDXploreFrame.this; } @Override public WindowOpener<JFrame> getWindowOpener() { return frameWindowOpener; } @Override public File getUserDataFolder() { return userDataFolder; } @Override public MessagePrinter getMessagePrinter() { return messagesPanel; } @Override public PrintStream getMessagePrintStream() { return messagesPanel.getPrintStream(); } @Override public PrintStreamMessageLogger getMessageLogger() { return messageLogger; } @Override public ImageIcon getKdxploreIcon() { return kdxploreIcon; } @Override public DALClientProvider getClientProvider() { return clientProvider; } @Override public BackgroundRunner getBackgroundRunner() { return desktopSupport; } final List<Action> allToolsMenuActions = new ArrayList<>(); @Override public void addToolsMenuActions(Action... actions) { Collections.addAll(allToolsMenuActions, actions); } @Override public Action[] getToolsMenuActions() { return allToolsMenuActions.toArray(new Action[allToolsMenuActions.size()]); } final Map<KdxApp, List<JMenu>> menusByApp = new LinkedHashMap<>(); @Override public Set<KdxApp> getAppsWithMenus() { return menusByApp.keySet(); } @Override public List<JMenu> getMainMenuActions(KdxApp app) { List<JMenu> result = menusByApp.get(app); if (result == null) { result = Collections.emptyList(); } else { result = Collections.unmodifiableList(result); } return result; } @Override public void addMenus(KdxApp app, List<JMenu> menus) { List<JMenu> list = menusByApp.get(app); if (list == null) { list = new ArrayList<>(); menusByApp.put(app, list); } list.addAll(menus); } @Override public void removeAppFromView(KdxApp kdxApp) { if (kdxAppTabs == null) { cardPanel.remove(kdxApp.getUIComponent()); JLabel label = new JLabel(Msg.MSG_NO_KDXPLORE_APPS_AVAILABLE()); label.setHorizontalAlignment(JLabel.CENTER); cardPanel.add(label, CARD_KDXAPPS); } else { final Component ui = kdxApp.getUIComponent(); if (ui != null) { int foundTabIndex = -1; for (int tabIndex = kdxAppTabs.getTabCount(); --tabIndex >= 0;) { Component tabComponent = kdxAppTabs.getComponentAt(tabIndex); if (tabComponent == ui) { foundTabIndex = tabIndex; break; } } if (foundTabIndex >= 0) { kdxAppTabs.removeTabAt(foundTabIndex); if (kdxAppTabs.getTabCount() == 1) { Component lastUi = kdxAppTabs.getComponentAt(0); cardPanel.remove(kdxAppTabs); cardPanel.add(lastUi, CARD_KDXAPPS); cardLayout.show(cardPanel, CARD_KDXAPPS); kdxAppTabs = null; } } } // TODO consider doing shutdown on the App and removing from activeKdxApps } } @Override public void appWantsToHide(KdxApp kdxApp) { ExplorerProperties.getInstance().addAppNameToHide(kdxApp.getAppName()); KdxplorePreferences.getInstance().setShowAllApps(false); } @Override public <R> R setSingletonSharedResource(Class<? extends R> rClass, R resource) { R previous = getSingletonSharedResource(rClass); sharedResources.put(rClass, resource); return previous; } private final Map<Class<?>, Object> sharedResources = new HashMap<>(); @SuppressWarnings("unchecked") @Override public <R> R getSingletonSharedResource(Class<R> rClass) { Object obj = sharedResources.get(rClass); if (obj != null && rClass.isAssignableFrom(obj.getClass())) { return (R) obj; } return null; } }; private void doPostUpdateCheckAppInit() { int nFailed = 0; for (KdxApp app : allKdxApps) { AfterUpdateResult check = app.initialiseAppAfterUpdateCheck(appInitContext); switch (check) { case ABORT: dispose(); break; case FAIL_IF_ALL: ++nFailed; break; case OK: break; } } if (nFailed > 0 && nFailed == allKdxApps.size()) { dispose(); } } private TimerTask updateCheckTask = new TimerTask() { UpdateDialog updateDialog; @Override public void run() { // Only do this if not already showing the dialog and we have a URL to use if (updateDialog == null && !Check.isEmpty(updateCheckUrl)) { UpdateCheckRequest request = new UpdateCheckRequest(KDXploreFrame.this, versionCode, version, false, updateCheckUrl); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { updateSiteMessage = "Checking for update on " + request.updateSite; //$NON-NLS-1$ statusInfoLine.setText(updateSiteMessage); } }); Consumer<String> onReceiveComplete = new Consumer<String>() { @Override public void accept(String msg) { updateSiteMessage = null; if (CheckStatus.UPDATE_AVAILABLE == request.checkStatus) { if (request.kdxploreUpdate != null) { UpdateCheckContext ctx = createUpdateCheckContext(updateCheckUrl); updateDialog = new UpdateDialog(request, ctx, KdxConstants.getVersionInfo()); updateDialog.addWindowListener(new WindowAdapter() { @Override public void windowClosed(WindowEvent e) { updateDialog = null; } }); updateDialog.handleCheckCompleted(msg); // new Toast(KDXploreFrame.this, // "Update " + request.kdxploreUpdate.versionName + " is Available", // Toast.LONG).show(); } } } }; SwingWorker<String, Void> worker = request.createWorker(messagesPanel.getPrintStream(), onReceiveComplete); worker.execute(); } } }; private String updateCheckUrl; private long updateCheckDelayMillis = EVERY_5_DAYS; private long updateCheckPeriodMillis = EVERY_5_DAYS; private Timer updateCheckTimerDaemon; private Date lastUpdateCheck; private void beginUpdateCheck(String url) { updateCheckUrl = url; // TODO: make this happen each 5 days in a "quiet" fashion UpdateCheckRequest request = new UpdateCheckRequest(KDXploreFrame.this, versionCode, version, false, url); UpdateCheckContext ctx = createUpdateCheckContext(url); UpdateDialog updateDialog = new UpdateDialog(request, ctx, KdxConstants.getVersionInfo()); updateDialog.addWindowListener(new WindowAdapter() { @Override public void windowClosed(WindowEvent e) { lastUpdateCheck = new Date(); if (updateCheckTimerDaemon == null) { updateCheckTimerDaemon = new Timer("UpdateCheckTimer", true); //$NON-NLS-1$ updateCheckTimerDaemon.scheduleAtFixedRate(updateCheckTask, updateCheckDelayMillis, updateCheckPeriodMillis); } doPostUpdateCheckAppInit(); } }); updateDialog.setVisible(true); } private void doBackupDatabase() { switch (backupProviders.size()) { case 0: MsgBox.warn(this, Msg.MSG_NO_DB_BACKUP_APPS_AVAILABLE(), getTitle()); return; case 1: backupProviders.get(0).doDatabaseBackup(this); break; default: Map<String, BackupProvider> bpByName = backupProviders.stream() .collect(Collectors.toMap(BackupProvider::getBackupProviderName, Function.identity())); String[] choices = backupProviders.stream().map(BackupProvider::getBackupProviderName) .collect(Collectors.toList()).toArray(new String[backupProviders.size()]); Object choice = JOptionPane.showInputDialog(this, Msg.MSG_SELECT_APP_FOR_BACKUP(), Msg.TITLE_BACKUP_DATABASE(), JOptionPane.QUESTION_MESSAGE, null, choices, choices[0]); if (choice != null) { BackupProvider bp = bpByName.get(choice); bp.doDatabaseBackup(this); } break; } } protected UpdateCheckContext createUpdateCheckContext(String url) { UpdateCheckContext ctx = new UpdateCheckContext() { @Override public Window getWindow() { return KDXploreFrame.this; } @Override public PrintStream getPrintStream() { return messagesPanel.getPrintStream(); } @Override public String getBaseUrl() { return url; } @Override public void backupDatabase() { doBackupDatabase(); } @Override public boolean canBackupDatabase() { return !backupProviders.isEmpty(); } }; return ctx; } }