diff --git a/platform-independent/java/gui/build.gradle b/platform-independent/java/gui/build.gradle index 04c2134e..6e3569cc 100644 --- a/platform-independent/java/gui/build.gradle +++ b/platform-independent/java/gui/build.gradle @@ -11,6 +11,7 @@ dependencies { implementation group: 'com.lyndir.lhunath.opal', name: 'opal-system', version: '1.7-p2' implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.1.2' implementation group: 'com.yuvimasory', name: 'orange-extensions', version: '1.3.0' + implementation group: 'com.github.tulskiy', name: 'jkeymaster', version: '1.2' compile project( ':masterpassword-model' ) } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/GUI.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/GUI.java index 3b5b3d3a..cf37a42d 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/GUI.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/GUI.java @@ -4,6 +4,10 @@ import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.masterpassword.gui.util.Platform; import com.lyndir.masterpassword.gui.util.Res; import com.lyndir.masterpassword.gui.view.MasterPasswordFrame; +import com.tulskiy.keymaster.common.Provider; +import java.awt.*; +import java.awt.event.*; +import javax.swing.*; /** @@ -18,9 +22,18 @@ public class GUI { public GUI() { Platform.get().installAppForegroundHandler( this::open ); Platform.get().installAppReopenHandler( this::open ); + + KeyStroke keyStroke = KeyStroke.getKeyStroke( KeyEvent.VK_P, InputEvent.CTRL_DOWN_MASK | InputEvent.META_DOWN_MASK ); + Provider.getCurrentProvider( true ).register( keyStroke, hotKey -> open() ); } public void open() { - Res.ui( () -> frame.setVisible( true ) ); + Res.ui( () -> { + frame.setAlwaysOnTop( true ); + frame.setVisible( true ); + frame.setExtendedState( Frame.NORMAL ); + frame.setAlwaysOnTop( false ); + Platform.get().requestForeground(); + } ); } } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Components.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Components.java index 99107ae9..8b38f234 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Components.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Components.java @@ -18,10 +18,12 @@ package com.lyndir.masterpassword.gui.util; +import com.lyndir.lhunath.opal.system.logging.Logger; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; +import java.net.URISyntaxException; import java.util.Arrays; import java.util.Collection; import java.util.function.Consumer; @@ -30,9 +32,9 @@ import javax.annotation.Nullable; import javax.swing.*; import javax.swing.border.Border; import javax.swing.border.CompoundBorder; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; +import javax.swing.event.*; import javax.swing.text.DefaultFormatterFactory; +import org.jetbrains.annotations.NonNls; /** @@ -41,6 +43,8 @@ import javax.swing.text.DefaultFormatterFactory; @SuppressWarnings({ "SerializableStoresNonSerializable", "serial" }) public abstract class Components { + private static final Logger logger = Logger.get( Components.class ); + public static final float TEXT_SIZE_HEADING = 19f; public static final float TEXT_SIZE_CONTROL = 13f; public static final int SIZE_MARGIN = 12; @@ -100,11 +104,11 @@ public abstract class Components { showDialog( dialog ); Object selectedValue = pane.getValue(); - if(selectedValue == null) + if (selectedValue == null) return JOptionPane.CLOSED_OPTION; Object[] options = pane.getOptions(); - if(options == null) + if (options == null) return (selectedValue instanceof Integer)? (Integer) selectedValue: JOptionPane.CLOSED_OPTION; int option = Arrays.binarySearch( options, selectedValue ); @@ -337,7 +341,7 @@ public abstract class Components { BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ); DefaultFormatterFactory formatterFactory = new DefaultFormatterFactory(); if (model instanceof UnsignedIntegerModel) - formatterFactory.setDefaultFormatter( ((UnsignedIntegerModel)model).getFormatter() ); + formatterFactory.setDefaultFormatter( ((UnsignedIntegerModel) model).getFormatter() ); ((DefaultEditor) getEditor()).getTextField().setFormatterFactory( formatterFactory ); ((DefaultEditor) getEditor()).getTextField().setBorder( editorBorder ); setAlignmentX( LEFT_ALIGNMENT ); @@ -474,6 +478,24 @@ public abstract class Components { }; } + public static JEditorPane linkLabel(@NonNls final String html) { + return new JEditorPane( "text/html", "" + html ) { + { + setOpaque( false ); + setEditable( false ); + addHyperlinkListener( event -> { + if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) + try { + Platform.get().open( event.getURL().toURI() ); + } + catch (final URISyntaxException e) { + logger.err( e, "After triggering hyperlink: %s", event ); + } + } ); + } + }; + } + public static class GradientPanel extends JPanel { @Nullable diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Res.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Res.java index 4b8496cc..d32a0c7f 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Res.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Res.java @@ -138,6 +138,10 @@ public abstract class Res { return icon( "media/icon_import.png" ); } + public Icon help() { + return icon( "media/icon_help.png" ); + } + public Icon export() { return icon( "media/icon_export.png" ); } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/ApplePlatform.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/ApplePlatform.java index 961bcc89..04da5931 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/ApplePlatform.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/ApplePlatform.java @@ -3,9 +3,9 @@ package com.lyndir.masterpassword.gui.util.platform; import com.apple.eawt.*; import com.apple.eio.FileManager; import com.google.common.base.Preconditions; -import com.google.common.base.Throwables; -import java.io.File; -import java.io.FileNotFoundException; +import com.lyndir.lhunath.opal.system.logging.Logger; +import java.io.*; +import java.net.URI; /** @@ -13,7 +13,8 @@ import java.io.FileNotFoundException; */ public class ApplePlatform implements IPlatform { - static Application application = Preconditions.checkNotNull( + private static final Logger logger = Logger.get( ApplePlatform.class ); + private static final Application application = Preconditions.checkNotNull( Application.getApplication(), "Not an Apple Java application." ); @Override @@ -37,12 +38,31 @@ public class ApplePlatform implements IPlatform { return true; } + @Override + public boolean requestForeground() { + application.requestForeground( true ); + return true; + } + @Override public boolean show(final File file) { try { return FileManager.revealInFinder( file ); } - catch (final FileNotFoundException ignored) { + catch (final FileNotFoundException e) { + logger.err( e, "While showing: %s", file ); + return false; + } + } + + @Override + public boolean open(final URI url) { + try { + FileManager.openURL( url.toString() ); + return true; + } + catch (final IOException e) { + logger.err( e, "While opening: %s", url ); return false; } } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/BasePlatform.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/BasePlatform.java index b6b41ea3..38884f40 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/BasePlatform.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/BasePlatform.java @@ -1,6 +1,7 @@ package com.lyndir.masterpassword.gui.util.platform; import java.io.File; +import java.net.URI; /** @@ -18,8 +19,18 @@ public class BasePlatform implements IPlatform { return false; } + @Override + public boolean requestForeground() { + return false; + } + @Override public boolean show(final File file) { return false; } + + @Override + public boolean open(final URI url) { + return false; + } } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/IPlatform.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/IPlatform.java index a6f3d48e..a568d9fb 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/IPlatform.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/IPlatform.java @@ -1,6 +1,8 @@ package com.lyndir.masterpassword.gui.util.platform; import java.io.File; +import java.net.URI; +import java.net.URL; /** @@ -12,5 +14,9 @@ public interface IPlatform { boolean installAppReopenHandler(Runnable handler); + boolean requestForeground(); + boolean show(File file); + + boolean open(URI url); } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/JDK9Platform.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/JDK9Platform.java index 1bc94782..b11a4f2f 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/JDK9Platform.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/JDK9Platform.java @@ -1,8 +1,11 @@ package com.lyndir.masterpassword.gui.util.platform; +import com.lyndir.lhunath.opal.system.logging.Logger; import java.awt.*; import java.awt.desktop.*; import java.io.File; +import java.io.IOException; +import java.net.URI; /** @@ -11,9 +14,12 @@ import java.io.File; @SuppressWarnings("Since15") public class JDK9Platform implements IPlatform { + private static final Logger logger = Logger.get( JDK9Platform.class ); + private static final Desktop desktop = Desktop.getDesktop(); + @Override public boolean installAppForegroundHandler(final Runnable handler) { - Desktop.getDesktop().addAppEventListener( new AppForegroundListener() { + desktop.addAppEventListener( new AppForegroundListener() { @Override public void appRaisedToForeground(final AppForegroundEvent e) { handler.run(); @@ -28,7 +34,13 @@ public class JDK9Platform implements IPlatform { @Override public boolean installAppReopenHandler(final Runnable handler) { - Desktop.getDesktop().addAppEventListener( (AppReopenedListener) e -> handler.run() ); + desktop.addAppEventListener( (AppReopenedListener) e -> handler.run() ); + return true; + } + + @Override + public boolean requestForeground() { + desktop.requestForeground( true ); return true; } @@ -37,7 +49,19 @@ public class JDK9Platform implements IPlatform { if (!file.exists()) return false; - Desktop.getDesktop().browseFileDirectory( file ); + desktop.browseFileDirectory( file ); return true; } + + @Override + public boolean open(final URI url) { + try { + desktop.browse( url ); + return true; + } + catch (final IOException e) { + logger.err( e, "While opening: %s", url ); + return false; + } + } } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserContentPanel.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserContentPanel.java index 4e14fd50..0c537e44 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserContentPanel.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserContentPanel.java @@ -49,6 +49,8 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, "Add a new user to Master Password." ); private final JButton importButton = Components.button( Res.icons().import_(), event -> importUser(), "Import a user from a backup file into Master Password." ); + private final JButton helpButton = Components.button( Res.icons().help(), event -> showHelp(), + "Show information on how to use Master Password." ); private final JPanel userToolbar = Components.panel( BoxLayout.PAGE_AXIS ); private final JPanel siteToolbar = Components.panel( BoxLayout.PAGE_AXIS ); @@ -128,7 +130,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, } private void addUser() { - JTextField nameField = Components.textField( "Robert Lee Mitchell", null ); + JTextField nameField = Components.textField( "Robert Lee Mitchell", null ); JCheckBox incognitoField = Components.checkBox( "Incognito (Do not save this user to disk)" ); if (JOptionPane.OK_OPTION != Components.showDialog( this, "Add User", new JOptionPane( Components.panel( BoxLayout.PAGE_AXIS, @@ -208,7 +210,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, this, strf( "Couldn't read import file:
%s
.", e.getLocalizedMessage() ), "Import Failed", JOptionPane.ERROR_MESSAGE ); } - catch (MPMarshalException e) { + catch (final MPMarshalException e) { logger.err( e, "While parsing user import file." ); JOptionPane.showMessageDialog( this, strf( "Couldn't parse import file:
%s
.", e.getLocalizedMessage() ), @@ -216,6 +218,31 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, } } + private void showHelp() { + JOptionPane.showMessageDialog( this, Components.linkLabel( strf( + "

Master Password

" + + "

The primary goal of this application is to provide a reliable security solution that also " + + "makes you independent from your computer. If you lose access to this computer or your data, " + + "the application can regenerate all your secrets from scratch on any new device.

" + + "

Opening Master Password

" + + "

To use Master Password, simply open the application on your computer. " + + "Once running, you can bring up the user interface at any time by pressing the keys " + + "%s + %s + p." + + "

Persistence

" + + "

Though at the core, Master Password does not require the use of any form of data " + + "storage, the application does remember the names of the sites you've used in the past to " + + "make it easier for you to use them again in the future. All user information is saved in " + + "files on your computer at the following location:

%s

" + + "

You can read, modify, backup or place new files in this location as you see fit. " + + "Some people even configure this location to be synced between their different computers " + + "using services such as those provided by SpiderOak or Dropbox.

" + + "

https://masterpassword.app — by Maarten Billemont

", + KeyEvent.getKeyText( KeyEvent.VK_CONTROL ), + KeyEvent.getKeyText( KeyEvent.VK_META ), + MPFileUserManager.get().getPath().getAbsolutePath() ) ), + "About Master Password", JOptionPane.INFORMATION_MESSAGE ); + } + private enum ContentMode { NO_USER, AUTHENTICATE, @@ -239,6 +266,8 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, userToolbar.add( addButton ); userToolbar.add( importButton ); + userToolbar.add( Box.createGlue() ); + userToolbar.add( helpButton ); add( Box.createGlue() ); add( Components.heading( "Select a user to proceed." ) ); @@ -275,6 +304,8 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, userToolbar.add( exportButton ); userToolbar.add( deleteButton ); userToolbar.add( resetButton ); + userToolbar.add( Box.createGlue() ); + userToolbar.add( helpButton ); add( Components.heading( user.getFullName(), SwingConstants.CENTER ) ); add( Components.strut() ); @@ -461,6 +492,8 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, userToolbar.add( addButton ); userToolbar.add( userButton ); userToolbar.add( logoutButton ); + userToolbar.add( Box.createGlue() ); + userToolbar.add( helpButton ); siteToolbar.add( settingsButton ); siteToolbar.add( questionsButton ); @@ -615,8 +648,14 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, Res.ui( () -> { Window window = SwingUtilities.windowForComponent( UserContentPanel.this ); - if (window != null) - window.dispatchEvent( new WindowEvent( window, WindowEvent.WINDOW_CLOSING ) ); + if (window instanceof Frame) { + ((Frame) window).setExtendedState( Frame.ICONIFIED ); + window.dispatchEvent( new WindowEvent( window, WindowEvent.WINDOW_DEACTIVATED ) ); + window.dispatchEvent( new WindowEvent( window, WindowEvent.WINDOW_ICONIFIED ) ); + window.dispatchEvent( new WindowEvent( window, WindowEvent.WINDOW_LOST_FOCUS ) ); + window.dispatchEvent( new WindowEvent( window, WindowEvent.WINDOW_CLOSED ) ); + } + // window.dispatchEvent( new WindowEvent( window, WindowEvent.WINDOW_ICONIFIED ) ); } ); } ); } diff --git a/platform-independent/java/gui/src/main/resources/media/icon_help.png b/platform-independent/java/gui/src/main/resources/media/icon_help.png new file mode 100644 index 00000000..c51cdca2 Binary files /dev/null and b/platform-independent/java/gui/src/main/resources/media/icon_help.png differ diff --git a/platform-independent/java/gui/src/main/resources/media/icon_help@2x.png b/platform-independent/java/gui/src/main/resources/media/icon_help@2x.png new file mode 100644 index 00000000..de9a996a Binary files /dev/null and b/platform-independent/java/gui/src/main/resources/media/icon_help@2x.png differ