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