From 3fc8acba703e212983e519c5a7140dbf4ef8407a Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Tue, 31 Jul 2018 14:55:19 -0400 Subject: [PATCH] Global hotkey, iconifying and application activation, help text. --- platform-independent/java/gui/build.gradle | 1 + .../com/lyndir/masterpassword/gui/GUI.java | 15 +++++- .../masterpassword/gui/util/Components.java | 32 ++++++++++-- .../lyndir/masterpassword/gui/util/Res.java | 4 ++ .../gui/util/platform/ApplePlatform.java | 30 +++++++++-- .../gui/util/platform/BasePlatform.java | 11 ++++ .../gui/util/platform/IPlatform.java | 6 +++ .../gui/util/platform/JDK9Platform.java | 30 +++++++++-- .../gui/view/UserContentPanel.java | 47 ++++++++++++++++-- .../src/main/resources/media/icon_help.png | Bin 0 -> 2173 bytes .../src/main/resources/media/icon_help@2x.png | Bin 0 -> 3827 bytes 11 files changed, 158 insertions(+), 18 deletions(-) create mode 100644 platform-independent/java/gui/src/main/resources/media/icon_help.png create mode 100644 platform-independent/java/gui/src/main/resources/media/icon_help@2x.png 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 0000000000000000000000000000000000000000..c51cdca2957612b366b74cb9f171720b1be1d3de GIT binary patch literal 2173 zcmbVOc~nzp7LNr%mMDTEi`CbF;3y>T1+q#A2_eW5U;tUdW=I|+Ly{M>03x!eRS=gd zF2fiVsI7_$MMkR!L3Rr`6rBo!3ZhkpqLm7RY=OQgI5X#X`p10dy!XE4ckl1s`~B`Y zFFz#67i(%~iosy8ew#U==oipGON`LBDkEbIgE2^wgh#2Pc!6{wBEt(rNFt0+mnqP5 z490_*t`G=QU^S2kCrjiE+?(p#I6xv|;37#pkf-p5#gfe#N;oVdC|sD4BBY6MOi#cg zosK$?!D<1JE|bbt^mGR9y{cEUF9l=1rHopB~(fjY6&6-^d1F?h(^u8p^<(_L8jpG zK021G<_m?EjF2u+5Fk8AkjeCUy+^Cmq40mg_#|2tzFh$mLSYr6Q3_!c?yz8t*6xpq z^guKkdazQ0UW!1e-a&?4RJc?&Ce>7geY0kCO13xX~w0H(Pn~+KkM#wA`=0QB>v64(0CCPS^f8 zt$laq@VhhNV9SQt;BcwacSUsudk>yUL`NEEGXt(UJf{tG%687O_F7Axs9TigaAcpK z7X*UEriP#Gm6Gipmc}F`5KP<^bv}U?Ic+>Ih)(ZazyFzs(GM?_E|&RCQt~52r_-&v z#(xducuGfd4e*H;F6qpN(ClD? zqsY1Q$&;Ee3qxP0!ups327{4se6%`zpB5 zD|UrjO-;>UYieE&_PQo1>E&W6o&N@deHP$oS%^PrRyATcxSDOAShC1fj0SzLySsb6 zlV#<(V~G*NX+qJa%@@A#@FbBQ1nq7K9WpNr9m>wh!hhdkZEfB6!XS5t1sr`de%jyPa&%I+?HXUVH2ac&;P?G+ZnIPF;(MeNlHET}PFXo;nfu~~40}rNT{uPE z(DCG8l-R4?laicsU27N~5Rk32TxI3{!;9gu$LtHw0;+uTntdjEdwcuN4^o!+p6=MR z=3(hB;NEDJw&1VU;<~}q>hqP6KRXtWPi3yL%PYE@7Y}z$>VK0+v4c0tMD^pJw_%TWDk!_DQUH!edC#Pp%*{5J1_UNtFna0874)eE^?5mvDkt_p# z=br1Ad17Lb@l3tNsPV3WLx&EvST<0HgRSD%-Qqlq{!5XC8CD!!JK41%&8m#gk2#EY z?LRqFvgM_M+OcwVW9E+iE8RW+-F(GVFB6rjAmUf3=@BWLA-0YCZ8F+%!?~6_cb1Jl zJBsWWXy1Kye?vsRO;f`%*IHlEN?c%C9FNEAepNwXSmZT7zm!^C^EF^%{?L2k*qpUf zhzY7^Gi+HN;vFXcQ^$`DwFbt$%v2YuaAQ1k=b?42&wsO-^by*x9os%zP*`eXXBTQY z=yD^E<>Fp^JfTJTtAULud%C?sTkE;AQH3-Id_!)zFk;v^F!CYa8)`A}h9=!B(Z=RT|_+KlFFGpD`d zHk%&x8o%-2)`QMYMeo*`MCq&hFPevXXlJMUb{Z8gv(J2*(=NQuXghuxG`ZZ5&w5U4 zU~-*J&QpVwkCu1zGz(YEwOqMDUB2RTY?)nlyHj7cBH@8UTsvjXR@XgYf66RFc&B&q Q3H@J#pHC2{oV|6|@28z@Q~&?~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..de9a996af6ab05d3c23c3c9bfd1be2019c0d0bf2 GIT binary patch literal 3827 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 literal 0 HcmV?d00001