diff --git a/platform-independent/java/gui/build.gradle b/platform-independent/java/gui/build.gradle index 6e3569cc..27809ee2 100644 --- a/platform-independent/java/gui/build.gradle +++ b/platform-independent/java/gui/build.gradle @@ -24,6 +24,8 @@ shadowJar { attributes 'Implementation-Version': version } doLast { + println("doLast: ${System.env.KEY_PW_DESKTOP}"); + println("doLast: "+System.getenv( 'KEY_PW_DESKTOP' )); if (System.getenv( 'KEY_PW_DESKTOP' ) != null) ant.signjar( jar: archivePath, alias: 'masterpassword-desktop', 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 17c67416..f4a99bb0 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 @@ -1,6 +1,5 @@ package com.lyndir.masterpassword.gui.util; -import com.google.common.collect.ImmutableList; import java.util.*; import java.util.function.Consumer; import javax.annotation.Nullable; @@ -18,25 +17,21 @@ public class CollectionListModel extends AbstractListModel private final List model = new LinkedList<>(); @Nullable - private E selectedItem; private JList list; @Nullable + private E selectedItem; + @Nullable private Consumer selectionConsumer; @SafeVarargs - public static CollectionListModel copy(final E... elements) { - return copy( Arrays.asList( elements ) ); + public CollectionListModel(final E... elements) { + this( Arrays.asList( elements ) ); } - public static CollectionListModel copy(final Collection elements) { - CollectionListModel model = new CollectionListModel<>(); - synchronized (model) { - model.model.addAll( elements ); - model.selectedItem = model.getElementAt( 0 ); - model.fireIntervalAdded( model, 0, model.model.size() ); - - return model; - } + public CollectionListModel(final Collection elements) { + model.addAll( elements ); + selectedItem = getElementAt( 0 ); + fireIntervalAdded( this, 0, model.size() ); } @Override @@ -44,8 +39,8 @@ public class CollectionListModel extends AbstractListModel return model.size(); } - @Override @Nullable + @Override public synchronized E getElementAt(final int index) { return (index < model.size())? model.get( index ): null; } @@ -109,6 +104,11 @@ public class CollectionListModel extends AbstractListModel return selectedItem; } + public CollectionListModel select(final E selectedItem) { + setSelectedItem( selectedItem ); + return this; + } + public synchronized void registerList(final JList list) { // TODO: This class should probably implement ListSelectionModel instead. if (this.list != null) 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 ba7847e5..662f768f 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 @@ -20,7 +20,6 @@ package com.lyndir.masterpassword.gui.util; import com.google.common.base.Strings; import com.lyndir.lhunath.opal.system.logging.Logger; -import com.lyndir.masterpassword.util.Utilities; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -34,8 +33,8 @@ import javax.annotation.Nullable; import javax.swing.*; import javax.swing.border.Border; import javax.swing.border.CompoundBorder; -import javax.swing.event.*; -import javax.swing.text.DefaultFormatterFactory; +import javax.swing.event.HyperlinkEvent; +import javax.swing.text.*; import org.jetbrains.annotations.NonNls; @@ -114,8 +113,13 @@ public abstract class Components { if (options == null) return (selectedValue instanceof Integer)? (Integer) selectedValue: JOptionPane.CLOSED_OPTION; - int option = Arrays.binarySearch( options, selectedValue ); - return (option < 0)? JOptionPane.CLOSED_OPTION: option; + try { + int option = Arrays.binarySearch( options, selectedValue ); + return (option < 0)? JOptionPane.CLOSED_OPTION: option; + } + catch (final ClassCastException ignored) { + return JOptionPane.CLOSED_OPTION; + } } @Nullable @@ -164,7 +168,11 @@ public abstract class Components { } public static JTextField textField() { - return new JTextField() { + return textField( null ); + } + + public static JTextField textField(@Nullable final Document document) { + return new JTextField( document, null, 0 ) { { setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ), BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) ); @@ -174,43 +182,30 @@ public abstract class Components { @Override public Dimension getMaximumSize() { - return new Dimension( Integer.MAX_VALUE, getPreferredSize().height ); + return new Dimension( Integer.MAX_VALUE, Integer.MAX_VALUE ); } }; } public static JTextField textField(@Nullable final String text, @Nullable final Consumer change) { - return new JTextField( text ) { + return textField( new DocumentModel( new PlainDocument() ).selection( text, change ).getDocument() ); + } + + public static JTextArea textArea() { + return new JTextArea() { { 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 (change != null) { - getDocument().addDocumentListener( new DocumentListener() { - @Override - public void insertUpdate(final DocumentEvent e) { - change.accept( getText() ); - } - - @Override - public void removeUpdate(final DocumentEvent e) { - change.accept( getText() ); - } - - @Override - public void changedUpdate(final DocumentEvent e) { - change.accept( getText() ); - } - } ); - change.accept( getText() ); - } + setLineWrap( true ); + setRows( 3 ); } @Override public Dimension getMaximumSize() { - return new Dimension( Integer.MAX_VALUE, getPreferredSize().height ); + return new Dimension( Integer.MAX_VALUE, Integer.MAX_VALUE ); } }; } @@ -443,17 +438,17 @@ public abstract class Components { public static JComboBox comboBox(final E[] values, final Function valueTransformer, @Nullable final E selectedItem, @Nullable final Consumer selectionConsumer) { - return comboBox( CollectionListModel.copy( values ).selection( selectedItem, selectionConsumer ), valueTransformer ); + return comboBox( new CollectionListModel<>( values ).selection( selectedItem, selectionConsumer ), valueTransformer ); } public static JComboBox comboBox(final Collection values, final Function valueTransformer, @Nullable final Consumer selectionConsumer) { - return comboBox( CollectionListModel.copy( values ).selection( selectionConsumer ), valueTransformer ); + return comboBox( new CollectionListModel<>( values ).selection( selectionConsumer ), valueTransformer ); } public static JComboBox comboBox(final Collection values, final Function valueTransformer, @Nullable final E selectedItem, @Nullable final Consumer selectionConsumer) { - return comboBox( CollectionListModel.copy( values ).selection( selectedItem, selectionConsumer ), valueTransformer ); + return comboBox( new CollectionListModel<>( values ).selection( selectedItem, selectionConsumer ), valueTransformer ); } public static JComboBox comboBox(final ComboBoxModel model, final Function valueTransformer) { diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/ConsumingTrigger.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/ConsumingTrigger.java new file mode 100644 index 00000000..11c1c03f --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/ConsumingTrigger.java @@ -0,0 +1,27 @@ +package com.lyndir.masterpassword.gui.util; + +import java.util.function.Consumer; +import javax.annotation.Nullable; + + +/** + * @author lhunath, 2018-08-23 + */ +public class ConsumingTrigger implements Consumer { + + private final Runnable trigger; + + @Nullable + private T value; + + public ConsumingTrigger(final Runnable trigger) { + this.trigger = trigger; + } + + @Override + public void accept(final T t) { + value = t; + + trigger.run(); + } +} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/DocumentModel.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/DocumentModel.java new file mode 100644 index 00000000..78e99809 --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/DocumentModel.java @@ -0,0 +1,95 @@ +package com.lyndir.masterpassword.gui.util; + +import com.lyndir.lhunath.opal.system.logging.Logger; +import java.util.function.Consumer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; + + +/** + * @author lhunath, 2018-08-24 + */ +public class DocumentModel implements Selectable { + + private static final Logger logger = Logger.get( DocumentModel.class ); + + private final Document document; + + @Nullable + private DocumentListener documentListener; + + public DocumentModel(final Document document) { + this.document = document; + } + + @Nonnull + public Document getDocument() { + return document; + } + + @Nullable + public String getText() { + try { + return (document.getLength() > 0)? document.getText( 0, document.getLength() ): null; + } + catch (final BadLocationException e) { + logger.wrn( "While getting text for model", e ); + return null; + } + } + + public void setText(@Nullable final String text) { + try { + if (document.getLength() > 0) + document.remove( 0, document.getLength() ); + + if (text != null) + document.insertString( 0, text, null ); + } + catch (final BadLocationException e) { + logger.err( "While setting text for model", e ); + } + } + + @Override + public DocumentModel selection(@Nullable final Consumer selectionConsumer) { + if (documentListener != null) + document.removeDocumentListener( documentListener ); + + if (selectionConsumer != null) + document.addDocumentListener( documentListener = new DocumentListener() { + @Override + public void insertUpdate(final DocumentEvent e) { + trigger(); + } + + @Override + public void removeUpdate(final DocumentEvent e) { + trigger(); + } + + @Override + public void changedUpdate(final DocumentEvent e) { + trigger(); + } + + private void trigger() { + selectionConsumer.accept( getText() ); + } + } ); + + return this; + } + + @Override + public DocumentModel selection(@Nullable final String selectedItem, @Nullable final Consumer selectionConsumer) { + selection( selectionConsumer ); + setText( selectedItem ); + + return this; + } +} 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 2a0241b7..80685b58 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 @@ -164,6 +164,10 @@ public abstract class Res { return icon( "media/icon_edit.png" ); } + public Icon key() { + return icon( "media/icon_key.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 index f7f1fc40..00700bd7 100644 --- 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 @@ -11,5 +11,5 @@ public interface Selectable { T selection(@Nullable Consumer selectionConsumer); - T selection(E selectedItem, @Nullable Consumer selectionConsumer); + T selection(@Nullable 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 68e24dc4..761393e5 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 @@ -109,13 +109,14 @@ public class UnsignedIntegerModel extends SpinnerNumberModel implements Selectab } @Override - public UnsignedIntegerModel selection(final UnsignedInteger selectedItem, @Nullable final Consumer selectionConsumer) { + public UnsignedIntegerModel selection(@Nullable final UnsignedInteger selectedItem, + @Nullable final Consumer selectionConsumer) { if (changeListener != null) { removeChangeListener( changeListener ); changeListener = null; } - setValue( selectedItem ); + setValue( (selectedItem != null)? selectedItem: getMinimum() ); return selection( selectionConsumer ); } 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 1825b905..c2ddda77 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 @@ -23,7 +23,7 @@ public class FilesPanel extends JPanel implements MPFileUserManager.Listener, Ma "Click to change the user's avatar." ); private final CollectionListModel> usersModel = - CollectionListModel.>copy( MPFileUserManager.get().getFiles() ).selection( MasterPassword.get()::activateUser ); + new CollectionListModel>( MPFileUserManager.get().getFiles() ).selection( MasterPassword.get()::activateUser ); protected FilesPanel() { setOpaque( 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 1bc494a0..3500d0e3 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 @@ -34,6 +34,7 @@ import javax.annotation.Nullable; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; +import javax.swing.text.PlainDocument; /** @@ -470,8 +471,10 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, "Show site settings." ); private final JButton questionsButton = Components.button( Res.icons().question(), event -> showSiteQuestions(), "Show site recovery questions." ); - private final JButton editButton = Components.button( Res.icons().edit(), event -> showEditSite(), + private final JButton editButton = Components.button( Res.icons().edit(), event -> showSiteValues(), "Set/save personal password/login." ); + private final JButton keyButton = Components.button( Res.icons().key(), event -> showSiteKeys(), + "Cryptographic site keys." ); private final JButton deleteButton = Components.button( Res.icons().delete(), event -> deleteSite(), "Delete the site from the user." ); @@ -502,9 +505,12 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, siteToolbar.add( settingsButton ); siteToolbar.add( questionsButton ); siteToolbar.add( editButton ); + siteToolbar.add( keyButton ); siteToolbar.add( deleteButton ); settingsButton.setEnabled( false ); questionsButton.setEnabled( false ); + editButton.setEnabled( false ); + keyButton.setEnabled( false ); deleteButton.setEnabled( false ); answerLabel = Components.label( "Answer:" ); @@ -665,7 +671,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, } ); } - public void showEditSite() { + public void showSiteValues() { MPSite site = sitesModel.getSelectedItem(); if (site == null) return; @@ -710,6 +716,76 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, } } + public void showSiteKeys() { + MPSite site = sitesModel.getSelectedItem(); + if (site == null) + return; + + JTextArea resultField = Components.textArea(); + resultField.setEnabled( false ); + + CollectionListModel purposeModel = new CollectionListModel<>( MPKeyPurpose.values() ); + DocumentModel contextModel = new DocumentModel( new PlainDocument() ); + UnsignedIntegerModel counterModel = new UnsignedIntegerModel( UnsignedInteger.ONE ); + CollectionListModel typeModel = new CollectionListModel<>( MPResultType.values() ); + DocumentModel stateModel = new DocumentModel( new PlainDocument() ); + + Runnable trigger = () -> Res.job( () -> { + try { + MPKeyPurpose purpose = purposeModel.getSelectedItem(); + MPResultType type = typeModel.getSelectedItem(); + + String result = ((purpose == null) || (type == null))? null: + site.getResult( purpose, contextModel.getText(), counterModel.getNumber(), type, stateModel.getText() ); + + Res.ui( () -> resultField.setText( result ) ); + } + catch (final MPKeyUnavailableException | MPAlgorithmException e) { + logger.err( e, "While computing site edit results." ); + } + } ); + + purposeModel.selection( MPKeyPurpose.Authentication, p -> trigger.run() ); + contextModel.selection( c -> trigger.run() ); + counterModel.selection( c -> trigger.run() ); + typeModel.selection( MPResultType.DeriveKey, t -> { + switch (t) { + case DeriveKey: + stateModel.setText( Integer.toString( site.getAlgorithm().mpw_keySize_min() ) ); + break; + + default: + stateModel.setText( null ); + } + + trigger.run(); + } ); + stateModel.selection( c -> trigger.run() ); + + if (JOptionPane.OK_OPTION == Components.showDialog( this, site.getSiteName(), new JOptionPane( Components.panel( + BoxLayout.PAGE_AXIS, + Components.heading( "Key Calculator" ), + Components.label( "Purpose:" ), + Components.comboBox( purposeModel, MPKeyPurpose::getShortName ), + Components.strut(), + Components.label( "Context:" ), + Components.textField( contextModel.getDocument() ), + Components.label( "Counter:" ), + Components.spinner( counterModel ), + Components.label( "Type:" ), + Components.comboBox( typeModel, this::getTypeDescription ), + Components.label( "State:" ), + Components.scrollPane( Components.textField( stateModel.getDocument() ) ), + Components.strut(), + resultField ) ) { + { + setOptions( new Object[]{ "Copy", "Cancel" } ); + setInitialValue( getOptions()[0] ); + } + } )) + copyResult( resultField.getText() ); + } + public void deleteSite() { MPSite site = sitesModel.getSelectedItem(); if (site == null) @@ -790,6 +866,8 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, passwordField.setText( (result != null)? result: " " ); settingsButton.setEnabled( result != null ); questionsButton.setEnabled( result != null ); + editButton.setEnabled( result != null ); + keyButton.setEnabled( result != null ); deleteButton.setEnabled( result != null ); } ) ) ); } diff --git a/platform-independent/java/gui/src/main/resources/media/icon_key.png b/platform-independent/java/gui/src/main/resources/media/icon_key.png new file mode 100644 index 00000000..25ea04cf Binary files /dev/null and b/platform-independent/java/gui/src/main/resources/media/icon_key.png differ diff --git a/platform-independent/java/gui/src/main/resources/media/icon_key@2x.png b/platform-independent/java/gui/src/main/resources/media/icon_key@2x.png new file mode 100644 index 00000000..af6c44cd Binary files /dev/null and b/platform-independent/java/gui/src/main/resources/media/icon_key@2x.png differ