diff --git a/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/MPIdenticon.java b/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/MPIdenticon.java index d0b4d0d6..3bd4d726 100644 --- a/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/MPIdenticon.java +++ b/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/MPIdenticon.java @@ -27,6 +27,7 @@ import com.lyndir.lhunath.opal.system.logging.Logger; import java.nio.*; import java.nio.charset.Charset; import java.util.Arrays; +import java.util.Locale; /** @@ -83,6 +84,10 @@ public class MPIdenticon { return text; } + public String getHTML() { + return strf( "%s", color.getCSS(), text ); + } + public Color getColor() { return color; } @@ -94,6 +99,15 @@ public class MPIdenticon { BLUE, MAGENTA, CYAN, - MONO + MONO { + @Override + public String getCSS() { + return "inherit"; + } + }; + + public String getCSS() { + return name().toLowerCase( Locale.ROOT ); + } } } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/Res.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/Res.java index ccfd3399..66aec042 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/Res.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/Res.java @@ -180,20 +180,20 @@ public abstract class Res { public static class Fonts { - public Font emoticonsFont() { - return emoticonsRegular(); + public Font emoticonsFont(final float size) { + return emoticonsRegular().deriveFont( size ); } - public Font controlFont() { - return exoRegular(); + public Font controlFont(final float size) { + return exoRegular().deriveFont( size ); } - public Font valueFont() { - return sourceSansProRegular(); + public Font valueFont(final float size) { + return sourceSansProRegular().deriveFont( size ); } - public Font bigValueFont() { - return sourceSansProBlack(); + public Font bigValueFont(final float size) { + return sourceSansProBlack().deriveFont( size ); } public Font emoticonsRegular() { @@ -268,12 +268,17 @@ public abstract class Res { public static class Colors { + private final Color transparent = new Color( 0, 0, 0, 0 ); private final Color frameBg = Color.decode( "#5A5D6B" ); private final Color controlBg = SystemColor.window; private final Color controlBorder = Color.decode( "#BFBFBF" ); private final Color highlightFg = SystemColor.controlHighlight; private final Color errorFg = Color.decode( "#FF3333" ); + public Color transparent() { + return transparent; + } + public Color frameBg() { return frameBg; } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/CollectionListModel.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/CollectionListModel.java index 5ca2c7e0..127d1b8f 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/CollectionListModel.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/CollectionListModel.java @@ -4,17 +4,20 @@ import com.google.common.collect.ImmutableList; import java.util.*; import javax.annotation.Nullable; import javax.swing.*; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; /** * @author lhunath, 2018-07-19 */ @SuppressWarnings("serial") -public class CollectionListModel extends AbstractListModel implements ComboBoxModel { +public class CollectionListModel extends AbstractListModel implements ComboBoxModel, ListSelectionListener { - private final List model = new LinkedList<>(); + private final List model = new LinkedList<>(); @Nullable - private E selectedItem; + private E selectedItem; + private JList list; public CollectionListModel() { } @@ -77,6 +80,10 @@ public class CollectionListModel extends AbstractListModel implements Comb if (!Objects.equals( selectedItem, newSelectedItem ) && model.contains( newSelectedItem )) { selectedItem = (E) newSelectedItem; fireContentsChanged( this, -1, -1 ); + + //noinspection ObjectEquality + if ((list != null) && (list.getModel() == this)) + list.setSelectedValue( selectedItem, true ); } } @@ -85,4 +92,21 @@ public class CollectionListModel extends AbstractListModel implements Comb public synchronized E getSelectedItem() { return selectedItem; } + + public synchronized void registerList(final JList list) { + // TODO: This class should probably implement ListSelectionModel instead. + if (this.list != null) + this.list.removeListSelectionListener( this ); + + this.list = list; + this.list.addListSelectionListener( this ); + this.list.setModel( this ); + } + + @Override + public synchronized void valueChanged(final ListSelectionEvent event) { + //noinspection ObjectEquality + if ((event.getSource() == list) && (list.getModel() == this)) + selectedItem = list.getSelectedValue(); + } } 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 bd73dec3..f8023f15 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 @@ -32,8 +32,10 @@ import javax.swing.border.CompoundBorder; */ public abstract class Components { - private static final float HEADING_TEXT_SIZE = 19f; - private static final float CONTROL_TEXT_SIZE = 13f; + public static final float TEXT_SIZE_HEADING = 19f; + public static final float TEXT_SIZE_CONTROL = 13f; + public static final int SIZE_MARGIN = 20; + public static final int SIZE_PADDING = 8; public static GradientPanel boxPanel(final int axis, final Component... components) { GradientPanel container = gradientPanel( null, null ); @@ -77,7 +79,7 @@ public abstract class Components { { setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ), BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) ); - setFont( Res.fonts().valueFont().deriveFont( CONTROL_TEXT_SIZE ) ); + setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) ); setAlignmentX( LEFT_ALIGNMENT ); setAlignmentY( BOTTOM_ALIGNMENT ); } @@ -108,7 +110,7 @@ public abstract class Components { public static JList list(final ListModel model, final Function valueTransformer) { return new JList( model ) { { - setFont( Res.fonts().valueFont().deriveFont( CONTROL_TEXT_SIZE ) ); + setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) ); setBorder( BorderFactory.createEmptyBorder( 4, 0, 4, 0 ) ); setCellRenderer( new DefaultListCellRenderer() { { @@ -147,7 +149,7 @@ public abstract class Components { public static JButton button(final String label) { return new JButton( label ) { { - setFont( Res.fonts().controlFont().deriveFont( CONTROL_TEXT_SIZE ) ); + setFont( Res.fonts().controlFont( TEXT_SIZE_CONTROL ) ); setAlignmentX( LEFT_ALIGNMENT ); setAlignmentY( BOTTOM_ALIGNMENT ); } @@ -160,7 +162,7 @@ public abstract class Components { } public static Component strut() { - return strut( 8 ); + return strut( SIZE_PADDING ); } public static Component strut(final int size) { @@ -172,6 +174,18 @@ public abstract class Components { return rigidArea; } + public static int margin() { + return SIZE_MARGIN; + } + + public static Border marginBorder() { + return marginBorder( margin() ); + } + + public static Border marginBorder(final int size) { + return BorderFactory.createEmptyBorder( size, size, size, size ); + } + public static JSpinner spinner(final SpinnerModel model) { return new JSpinner( model ) { { @@ -191,6 +205,14 @@ public abstract class Components { }; } + public static JLabel heading() { + return heading( " " ); + } + + public static JLabel heading(final int horizontalAlignment) { + return heading( " ", horizontalAlignment ); + } + public static JLabel heading(@Nullable final String heading) { return heading( heading, SwingConstants.CENTER ); } @@ -207,7 +229,7 @@ public abstract class Components { public static JLabel heading(@Nullable final String heading, final int horizontalAlignment) { return new JLabel( heading, horizontalAlignment ) { { - setFont( Res.fonts().controlFont().deriveFont( Font.BOLD, HEADING_TEXT_SIZE ) ); + setFont( Res.fonts().controlFont( TEXT_SIZE_HEADING ).deriveFont( Font.BOLD ) ); setAlignmentX( LEFT_ALIGNMENT ); setAlignmentY( BOTTOM_ALIGNMENT ); } @@ -219,6 +241,14 @@ public abstract class Components { }; } + public static JLabel label() { + return label( " " ); + } + + public static JLabel label(final int horizontalAlignment) { + return label( " ", horizontalAlignment ); + } + public static JLabel label(@Nullable final String label) { return label( label, SwingConstants.LEADING ); } @@ -235,7 +265,7 @@ public abstract class Components { public static JLabel label(@Nullable final String label, final int horizontalAlignment) { return new JLabel( label, horizontalAlignment ) { { - setFont( Res.fonts().controlFont().deriveFont( CONTROL_TEXT_SIZE ) ); + setFont( Res.fonts().controlFont( TEXT_SIZE_CONTROL ) ); setAlignmentX( LEFT_ALIGNMENT ); setAlignmentY( BOTTOM_ALIGNMENT ); } @@ -250,7 +280,7 @@ public abstract class Components { public static JCheckBox checkBox(final String label) { return new JCheckBox( label ) { { - setFont( Res.fonts().controlFont().deriveFont( CONTROL_TEXT_SIZE ) ); + setFont( Res.fonts().controlFont( TEXT_SIZE_CONTROL ) ); setBackground( null ); setAlignmentX( LEFT_ALIGNMENT ); setAlignmentY( BOTTOM_ALIGNMENT ); @@ -266,7 +296,7 @@ public abstract class Components { public static JComboBox comboBox(final ComboBoxModel model, final Function valueTransformer) { return new JComboBox( model ) { { - setFont( Res.fonts().valueFont().deriveFont( CONTROL_TEXT_SIZE ) ); + setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) ); setBorder( BorderFactory.createEmptyBorder( 4, 0, 4, 0 ) ); setRenderer( new DefaultListCellRenderer() { { diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/FilesPanel.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/FilesPanel.java index 18b97d1d..4290fbb9 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/FilesPanel.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/FilesPanel.java @@ -28,7 +28,7 @@ public class FilesPanel extends JPanel implements ItemListener { protected FilesPanel() { setOpaque( false ); - setBackground( new Color( 0, 0, 0, 0 ) ); + setBackground( Res.colors().transparent() ); setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) ); // - @@ -46,9 +46,6 @@ public class FilesPanel extends JPanel implements ItemListener { // User Selection add( userField ); userField.addItemListener( this ); - - // - - add( Box.createVerticalGlue() ); } public void reload() { diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/MasterPasswordFrame.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/MasterPasswordFrame.java index d39183d3..8139fa9d 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/MasterPasswordFrame.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/MasterPasswordFrame.java @@ -1,9 +1,12 @@ package com.lyndir.masterpassword.gui.view; +import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.masterpassword.gui.Res; import com.lyndir.masterpassword.gui.util.Components; import com.lyndir.masterpassword.model.MPUser; import java.awt.*; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; import javax.annotation.Nullable; import javax.swing.*; @@ -11,21 +14,24 @@ import javax.swing.*; /** * @author lhunath, 2018-07-14 */ -@SuppressWarnings("MagicNumber") -public class MasterPasswordFrame extends JFrame implements FilesPanel.Listener { +@SuppressWarnings("serial") +public class MasterPasswordFrame extends JFrame implements FilesPanel.Listener, ComponentListener { + + private static final Logger logger = Logger.get( MasterPasswordFrame.class ); @SuppressWarnings("FieldCanBeLocal") private final Components.GradientPanel root; private final FilesPanel filesPanel = new FilesPanel(); private final UserPanel userPanel = new UserPanel(); + @SuppressWarnings("MagicNumber") public MasterPasswordFrame() { super( "Master Password" ); setDefaultCloseOperation( DISPOSE_ON_CLOSE ); setContentPane( root = Components.gradientPanel( Res.colors().frameBg(), new FlowLayout() ) ); root.setLayout( new BoxLayout( root, BoxLayout.PAGE_AXIS ) ); - root.setBorder( BorderFactory.createEmptyBorder( 20, 20, 20, 20 ) ); + root.setBorder( Components.marginBorder() ); root.add( filesPanel ); root.add( new JSeparator( SwingConstants.HORIZONTAL ) ); @@ -35,7 +41,8 @@ public class MasterPasswordFrame extends JFrame implements FilesPanel.Listener { filesPanel.addListener( this ); filesPanel.reload(); - setMinimumSize( new Dimension( 640, 480 ) ); + addComponentListener(this ); + setPreferredSize( new Dimension( 640, 480 ) ); pack(); setLocationByPlatform( true ); @@ -46,4 +53,21 @@ public class MasterPasswordFrame extends JFrame implements FilesPanel.Listener { public void onUserSelected(@Nullable final MPUser selectedUser) { userPanel.setUser( selectedUser ); } + + @Override + public void componentResized(final ComponentEvent e) { + } + + @Override + public void componentMoved(final ComponentEvent e) { + } + + @Override + public void componentShown(final ComponentEvent e) { + userPanel.transferFocus(); + } + + @Override + public void componentHidden(final ComponentEvent e) { + } } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserPanel.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserPanel.java index 2ea6e2a0..86d3f3df 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserPanel.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserPanel.java @@ -15,7 +15,9 @@ import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import java.awt.event.*; import java.util.Objects; +import java.util.Random; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -34,8 +36,9 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen private MPUser user; public UserPanel() { - super( new BorderLayout( 20, 20 ), null ); - setBorder( BorderFactory.createEmptyBorder( 20, 20, 20, 20 ) ); + super( new BorderLayout( Components.margin(), Components.margin() ), null ); + setBorder( Components.marginBorder() ); + setUser( null ); } public void setUser(@Nullable final MPUser user) { @@ -61,6 +64,7 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen } revalidate(); + transferFocus(); } ); } @@ -88,11 +92,16 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen private static final class AuthenticateUserPanel extends JPanel implements ActionListener, DocumentListener { + private static final Random random = new Random(); + @Nonnull private final MPUser user; private final JPasswordField masterPasswordField = Components.passwordField(); - private final JLabel errorLabel = Components.label( null ); + private final JLabel errorLabel = Components.label(); + private final JLabel identiconLabel = Components.label( SwingConstants.CENTER ); + + private Future identiconJob; private AuthenticateUserPanel(@Nonnull final MPUser user) { setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) ); @@ -110,39 +119,72 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen add( errorLabel ); errorLabel.setForeground( Res.colors().errorFg() ); - add( Box.createGlue() ); + add( Components.strut() ); + add( identiconLabel ); + identiconLabel.setFont( Res.fonts().emoticonsFont( Components.TEXT_SIZE_CONTROL ) ); - Res.ui( false, masterPasswordField::requestFocusInWindow ); + add( Box.createGlue() ); } @Override public void actionPerformed(final ActionEvent event) { - try { - user.authenticate( masterPasswordField.getPassword() ); - } - catch (final MPIncorrectMasterPasswordException e) { - logger.wrn( e, "During user authentication for: %s", user ); - errorLabel.setText( e.getLocalizedMessage() ); - } - catch (final MPAlgorithmException e) { - logger.err( e, "During user authentication for: %s", user ); - errorLabel.setText( e.getLocalizedMessage() ); - } + updateIdenticon(); + + char[] masterPassword = masterPasswordField.getPassword(); + Res.job( () -> { + try { + user.authenticate( masterPassword ); + } + catch (final MPIncorrectMasterPasswordException e) { + logger.wrn( e, "During user authentication for: %s", user ); + errorLabel.setText( e.getLocalizedMessage() ); + } + catch (final MPAlgorithmException e) { + logger.err( e, "During user authentication for: %s", user ); + errorLabel.setText( e.getLocalizedMessage() ); + } + } ); } @Override public void insertUpdate(final DocumentEvent event) { - errorLabel.setText( null ); + update(); } @Override public void removeUpdate(final DocumentEvent event) { - errorLabel.setText( null ); + update(); } @Override public void changedUpdate(final DocumentEvent event) { + update(); + } + + private synchronized void update() { errorLabel.setText( null ); + + if (identiconJob != null) + identiconJob.cancel( true ); + + identiconJob = Res.job( this::updateIdenticon, 100 + random.nextInt( 100 ), TimeUnit.MILLISECONDS ); + } + + private void updateIdenticon() { + char[] masterPassword = masterPasswordField.getPassword(); + MPIdenticon identicon = ((masterPassword != null) && (masterPassword.length > 0))? + new MPIdenticon( user.getFullName(), masterPassword ): null; + + Res.ui( () -> { + if (identicon != null) { + identiconLabel.setForeground( + Res.colors().fromIdenticonColor( identicon.getColor(), Res.Colors.BackgroundMode.LIGHT ) ); + identiconLabel.setText( identicon.getText() ); + } else { + identiconLabel.setForeground( null ); + identiconLabel.setText( " " ); + } + } ); } } @@ -150,11 +192,13 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen private static final class AuthenticatedUserPanel extends JPanel implements ActionListener, DocumentListener, ListSelectionListener, KeyListener { + public static final int SIZE_RESULT = 48; + @Nonnull private final MPUser user; - private final JLabel passwordLabel = Components.label( " ", SwingConstants.CENTER ); - private final JLabel passwordField = Components.heading( " ", SwingConstants.CENTER ); - private final JLabel queryLabel = Components.label( " " ); + private final JLabel passwordLabel = Components.label( SwingConstants.CENTER ); + private final JLabel passwordField = Components.heading( SwingConstants.CENTER ); + private final JLabel queryLabel = Components.label(); private final JTextField queryField = Components.textField(); private final CollectionListModel> sitesModel = new CollectionListModel<>(); private final JList> sitesList = Components.list( sitesModel, @@ -172,7 +216,7 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen add( passwordLabel ); add( passwordField ); passwordField.setForeground( Res.colors().highlightFg() ); - passwordField.setFont( Res.fonts().bigValueFont().deriveFont( Font.BOLD, 48 ) ); + passwordField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) ); add( Box.createGlue() ); add( Components.strut() ); @@ -182,12 +226,12 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen queryField.addActionListener( this ); queryField.addKeyListener( this ); queryField.getDocument().addDocumentListener( this ); + queryField.requestFocusInWindow(); add( Components.strut() ); add( Components.scrollPane( sitesList ) ); + sitesModel.registerList( sitesList ); sitesList.addListSelectionListener( this ); add( Box.createGlue() ); - - Res.ui( false, queryField::requestFocusInWindow ); } @Override