diff --git a/platform-independent/java/algorithm/build.gradle b/platform-independent/java/algorithm/build.gradle index 645e689c..901f9d30 100644 --- a/platform-independent/java/algorithm/build.gradle +++ b/platform-independent/java/algorithm/build.gradle @@ -6,7 +6,7 @@ description = 'Master Password Algorithm Implementation' tasks.withType( JavaCompile ) { // Native headers - options.compilerArgs += ["-h", new File( new File( project( ':masterpassword-core' ).projectDir, 'src' ), 'java' ).absolutePath] + options.compilerArgs += ["-h", new File( new File( project.project( ':masterpassword-core' ).projectDir, 'src' ), 'java' ).absolutePath] } configurations { diff --git a/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/util/Utilities.java b/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/util/Utilities.java index e0e83937..1bbe3602 100644 --- a/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/util/Utilities.java +++ b/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/util/Utilities.java @@ -2,6 +2,7 @@ package com.lyndir.masterpassword.util; import java.util.function.Consumer; import java.util.function.Function; +import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -18,6 +19,14 @@ public final class Utilities { return consumer.apply( value ); } + @Nonnull + public static R ifNotNullElse(@Nullable final T value, final Function consumer, @Nonnull final R nullValue) { + if (value == null) + return nullValue; + + return consumer.apply( value ); + } + public static void ifNotNullDo(@Nullable final T value, final Consumer consumer) { if (value != null) consumer.accept( value ); 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 e6a5ea8c..65917500 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 @@ -47,8 +47,8 @@ public class GUI { private final MasterPasswordFrame frame = new MasterPasswordFrame(); public static void main(final String... args) { - Thread.setDefaultUncaughtExceptionHandler( - (t, e) -> logger.bug( e, "Uncaught: %s", e.getLocalizedMessage() ) ); +// Thread.setDefaultUncaughtExceptionHandler( +// (t, e) -> logger.bug( e, "Uncaught: %s", e.getLocalizedMessage() ) ); if (Config.get().checkForUpdates()) checkUpdate(); diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPIncognitoSite.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPIncognitoSite.java index 44efed4e..32d47662 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPIncognitoSite.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPIncognitoSite.java @@ -29,25 +29,15 @@ import javax.annotation.Nullable; /** * @author lhunath, 14-12-16 */ -public class MPIncognitoSite extends MPBasicSite { +public class MPIncognitoSite extends MPBasicSite { - private final MPIncognitoUser user; - - public MPIncognitoSite(final MPIncognitoUser user, final String name) { - this( user, name, null, null, null, null ); + public MPIncognitoSite(final MPIncognitoUser user, final String siteName) { + this( user, siteName, null, null, null, null ); } - public MPIncognitoSite(final MPIncognitoUser user, final String name, + public MPIncognitoSite(final MPIncognitoUser user, final String siteName, @Nullable final MPAlgorithm algorithm, @Nullable final UnsignedInteger counter, @Nullable final MPResultType resultType, @Nullable final MPResultType loginType) { - super( name, (algorithm == null)? user.getAlgorithm(): algorithm, counter, resultType, loginType ); - - this.user = user; - } - - @Nonnull - @Override - public MPIncognitoUser getUser() { - return user; + super( user, siteName, (algorithm == null)? user.getAlgorithm(): algorithm, counter, resultType, loginType ); } } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPIncognitoUser.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPIncognitoUser.java index 80e929e4..20d2e053 100755 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPIncognitoUser.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPIncognitoUser.java @@ -37,4 +37,9 @@ public class MPIncognitoUser extends MPBasicUser { public byte[] getKeyID() { return null; } + + @Override + public MPIncognitoSite addSite(final String siteName) { + return addSite( new MPIncognitoSite( this, siteName ) ); + } } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPNewSite.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPNewSite.java new file mode 100644 index 00000000..8cc864d7 --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPNewSite.java @@ -0,0 +1,15 @@ +package com.lyndir.masterpassword.gui.model; + +import com.lyndir.masterpassword.model.*; +import com.lyndir.masterpassword.model.impl.*; + + +/** + * @author lhunath, 2018-07-27 + */ +public class MPNewSite extends MPBasicSite, MPQuestion> { + + public MPNewSite(final MPUser user, final String siteName) { + super( user, siteName, user.getAlgorithm() ); + } +} 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 9ab70ae4..cc357770 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 @@ -13,7 +13,8 @@ import javax.swing.event.ListSelectionListener; * @author lhunath, 2018-07-19 */ @SuppressWarnings("serial") -public class CollectionListModel extends AbstractListModel implements ComboBoxModel, ListSelectionListener { +public class CollectionListModel extends AbstractListModel + implements ComboBoxModel, ListSelectionListener, Selectable> { private final List model = new LinkedList<>(); @Nullable @@ -55,13 +56,16 @@ public class CollectionListModel extends AbstractListModel implements Comb * This operation will mutate the internal model to reflect the given model. * The given model will remain untouched and independent from this object. */ - @SuppressWarnings("AssignmentToForLoopParameter") - public synchronized void set(final Collection newModel) { - ImmutableList newModelList = ImmutableList.copyOf( newModel ); + @SuppressWarnings({ "unchecked", "SuspiciousToArrayCall" }) + public synchronized void set(final Collection elements) { + set( (E[]) elements.toArray( new Object[0] ) ); + } + @SuppressWarnings("AssignmentToForLoopParameter") + public synchronized void set(final E... elements) { ListIterator oldIt = model.listIterator(); for (int from = 0; oldIt.hasNext(); ++from) { - int to = newModelList.indexOf( oldIt.next() ); + int to = Arrays.binarySearch( elements, oldIt.next() ); if (to != from) { oldIt.remove(); @@ -70,9 +74,8 @@ public class CollectionListModel extends AbstractListModel implements Comb } } - Iterator newIt = newModelList.iterator(); - for (int to = 0; newIt.hasNext(); ++to) { - E newSite = newIt.next(); + for (int to = 0; to < elements.length; ++to) { + E newSite = elements[to]; if ((to >= model.size()) || !Objects.equals( model.get( to ), newSite )) { model.add( to, newSite ); @@ -116,6 +119,7 @@ public class CollectionListModel extends AbstractListModel implements Comb this.list.setModel( this ); } + @Override public synchronized CollectionListModel selection(@Nullable final Consumer selectionConsumer) { this.selectionConsumer = selectionConsumer; if (selectionConsumer != null) @@ -124,6 +128,7 @@ public class CollectionListModel extends AbstractListModel implements Comb return this; } + @Override public synchronized CollectionListModel selection(@Nullable final E selectedItem, @Nullable final Consumer selectionConsumer) { this.selectionConsumer = null; setSelectedItem( selectedItem ); @@ -134,7 +139,11 @@ public class CollectionListModel extends AbstractListModel implements Comb @Override public synchronized void valueChanged(final ListSelectionEvent event) { //noinspection ObjectEquality - if ((event.getSource() == list) && (list.getModel() == this)) + if (!event.getValueIsAdjusting() && (event.getSource() == list) && (list.getModel() == this)) { selectedItem = list.getSelectedValue(); + + if (selectionConsumer != null) + selectionConsumer.accept( selectedItem ); + } } } 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 294337c7..3bd1a4c7 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 @@ -28,6 +28,8 @@ 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; /** @@ -41,6 +43,12 @@ public abstract class Components { public static final int SIZE_MARGIN = 12; public static final int SIZE_PADDING = 8; + public static GradientPanel panel(final Component... components) { + GradientPanel panel = panel( BoxLayout.LINE_AXIS, null, components ); + panel.setLayout( new OverlayLayout( panel ) ); + return panel; + } + public static GradientPanel panel(final int axis, final Component... components) { return panel( axis, null, components ); } @@ -124,6 +132,40 @@ public abstract class Components { }; } + public static JTextField textField(@Nullable final String text, @Nullable final Consumer selection) { + return new JTextField( text ) { + { + setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ), + BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) ); + setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) ); + setAlignmentX( LEFT_ALIGNMENT ); + + if (selection != null) + getDocument().addDocumentListener( new DocumentListener() { + @Override + public void insertUpdate(final DocumentEvent e) { + selection.accept( getText() ); + } + + @Override + public void removeUpdate(final DocumentEvent e) { + selection.accept( getText() ); + } + + @Override + public void changedUpdate(final DocumentEvent e) { + selection.accept( getText() ); + } + } ); + } + + @Override + public Dimension getMaximumSize() { + return new Dimension( Integer.MAX_VALUE, getPreferredSize().height ); + } + }; + } + public static JPasswordField passwordField() { return new JPasswordField() { { @@ -143,18 +185,16 @@ public abstract class Components { return new JList( model ) { { setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) ); - setBorder( BorderFactory.createEmptyBorder( 4, 0, 4, 0 ) ); setCellRenderer( new DefaultListCellRenderer() { - { - setBorder( BorderFactory.createEmptyBorder( 0, 4, 0, 4 ) ); - } - @Override @SuppressWarnings({ "unchecked", "SerializableStoresNonSerializable" }) public Component getListCellRendererComponent(final JList list, final Object value, final int index, final boolean isSelected, final boolean cellHasFocus) { - return super.getListCellRendererComponent( + super.getListCellRendererComponent( list, valueTransformer.apply( (E) value ), index, isSelected, cellHasFocus ); + setBorder( BorderFactory.createEmptyBorder( 2, 4, 2, 4 ) ); + + return this; } } ); setAlignmentX( LEFT_ALIGNMENT ); @@ -248,11 +288,6 @@ public abstract class Components { setAlignmentX( LEFT_ALIGNMENT ); setBorder( null ); } - - @Override - public Dimension getMaximumSize() { - return new Dimension( 20, getPreferredSize().height ); - } }; } @@ -362,16 +397,15 @@ public abstract class Components { setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) ); setBorder( BorderFactory.createEmptyBorder( 4, 0, 4, 0 ) ); setRenderer( new DefaultListCellRenderer() { - { - setBorder( BorderFactory.createEmptyBorder( 0, 4, 0, 4 ) ); - } - @Override @SuppressWarnings({ "unchecked", "SerializableStoresNonSerializable" }) public Component getListCellRendererComponent(final JList list, final Object value, final int index, final boolean isSelected, final boolean cellHasFocus) { - return super.getListCellRendererComponent( + super.getListCellRendererComponent( list, valueTransformer.apply( (E) value ), index, isSelected, cellHasFocus ); + setBorder( BorderFactory.createEmptyBorder( 0, 4, 0, 4 ) ); + + return this; } } ); putClientProperty( "JComboBox.isPopDown", Boolean.TRUE ); 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 dbdaa367..bd707a9c 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 @@ -34,6 +34,9 @@ import java.util.concurrent.*; import javax.annotation.Nullable; import javax.swing.*; import org.jetbrains.annotations.NonNls; +import org.joda.time.ReadableInstant; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; /** @@ -103,6 +106,10 @@ public abstract class Res { return colors; } + public static String format(final ReadableInstant instant) { + return DateTimeFormat.mediumDateTime().print( instant ); + } + public static final class Icons { public Icon add() { @@ -121,6 +128,10 @@ public abstract class Res { return icon( "media/icon_user.png" ); } + public Icon settings() { + return icon( "media/icon_settings.png" ); + } + public Icon avatar(final int index) { return icon( strf( "media/avatar-%d.png", index % avatars() ) ); } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Selectable.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Selectable.java new file mode 100644 index 00000000..f7f1fc40 --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Selectable.java @@ -0,0 +1,15 @@ +package com.lyndir.masterpassword.gui.util; + +import java.util.function.Consumer; +import javax.annotation.Nullable; + + +/** + * @author lhunath, 2018-07-26 + */ +public interface Selectable { + + T selection(@Nullable Consumer selectionConsumer); + + T selection(E selectedItem, @Nullable Consumer selectionConsumer); +} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/UnsignedIntegerModel.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/UnsignedIntegerModel.java index ae2655e4..02fa57fb 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/UnsignedIntegerModel.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/UnsignedIntegerModel.java @@ -19,16 +19,24 @@ package com.lyndir.masterpassword.gui.util; import com.google.common.primitives.UnsignedInteger; +import java.util.function.Consumer; +import javax.annotation.Nullable; import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; /** * @author lhunath, 2016-10-29 */ -public class UnsignedIntegerModel extends SpinnerNumberModel { +public class UnsignedIntegerModel extends SpinnerNumberModel +implements Selectable { private static final long serialVersionUID = 1L; + @Nullable + private ChangeListener changeListener; + public UnsignedIntegerModel() { this( UnsignedInteger.ZERO, UnsignedInteger.ZERO, UnsignedInteger.MAX_VALUE, UnsignedInteger.ONE ); } @@ -55,4 +63,30 @@ public class UnsignedIntegerModel extends SpinnerNumberModel { public UnsignedInteger getNumber() { return (UnsignedInteger) super.getNumber(); } + + @Override + public UnsignedIntegerModel selection(@Nullable final Consumer selectionConsumer) { + if (changeListener != null) { + removeChangeListener( changeListener ); + changeListener = null; + } + + if (selectionConsumer != null) { + addChangeListener( changeListener = e -> selectionConsumer.accept( getNumber() ) ); + selectionConsumer.accept( getNumber() ); + } + + return this; + } + + @Override + public UnsignedIntegerModel selection(final UnsignedInteger selectedItem, @Nullable final Consumer selectionConsumer) { + if (changeListener != null) { + removeChangeListener( changeListener ); + changeListener = null; + } + + setValue( selectedItem ); + return selection( selectionConsumer ); + } } 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 96c0220d..50da1ea9 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 @@ -32,7 +32,6 @@ public class MasterPasswordFrame extends JFrame implements FilesPanel.Listener, setDefaultCloseOperation( DISPOSE_ON_CLOSE ); setContentPane( root = Components.borderPanel( Res.colors().frameBg(), BoxLayout.PAGE_AXIS ) ); root.add( filesPanel ); - root.add( new JSeparator( SwingConstants.HORIZONTAL ) ); root.add( Components.strut() ); root.add( Components.borderPanel( BorderFactory.createBevelBorder( BevelBorder.RAISED, Res.colors().controlBorder(), Res.colors().frameBg() ), 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 6a331d33..8b0e64b6 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 @@ -2,14 +2,14 @@ package com.lyndir.masterpassword.gui.view; import static com.lyndir.lhunath.opal.system.util.StringUtils.*; -import com.google.common.collect.ImmutableCollection; +import com.google.common.base.Joiner; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.primitives.UnsignedInteger; import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.masterpassword.*; -import com.lyndir.masterpassword.gui.util.Res; -import com.lyndir.masterpassword.gui.util.CollectionListModel; -import com.lyndir.masterpassword.gui.util.Components; +import com.lyndir.masterpassword.gui.model.MPNewSite; +import com.lyndir.masterpassword.gui.util.*; import com.lyndir.masterpassword.model.*; import com.lyndir.masterpassword.model.impl.MPFileSite; import com.lyndir.masterpassword.model.impl.MPFileUser; @@ -17,15 +17,15 @@ import java.awt.*; 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.*; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.swing.*; -import javax.swing.event.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; /** @@ -166,7 +166,7 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen } private synchronized void update() { - errorLabel.setText( null ); + errorLabel.setText( " " ); if (identiconJob != null) identiconJob.cancel( true ); @@ -193,38 +193,47 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen } - private static final class AuthenticatedUserPanel extends JPanel implements ActionListener, DocumentListener, ListSelectionListener, - KeyListener { + private static final class AuthenticatedUserPanel extends JPanel implements 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 JTextField queryField = Components.textField(); - private final CollectionListModel> sitesModel = new CollectionListModel<>(); - private final JList> sitesList = Components.list( sitesModel, - value -> (value != null)? value.getName(): null ); - private Future updateSitesJob; + private final JLabel passwordLabel = Components.label( SwingConstants.CENTER ); + private final JLabel passwordField = Components.heading( SwingConstants.CENTER ); + private final JButton passwordButton = + Components.button( Res.icons().settings(), event -> showSiteSettings() ); + private final JLabel queryLabel = Components.label(); + private final JTextField queryField = Components.textField( null, this::updateSites ); + private final CollectionListModel> sitesModel = + new CollectionListModel>().selection( this::showSiteResult ); + private final JList> sitesList = + Components.list( sitesModel, this::getSiteDescription ); + + private Future updateSitesJob; private AuthenticatedUserPanel(@Nonnull final MPUser user) { setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) ); this.user = user; - add( new Components.GradientPanel( new BorderLayout() ) { - { - add( Components.heading( user.getFullName(), SwingConstants.CENTER ), BorderLayout.CENTER ); - add( Components.button( Res.icons().user(), event -> showPreferences() ), BorderLayout.LINE_END ); - } - } ); + add( Components.panel( + Components.heading( user.getFullName(), SwingConstants.CENTER ), + Components.panel( + BoxLayout.LINE_AXIS, + Box.createGlue(), + Components.button( Res.icons().user(), event -> showUserPreferences() ) ) ) ); add( passwordLabel ); - add( passwordField ); + add( Components.panel( + passwordField, + Components.panel( + BoxLayout.LINE_AXIS, + Box.createGlue(), + passwordButton ) ) ); passwordField.setForeground( Res.colors().highlightFg() ); passwordField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) ); + passwordButton.setVisible( false ); add( Box.createGlue() ); add( Components.strut() ); @@ -232,18 +241,16 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen queryLabel.setText( strf( "%s's password for:", user.getFullName() ) ); add( queryField ); queryField.putClientProperty( "JTextField.variant", "search" ); - queryField.addActionListener( this ); + queryField.addActionListener( event -> useSite() ); 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() ); } - public void showPreferences() { + public void showUserPreferences() { ImmutableList.Builder components = ImmutableList.builder(); MPFileUser fileUser = (user instanceof MPFileUser)? (MPFileUser) user: null; @@ -262,9 +269,79 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) ); } - @Override - public void actionPerformed(final ActionEvent event) { - MPSite site = sitesList.getSelectedValue(); + public void showSiteSettings() { + ImmutableList.Builder components = ImmutableList.builder(); + + MPSite site = sitesModel.getSelectedItem(); + if (site == null) + return; + + components.add( Components.label( "Algorithm:" ), + Components.comboBox( MPAlgorithm.Version.values(), MPAlgorithm.Version::name, + site.getAlgorithm().version(), + version -> site.setAlgorithm( version.getAlgorithm() ) ) ); + + components.add( Components.label( "Counter:" ), + Components.spinner( new UnsignedIntegerModel( site.getCounter(), UnsignedInteger.ONE ) + .selection( site::setCounter ) ), + Components.strut() ); + + components.add( Components.label( "Password Type:" ), + Components.comboBox( MPResultType.values(), MPResultType::getLongName, + site.getResultType(), site::setResultType ), + Components.strut() ); + + components.add( Components.label( "Login Type:" ), + Components.comboBox( MPResultType.values(), MPResultType::getLongName, + site.getLoginType(), site::setLoginType ), + Components.strut() ); + + MPFileSite fileSite = (site instanceof MPFileSite)? (MPFileSite) site: null; + if (fileSite != null) + components.add( Components.label( "URL:" ), + Components.textField( fileSite.getUrl(), fileSite::setUrl ), + Components.strut() ); + + Components.showDialog( this, site.getSiteName(), new JOptionPane( Components.panel( + BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) ); + } + + private String getSiteDescription(@Nonnull final MPSite site) { + if (site instanceof MPNewSite) + return strf( "%s <Add new site>", queryField.getText() ); + + ImmutableList.Builder parameters = ImmutableList.builder(); + try { + MPFileSite fileSite = (site instanceof MPFileSite)? (MPFileSite) site: null; + if (fileSite != null) + parameters.add( Res.format( fileSite.getLastUsed() ) ); + parameters.add( site.getAlgorithm().version() ); + parameters.add( strf( "#%d", site.getCounter().longValue() ) ); + parameters.add( strf( "%s", site.getLogin() ) ); + if ((fileSite != null) && (fileSite.getUrl() != null)) + parameters.add( fileSite.getUrl() ); + } + catch (final MPAlgorithmException | MPKeyUnavailableException e) { + logger.err( e, "While generating site description." ); + parameters.add( e.getLocalizedMessage() ); + } + + return strf( "%s (%s)", site.getSiteName(), + Joiner.on( " - " ).skipNulls().join( parameters.build() ) ); + } + + private void useSite() { + MPSite site = sitesModel.getSelectedItem(); + if (site instanceof MPNewSite) { + if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog( + this, strf( "Remember the site [%s]?", site.getSiteName() ), + "New Site", JOptionPane.YES_NO_OPTION )) { + sitesModel.setSelectedItem( user.addSite( site.getSiteName() ) ); + useSite(); + } + return; + } + showSiteResult( site, result -> { if (result == null) return; @@ -282,24 +359,8 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen } ); } - @Override - public void insertUpdate(final DocumentEvent event) { - updateSites(); - } - - @Override - public void removeUpdate(final DocumentEvent event) { - updateSites(); - } - - @Override - public void changedUpdate(final DocumentEvent event) { - updateSites(); - } - - @Override - public void valueChanged(final ListSelectionEvent event) { - showSiteResult( event.getValueIsAdjusting()? null: sitesList.getSelectedValue(), null ); + private void showSiteResult(@Nullable final MPSite site) { + showSiteResult( site, null ); } private void showSiteResult(@Nullable final MPSite site, @Nullable final Consumer resultCallback) { @@ -309,22 +370,21 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen Res.ui( () -> { passwordLabel.setText( " " ); passwordField.setText( " " ); + passwordButton.setVisible( false ); } ); return; } - String siteName = site.getName(); Res.job( () -> { try { - String result = user.getMasterKey().siteResult( - siteName, user.getAlgorithm(), UnsignedInteger.ONE, - MPKeyPurpose.Authentication, null, MPResultType.GeneratedLong, null ); + String result = site.getResult(); if (resultCallback != null) resultCallback.accept( result ); Res.ui( () -> { - passwordLabel.setText( strf( "Your password for %s:", siteName ) ); + passwordLabel.setText( strf( "Your password for %s:", site.getSiteName() ) ); passwordField.setText( result ); + passwordButton.setVisible( true ); } ); } catch (final MPKeyUnavailableException | MPAlgorithmException e) { @@ -349,12 +409,19 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen sitesList.dispatchEvent( event ); } - private synchronized void updateSites() { + private synchronized void updateSites(@Nullable final String query) { if (updateSitesJob != null) updateSitesJob.cancel( true ); updateSitesJob = Res.job( () -> { - ImmutableCollection> sites = user.findSites( queryField.getText() ); + Collection> sites = new LinkedList<>(); + if (!Strings.isNullOrEmpty( query )) { + sites.addAll( new LinkedList<>( user.findSites( query ) ) ); + + if (sites.stream().noneMatch( site -> site.getSiteName().equalsIgnoreCase( query ) )) + sites.add( new MPNewSite( user, query ) ); + } + Res.ui( () -> sitesModel.set( sites ) ); } ); } diff --git a/platform-independent/java/gui/src/main/resources/media/icon_settings.png b/platform-independent/java/gui/src/main/resources/media/icon_settings.png new file mode 100644 index 00000000..c0df9db9 Binary files /dev/null and b/platform-independent/java/gui/src/main/resources/media/icon_settings.png differ diff --git a/platform-independent/java/gui/src/main/resources/media/icon_settings@2x.png b/platform-independent/java/gui/src/main/resources/media/icon_settings@2x.png new file mode 100644 index 00000000..960dc964 Binary files /dev/null and b/platform-independent/java/gui/src/main/resources/media/icon_settings@2x.png differ diff --git a/platform-independent/java/gui/src/main/resources/media/icon_user.png b/platform-independent/java/gui/src/main/resources/media/icon_user.png new file mode 100644 index 00000000..534bb091 Binary files /dev/null and b/platform-independent/java/gui/src/main/resources/media/icon_user.png differ diff --git a/platform-independent/java/gui/src/main/resources/media/icon_user@2x.png b/platform-independent/java/gui/src/main/resources/media/icon_user@2x.png new file mode 100644 index 00000000..602ce16e Binary files /dev/null and b/platform-independent/java/gui/src/main/resources/media/icon_user@2x.png differ diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPSite.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPSite.java index 521821e8..115ad20a 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPSite.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPSite.java @@ -33,7 +33,7 @@ public interface MPSite extends Comparable> { // - Meta @Nonnull - String getName(); + String getSiteName(); // - Algorithm @@ -57,10 +57,34 @@ public interface MPSite extends Comparable> { void setLoginType(@Nullable MPResultType loginType); + default String getResult() + throws MPKeyUnavailableException, MPAlgorithmException { + + return getResult( MPKeyPurpose.Authentication ); + } + + @Nonnull + default String getResult(final MPKeyPurpose keyPurpose) + throws MPKeyUnavailableException, MPAlgorithmException { + return getResult( keyPurpose, null ); + } + + @Nonnull + default String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext) + throws MPKeyUnavailableException, MPAlgorithmException { + return getResult( keyPurpose, keyContext, null ); + } + @Nonnull String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, @Nullable String state) throws MPKeyUnavailableException, MPAlgorithmException; + @Nonnull + default String getLogin() + throws MPKeyUnavailableException, MPAlgorithmException { + return getLogin( null ); + } + @Nonnull String getLogin(@Nullable String state) throws MPKeyUnavailableException, MPAlgorithmException; diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUser.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUser.java index 0c025269..5da8f400 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUser.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUser.java @@ -20,8 +20,6 @@ package com.lyndir.masterpassword.model; import com.google.common.collect.ImmutableCollection; import com.lyndir.masterpassword.*; -import com.lyndir.masterpassword.model.impl.MPBasicSite; -import com.lyndir.masterpassword.model.impl.MPBasicUser; import java.util.Collection; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -87,7 +85,10 @@ public interface MPUser> extends Comparable> { // - Relations - void addSite(S site); + S addSite(String siteName); + + @Nonnull + S addSite(S site); void deleteSite(S site); @@ -95,7 +96,7 @@ public interface MPUser> extends Comparable> { Collection getSites(); @Nonnull - ImmutableCollection findSites(String query); + ImmutableCollection findSites(@Nullable String query); boolean addListener(Listener listener); diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicQuestion.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicQuestion.java index 4285032d..cba4538b 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicQuestion.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicQuestion.java @@ -70,7 +70,7 @@ public abstract class MPBasicQuestion extends Changeable implements MPQuestion { @Nonnull @Override - public abstract MPBasicSite getSite(); + public abstract MPBasicSite getSite(); @Override protected void onChanged() { diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicSite.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicSite.java index e0163fc9..a9aabfb5 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicSite.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicSite.java @@ -23,34 +23,36 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*; import com.google.common.primitives.UnsignedInteger; import com.lyndir.masterpassword.*; -import com.lyndir.masterpassword.model.MPQuestion; -import com.lyndir.masterpassword.model.MPSite; +import com.lyndir.masterpassword.model.*; import java.util.*; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import org.jetbrains.annotations.NotNull; /** * @author lhunath, 14-12-16 */ -public abstract class MPBasicSite extends Changeable implements MPSite { +public abstract class MPBasicSite, Q extends MPQuestion> extends Changeable + implements MPSite { + + private final Collection questions = new LinkedHashSet<>(); + private final U user; + private final String siteName; - private String name; private MPAlgorithm algorithm; private UnsignedInteger counter; private MPResultType resultType; private MPResultType loginType; - private final Collection questions = new LinkedHashSet<>(); - - protected MPBasicSite(final String name, final MPAlgorithm algorithm) { - this( name, algorithm, null, null, null ); + protected MPBasicSite(final U user, final String siteName, final MPAlgorithm algorithm) { + this( user, siteName, algorithm, null, null, null ); } - protected MPBasicSite(final String name, final MPAlgorithm algorithm, @Nullable final UnsignedInteger counter, + protected MPBasicSite(final U user, final String siteName, final MPAlgorithm algorithm, + @Nullable final UnsignedInteger counter, @Nullable final MPResultType resultType, @Nullable final MPResultType loginType) { - this.name = name; + this.user = user; + this.siteName = siteName; this.algorithm = algorithm; this.counter = (counter == null)? algorithm.mpw_default_counter(): counter; this.resultType = (resultType == null)? algorithm.mpw_default_result_type(): resultType; @@ -59,8 +61,8 @@ public abstract class MPBasicSite extends Changeable imple @Nonnull @Override - public String getName() { - return name; + public String getSiteName() { + return siteName; } @Nonnull @@ -128,7 +130,7 @@ public abstract class MPBasicSite extends Changeable imple throws MPKeyUnavailableException, MPAlgorithmException { return getUser().getMasterKey().siteResult( - getName(), getAlgorithm(), ifNotNullElse( counter, getAlgorithm().mpw_default_counter() ), + getSiteName(), getAlgorithm(), ifNotNullElse( counter, getAlgorithm().mpw_default_counter() ), keyPurpose, keyContext, type, state ); } @@ -137,7 +139,7 @@ public abstract class MPBasicSite extends Changeable imple throws MPKeyUnavailableException, MPAlgorithmException { return getUser().getMasterKey().siteState( - getName(), getAlgorithm(), ifNotNullElse( counter, getAlgorithm().mpw_default_counter() ), + getSiteName(), getAlgorithm(), ifNotNullElse( counter, getAlgorithm().mpw_default_counter() ), keyPurpose, keyContext, type, state ); } @@ -171,32 +173,35 @@ public abstract class MPBasicSite extends Changeable imple @Nonnull @Override - public abstract MPBasicUser getUser(); + public U getUser() { + return user; + } @Override protected void onChanged() { super.onChanged(); - getUser().setChanged(); + if (user instanceof Changeable) + ((Changeable) user).setChanged(); } @Override public int hashCode() { - return Objects.hashCode( getName() ); + return Objects.hashCode( getSiteName() ); } @Override public boolean equals(final Object obj) { - return (this == obj) || ((obj instanceof MPSite) && Objects.equals( getName(), ((MPSite) obj).getName() )); + return (this == obj) || ((obj instanceof MPSite) && Objects.equals( getSiteName(), ((MPSite) obj).getSiteName() )); } @Override - public int compareTo(@NotNull final MPSite o) { - return getName().compareTo( o.getName() ); + public int compareTo(@Nonnull final MPSite o) { + return getSiteName().compareTo( o.getSiteName() ); } @Override public String toString() { - return strf( "{%s: %s}", getClass().getSimpleName(), getName() ); + return strf( "{%s: %s}", getClass().getSimpleName(), getSiteName() ); } } diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicUser.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicUser.java index 97c14b07..1e153bfa 100755 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicUser.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicUser.java @@ -36,7 +36,7 @@ import javax.annotation.Nullable; /** * @author lhunath, 2014-06-08 */ -public abstract class MPBasicUser> extends Changeable implements MPUser { +public abstract class MPBasicUser> extends Changeable implements MPUser { protected final Logger logger = Logger.get( getClass() ); private final Set listeners = new CopyOnWriteArraySet<>(); @@ -152,10 +152,11 @@ public abstract class MPBasicUser> extends Changeable i } @Override - public void addSite(final S site) { - sites.put( site.getName(), site ); + public S addSite(final S site) { + sites.put( site.getSiteName(), site ); setChanged(); + return site; } @Override @@ -173,11 +174,12 @@ public abstract class MPBasicUser> extends Changeable i @Nonnull @Override - public ImmutableCollection findSites(final String query) { + public ImmutableCollection findSites(@Nullable final String query) { ImmutableSortedSet.Builder results = ImmutableSortedSet.naturalOrder(); - for (final S site : getSites()) - if (site.getName().startsWith( query )) - results.add( site ); + if (query != null) + for (final S site : getSites()) + if (site.getSiteName().startsWith( query )) + results.add( site ); return results.build(); } @@ -211,7 +213,7 @@ public abstract class MPBasicUser> extends Changeable i } @Override - public int compareTo(final MPUser o) { + public int compareTo(@Nonnull final MPUser o) { return getFullName().compareTo( o.getFullName() ); } diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileSite.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileSite.java index 6c12c0dc..74ed7bad 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileSite.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileSite.java @@ -31,9 +31,7 @@ import org.joda.time.ReadableInstant; * @author lhunath, 14-12-05 */ @SuppressWarnings("ComparableImplementedButEqualsNotOverridden") -public class MPFileSite extends MPBasicSite { - - private final MPFileUser user; +public class MPFileSite extends MPBasicSite { @Nullable private String url; @@ -61,10 +59,9 @@ public class MPFileSite extends MPBasicSite { @Nullable final MPResultType resultType, @Nullable final String resultState, @Nullable final MPResultType loginType, @Nullable final String loginState, @Nullable final String url, final int uses, final ReadableInstant lastUsed) { - super( name, (algorithm == null)? user.getAlgorithm(): algorithm, counter, + super( user, name, (algorithm == null)? user.getAlgorithm(): algorithm, counter, (resultType == null)? user.getDefaultType(): resultType, loginType ); - this.user = user; this.resultState = resultState; this.loginState = loginState; this.url = url; @@ -94,23 +91,21 @@ public class MPFileSite extends MPBasicSite { public void use() { uses++; lastUsed = new Instant(); - user.use(); + getUser().use(); setChanged(); } - public String getResult() - throws MPKeyUnavailableException, MPAlgorithmException { - - return getResult( MPKeyPurpose.Authentication, null ); - } - + @Nonnull + @Override public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext) throws MPKeyUnavailableException, MPAlgorithmException { return getResult( keyPurpose, keyContext, getResultState() ); } + @Nonnull + @Override public String getLogin() throws MPKeyUnavailableException, MPAlgorithmException { @@ -153,14 +148,8 @@ public class MPFileSite extends MPBasicSite { setChanged(); } - @Nonnull @Override - public MPFileUser getUser() { - return user; - } - - @Override - public int compareTo(final MPSite o) { + public int compareTo(@Nonnull final MPSite o) { int comparison = (o instanceof MPFileSite)? ((MPFileSite) o).getLastUsed().compareTo( getLastUsed() ): 0; if (comparison != 0) return comparison; diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUser.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUser.java index fd826e99..f3c8d7f9 100755 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUser.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUser.java @@ -23,6 +23,7 @@ import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException; import com.lyndir.masterpassword.model.MPUser; import java.io.File; import java.io.IOException; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.joda.time.Instant; import org.joda.time.ReadableInstant; @@ -163,6 +164,11 @@ public class MPFileUser extends MPBasicUser { } } + @Override + public MPFileSite addSite(final String siteName) { + return addSite( new MPFileSite( this, siteName ) ); + } + @Override protected void onChanged() { try { @@ -180,7 +186,7 @@ public class MPFileUser extends MPBasicUser { } @Override - public int compareTo(final MPUser o) { + public int compareTo(@Nonnull final MPUser o) { int comparison = (o instanceof MPFileUser)? ((MPFileUser) o).getLastUsed().compareTo( getLastUsed() ): 0; if (comparison != 0) return comparison; diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFlatMarshaller.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFlatMarshaller.java index 3e5265af..4e92aa56 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFlatMarshaller.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFlatMarshaller.java @@ -79,7 +79,7 @@ public class MPFlatMarshaller implements MPMarshaller { site.getAlgorithm().version().toInt(), // algorithm site.getCounter().intValue() ), // counter ifNotNullElse( loginName, "" ), // loginName - site.getName(), // siteName + site.getSiteName(), // siteName ifNotNullElse( password, "" ) // password ) ); } diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONFile.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONFile.java index acbea3e4..29b585f3 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONFile.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONFile.java @@ -90,7 +90,7 @@ public class MPJSONFile extends MPJSONAnyObject { // Clear Text content = modelSite.getResult(); loginContent = modelUser.getMasterKey().siteResult( - modelSite.getName(), modelSite.getAlgorithm(), modelSite.getAlgorithm().mpw_default_counter(), + modelSite.getSiteName(), modelSite.getAlgorithm(), modelSite.getAlgorithm().mpw_default_counter(), MPKeyPurpose.Identification, null, modelSite.getLoginType(), modelSite.getLoginState() ); } else { // Redacted @@ -100,9 +100,9 @@ public class MPJSONFile extends MPJSONAnyObject { loginContent = modelSite.getLoginState(); } - Site site = sites.get( modelSite.getName() ); + Site site = sites.get( modelSite.getSiteName() ); if (site == null) - sites.put( modelSite.getName(), site = new Site() ); + sites.put( modelSite.getSiteName(), site = new Site() ); site.type = modelSite.getResultType(); site.counter = modelSite.getCounter().longValue(); site.algorithm = modelSite.getAlgorithm().version();