From 400ebe59db70ce4900ddb33a107e26e4e32711de Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Thu, 19 Jul 2018 13:56:26 -0400 Subject: [PATCH] Implement sites list and copy result. --- .../gui/util/CollectionListModel.java | 88 +++++++++++++ .../masterpassword/gui/util/Components.java | 80 ++++++++++-- .../masterpassword/gui/view/FilesPanel.java | 75 ++++------- .../gui/view/MasterPasswordFrame.java | 6 +- .../masterpassword/gui/view/UserPanel.java | 118 ++++++++++++++---- .../lyndir/masterpassword/model/MPUser.java | 3 +- .../model/impl/MPBasicUser.java | 3 +- 7 files changed, 280 insertions(+), 93 deletions(-) create mode 100644 platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/CollectionListModel.java 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 new file mode 100644 index 00000000..5ca2c7e0 --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/CollectionListModel.java @@ -0,0 +1,88 @@ +package com.lyndir.masterpassword.gui.util; + +import com.google.common.collect.ImmutableList; +import java.util.*; +import javax.annotation.Nullable; +import javax.swing.*; + + +/** + * @author lhunath, 2018-07-19 + */ +@SuppressWarnings("serial") +public class CollectionListModel extends AbstractListModel implements ComboBoxModel { + + private final List model = new LinkedList<>(); + @Nullable + private E selectedItem; + + public CollectionListModel() { + } + + public CollectionListModel(final Collection model) { + this.model.addAll( model ); + fireIntervalAdded( this, 0, model.size() ); + } + + @Override + public synchronized int getSize() { + return model.size(); + } + + @Override + public synchronized E getElementAt(final int index) { + return model.get( index ); + } + + /** + * Replace this model's contents with the objects from the new model collection. + * + * 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 ); + + ListIterator oldIt = model.listIterator(); + for (int from = 0; oldIt.hasNext(); ++from) { + int to = newModelList.indexOf( oldIt.next() ); + + if (to != from) { + oldIt.remove(); + fireIntervalRemoved( this, from, from ); + --from; + } + } + + Iterator newIt = newModelList.iterator(); + for (int to = 0; newIt.hasNext(); ++to) { + E newSite = newIt.next(); + + if ((to >= model.size()) || !Objects.equals( model.get( to ), newSite )) { + model.add( to, newSite ); + fireIntervalAdded( this, to, to ); + } + } + + if ((selectedItem == null) && !model.isEmpty()) + setSelectedItem( model.get( 0 ) ); + else if (!model.contains( selectedItem )) + setSelectedItem( null ); + } + + @Override + @SuppressWarnings({ "unchecked", "SuspiciousMethodCalls" }) + public synchronized void setSelectedItem(@Nullable final Object newSelectedItem) { + if (!Objects.equals( selectedItem, newSelectedItem ) && model.contains( newSelectedItem )) { + selectedItem = (E) newSelectedItem; + fireContentsChanged( this, -1, -1 ); + } + } + + @Nullable + @Override + public synchronized E getSelectedItem() { + return 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 46dd19e6..bd73dec3 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,6 +20,7 @@ package com.lyndir.masterpassword.gui.util; import com.lyndir.masterpassword.gui.Res; import java.awt.*; +import java.util.function.Function; import javax.annotation.Nullable; import javax.swing.*; import javax.swing.border.Border; @@ -34,9 +35,8 @@ public abstract class Components { private static final float HEADING_TEXT_SIZE = 19f; private static final float CONTROL_TEXT_SIZE = 13f; - public static GradientPanel boxLayout(final int axis, final Component... components) { + public static GradientPanel boxPanel(final int axis, final Component... components) { GradientPanel container = gradientPanel( null, null ); - // container.setBackground( Color.red ); container.setLayout( new BoxLayout( container, axis ) ); for (final Component component : components) container.add( component ); @@ -44,12 +44,13 @@ public abstract class Components { return container; } - public static GradientPanel borderPanel(final JComponent component, @Nullable final Border border) { - return borderPanel( component, border, null ); + public static GradientPanel borderPanel(@Nullable final Border border, final Component... components) { + return borderPanel( border, null, components ); } - public static GradientPanel borderPanel(final JComponent component, @Nullable final Border border, @Nullable final Color background) { - GradientPanel box = boxLayout( BoxLayout.LINE_AXIS, component ); + public static GradientPanel borderPanel(@Nullable final Border border, @Nullable final Color background, + final Component... components) { + GradientPanel box = boxPanel( BoxLayout.LINE_AXIS, components ); if (border != null) box.setBorder( border ); @@ -60,11 +61,11 @@ public abstract class Components { return box; } - public static GradientPanel gradientPanel(@Nullable final LayoutManager layout, @Nullable final Color color) { + public static GradientPanel gradientPanel(@Nullable final Color color, @Nullable final LayoutManager layout) { return new GradientPanel( layout, color ) { { setOpaque( color != null ); - setBackground( null ); + setBackground( color ); setAlignmentX( LEFT_ALIGNMENT ); setAlignmentY( BOTTOM_ALIGNMENT ); } @@ -104,6 +105,45 @@ 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 ) ); + 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( + list, valueTransformer.apply( (E) value ), index, isSelected, cellHasFocus ); + } + } ); + setAlignmentX( LEFT_ALIGNMENT ); + setAlignmentY( BOTTOM_ALIGNMENT ); + } + + @Override + public Dimension getMaximumSize() { + return new Dimension( Integer.MAX_VALUE, Integer.MAX_VALUE ); + } + }; + } + + public static JScrollPane scrollPane(final Component child) { + return new JScrollPane( child ) { + { + setBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ) ); + setAlignmentX( LEFT_ALIGNMENT ); + setAlignmentY( BOTTOM_ALIGNMENT ); + } + }; + } + public static JButton button(final String label) { return new JButton( label ) { { @@ -219,14 +259,28 @@ public abstract class Components { } @SafeVarargs - public static JComboBox comboBox(final V... values) { - return comboBox( new DefaultComboBoxModel<>( values ) ); + public static JComboBox comboBox(final Function valueTransformer, final E... values) { + return comboBox( new DefaultComboBoxModel<>( values ), valueTransformer ); } - public static JComboBox comboBox(final ComboBoxModel model) { - return new JComboBox( model ) { + public static JComboBox comboBox(final ComboBoxModel model, final Function valueTransformer) { + return new JComboBox( model ) { { - setFont( Res.fonts().controlFont().deriveFont( CONTROL_TEXT_SIZE ) ); + setFont( Res.fonts().valueFont().deriveFont( CONTROL_TEXT_SIZE ) ); + 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( + list, valueTransformer.apply( (E) value ), index, isSelected, cellHasFocus ); + } + } ); setAlignmentX( LEFT_ALIGNMENT ); setAlignmentY( BOTTOM_ALIGNMENT ); } 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 612f5160..18b97d1d 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 @@ -1,28 +1,30 @@ package com.lyndir.masterpassword.gui.view; import com.lyndir.masterpassword.gui.Res; +import com.lyndir.masterpassword.gui.util.CollectionListModel; import com.lyndir.masterpassword.gui.util.Components; import com.lyndir.masterpassword.model.MPUser; -import com.lyndir.masterpassword.model.impl.MPFileUser; import com.lyndir.masterpassword.model.impl.MPFileUserManager; import java.awt.*; -import java.awt.event.*; -import java.util.Set; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.util.Collection; import java.util.concurrent.CopyOnWriteArraySet; import javax.annotation.Nullable; import javax.swing.*; -import javax.swing.plaf.metal.MetalComboBoxEditor; /** * @author lhunath, 2018-07-14 */ -public class FilesPanel extends JPanel implements ActionListener { +public class FilesPanel extends JPanel implements ItemListener { - private final Set listeners = new CopyOnWriteArraySet<>(); + private final Collection listeners = new CopyOnWriteArraySet<>(); - private final JLabel avatarLabel = new JLabel(); - private final JComboBox userField = Components.comboBox(); + private final JLabel avatarLabel = new JLabel(); + private final CollectionListModel> usersModel = new CollectionListModel<>(); + private final JComboBox> userField = + Components.comboBox( usersModel, user -> (user != null)? user.getFullName(): null ); protected FilesPanel() { setOpaque( false ); @@ -43,25 +45,7 @@ public class FilesPanel extends JPanel implements ActionListener { // User Selection add( userField ); - userField.addActionListener( this ); - userField.setFont( Res.fonts().valueFont().deriveFont( userField.getFont().getSize2D() ) ); - userField.setRenderer( new DefaultListCellRenderer() { - @Override - @SuppressWarnings("unchecked") - public Component getListCellRendererComponent(final JList list, final Object value, final int index, - final boolean isSelected, final boolean cellHasFocus) { - String userValue = (value == null)? null: ((MPFileUser) value).getFullName(); - return super.getListCellRendererComponent( list, userValue, index, isSelected, cellHasFocus ); - } - } ); - userField.setEditor( new MetalComboBoxEditor() { - @Override - protected JTextField createEditorComponent() { - JTextField editorComponents = Components.textField(); - editorComponents.setForeground( Color.red ); - return editorComponents; - } - } ); + userField.addItemListener( this ); // - add( Box.createVerticalGlue() ); @@ -69,36 +53,25 @@ public class FilesPanel extends JPanel implements ActionListener { public void reload() { MPFileUserManager.get().reload(); - userField.setModel( new DefaultComboBoxModel<>( MPFileUserManager.get().getFiles().toArray( new MPFileUser[0] ) ) ); - updateFile(); - } - - @Override - public void actionPerformed(final ActionEvent e) { - updateFile(); - } - - @Nullable - private MPFileUser getSelectedUser() { - int selectedIndex = userField.getSelectedIndex(); - if (selectedIndex < 0) - return null; - - return userField.getModel().getElementAt( selectedIndex ); - } - - private void updateFile() { - MPFileUser selectedFile = getSelectedUser(); - avatarLabel.setIcon( Res.avatar( (selectedFile == null)? 0: selectedFile.getAvatar() ) ); - - for (final Listener listener : listeners) - listener.onUserSelected( selectedFile ); + usersModel.set( MPFileUserManager.get().getFiles() ); } public boolean addListener(final Listener listener) { return listeners.add( listener ); } + @Override + public void itemStateChanged(final ItemEvent e) { + if (e.getStateChange() != ItemEvent.SELECTED) + return; + + MPUser selectedUser = usersModel.getSelectedItem(); + avatarLabel.setIcon( Res.avatar( (selectedUser == null)? 0: selectedUser.getAvatar() ) ); + + for (final Listener listener : listeners) + listener.onUserSelected( selectedUser ); + } + public interface Listener { void onUserSelected(@Nullable MPUser selectedUser); 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 913d54bd..d39183d3 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 @@ -23,12 +23,14 @@ public class MasterPasswordFrame extends JFrame implements FilesPanel.Listener { super( "Master Password" ); setDefaultCloseOperation( DISPOSE_ON_CLOSE ); - setContentPane( root = Components.gradientPanel( new FlowLayout(), Res.colors().frameBg() ) ); + 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.add( filesPanel ); - root.add( Components.borderPanel( userPanel, BorderFactory.createRaisedBevelBorder(), Res.colors().controlBg() ) ); + root.add( new JSeparator( SwingConstants.HORIZONTAL ) ); + root.add( Components.strut() ); + root.add( Components.borderPanel( BorderFactory.createRaisedBevelBorder(), Res.colors().controlBg(), userPanel ) ); filesPanel.addListener( this ); filesPanel.reload(); 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 5b4e6bb3..2ea6e2a0 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,22 +2,25 @@ 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.primitives.UnsignedInteger; import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.gui.Res; +import com.lyndir.masterpassword.gui.util.CollectionListModel; import com.lyndir.masterpassword.gui.util.Components; -import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException; -import com.lyndir.masterpassword.model.MPUser; +import com.lyndir.masterpassword.model.*; import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; +import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.Transferable; +import java.awt.event.*; import java.util.Objects; +import java.util.concurrent.Future; +import java.util.function.Consumer; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.swing.*; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; +import javax.swing.event.*; /** @@ -128,30 +131,35 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen } @Override - public void insertUpdate(final DocumentEvent e) { + public void insertUpdate(final DocumentEvent event) { errorLabel.setText( null ); } @Override - public void removeUpdate(final DocumentEvent e) { + public void removeUpdate(final DocumentEvent event) { errorLabel.setText( null ); } @Override - public void changedUpdate(final DocumentEvent e) { + public void changedUpdate(final DocumentEvent event) { errorLabel.setText( null ); } } - private static final class AuthenticatedUserPanel extends JPanel implements ActionListener, DocumentListener { + private static final class AuthenticatedUserPanel extends JPanel implements ActionListener, DocumentListener, ListSelectionListener, + KeyListener { @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 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 AuthenticatedUserPanel(@Nonnull final MPUser user) { setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) ); @@ -166,12 +174,17 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen passwordField.setForeground( Res.colors().highlightFg() ); passwordField.setFont( Res.fonts().bigValueFont().deriveFont( Font.BOLD, 48 ) ); add( Box.createGlue() ); + add( Components.strut() ); add( queryLabel ); queryLabel.setText( strf( "%s's password for:", user.getFullName() ) ); add( queryField ); queryField.addActionListener( this ); + queryField.addKeyListener( this ); queryField.getDocument().addDocumentListener( this ); + add( Components.strut() ); + add( Components.scrollPane( sitesList ) ); + sitesList.addListSelectionListener( this ); add( Box.createGlue() ); Res.ui( false, queryField::requestFocusInWindow ); @@ -179,12 +192,59 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen @Override public void actionPerformed(final ActionEvent event) { - String siteName = queryField.getText(); + showSiteResult( sitesList.getSelectedValue(), result -> { + if (result == null) + return; + + Transferable clipboardContents = new StringSelection( result ); + Toolkit.getDefaultToolkit().getSystemClipboard().setContents( clipboardContents, null ); + + Res.ui( () -> { + Window window = SwingUtilities.windowForComponent( this ); + window.dispatchEvent( new WindowEvent( window, WindowEvent.WINDOW_CLOSING ) ); + } ); + } ); + } + + @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, @Nullable final Consumer resultCallback) { + if (site == null) { + if (resultCallback != null) + resultCallback.accept( null ); + Res.ui( () -> { + passwordLabel.setText( " " ); + passwordField.setText( " " ); + } ); + return; + } + + String siteName = site.getName(); Res.job( () -> { try { String result = user.getMasterKey().siteResult( siteName, user.getAlgorithm(), UnsignedInteger.ONE, MPKeyPurpose.Authentication, null, MPResultType.GeneratedLong, null ); + if (resultCallback != null) + resultCallback.accept( result ); Res.ui( () -> { passwordLabel.setText( strf( "Your password for %s:", siteName ) ); @@ -192,27 +252,35 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen } ); } catch (final MPKeyUnavailableException | MPAlgorithmException e) { - logger.err( e, "While resolving password for: %s", siteName ); + logger.err( e, "While resolving password for: %s", site ); } } ); } @Override - public void insertUpdate(final DocumentEvent e) { - // TODO - + public void keyTyped(final KeyEvent event) { } @Override - public void removeUpdate(final DocumentEvent e) { - // TODO - + public void keyPressed(final KeyEvent event) { + if ((event.getKeyCode() == KeyEvent.VK_UP) || (event.getKeyCode() == KeyEvent.VK_DOWN)) + sitesList.dispatchEvent( event ); } @Override - public void changedUpdate(final DocumentEvent e) { - // TODO + public void keyReleased(final KeyEvent event) { + if ((event.getKeyCode() == KeyEvent.VK_UP) || (event.getKeyCode() == KeyEvent.VK_DOWN)) + sitesList.dispatchEvent( event ); + } + private synchronized void updateSites() { + if (updateSitesJob != null) + updateSitesJob.cancel( true ); + + updateSitesJob = Res.job( () -> { + ImmutableCollection> sites = user.findSites( queryField.getText() ); + Res.ui( () -> sitesModel.set( sites ) ); + } ); } } } 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 dbce1fef..0c025269 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 @@ -18,6 +18,7 @@ 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; @@ -94,7 +95,7 @@ public interface MPUser> extends Comparable> { Collection getSites(); @Nonnull - Collection findSites(String query); + ImmutableCollection findSites(String query); boolean addListener(Listener listener); 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 c27ad308..97c14b07 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 @@ -20,6 +20,7 @@ package com.lyndir.masterpassword.model.impl; import static com.lyndir.lhunath.opal.system.util.StringUtils.*; +import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableSortedSet; import com.lyndir.lhunath.opal.system.CodeUtils; import com.lyndir.lhunath.opal.system.logging.Logger; @@ -172,7 +173,7 @@ public abstract class MPBasicUser> extends Changeable i @Nonnull @Override - public Collection findSites(final String query) { + public ImmutableCollection findSites(final String query) { ImmutableSortedSet.Builder results = ImmutableSortedSet.naturalOrder(); for (final S site : getSites()) if (site.getName().startsWith( query ))