2
0

Avatar configuration & move preferences into user panel.

This commit is contained in:
Maarten Billemont 2018-07-26 15:07:37 -04:00
parent 7c83a62f91
commit e639137304
13 changed files with 196 additions and 168 deletions

View File

@ -24,7 +24,7 @@
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="false" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="false" project-jdk-name="10" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@ -0,0 +1,25 @@
package com.lyndir.masterpassword.util;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.annotation.Nullable;
/**
* @author lhunath, 2018-07-25
*/
public final class Utilities {
@Nullable
public static <T, R> R ifNotNull(@Nullable final T value, final Function<T, R> consumer) {
if (value == null)
return null;
return consumer.apply( value );
}
public static <T> void ifNotNullDo(@Nullable final T value, final Consumer<T> consumer) {
if (value != null)
consumer.accept( value );
}
}

View File

@ -0,0 +1,7 @@
/**
* @author lhunath, 2018-07-25
*/
@ParametersAreNonnullByDefault
package com.lyndir.masterpassword.util;
import javax.annotation.ParametersAreNonnullByDefault;

View File

@ -24,6 +24,7 @@ import com.google.common.base.Charsets;
import com.google.common.io.ByteSource;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.TypeUtils;
import com.lyndir.masterpassword.gui.util.Res;
import com.lyndir.masterpassword.gui.view.MasterPasswordFrame;
import java.io.IOException;
import java.io.InputStream;

View File

@ -16,7 +16,7 @@
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
//==============================================================================
package com.lyndir.masterpassword.gui.platform.mac;
package com.lyndir.masterpassword.gui.platform.macos;
import com.apple.eawt.*;
import com.google.common.base.Preconditions;

View File

@ -20,6 +20,6 @@
* @author lhunath, 2018-04-26
*/
@ParametersAreNonnullByDefault
package com.lyndir.masterpassword.gui.platform.mac;
package com.lyndir.masterpassword.gui.platform.macos;
import javax.annotation.ParametersAreNonnullByDefault;

View File

@ -27,12 +27,15 @@ public class CollectionListModel<E> extends AbstractListModel<E> implements Comb
return copy( Arrays.asList( elements ) );
}
public static <E> CollectionListModel<E> copy(final Collection<E> elements) {
public static <E> CollectionListModel<E> copy(final Collection<? extends E> elements) {
CollectionListModel<E> model = new CollectionListModel<>();
model.model.addAll( elements );
model.fireIntervalAdded( model, 0, model.model.size() );
synchronized (model) {
model.model.addAll( elements );
model.selectedItem = model.getElementAt( 0 );
model.fireIntervalAdded( model, 0, model.model.size() );
return model;
return model;
}
}
@Override
@ -41,8 +44,9 @@ public class CollectionListModel<E> extends AbstractListModel<E> implements Comb
}
@Override
@Nullable
public synchronized E getElementAt(final int index) {
return model.get( index );
return (index < model.size())? model.get( index ): null;
}
/**
@ -76,10 +80,8 @@ public class CollectionListModel<E> extends AbstractListModel<E> implements Comb
}
}
if ((selectedItem == null) && !model.isEmpty())
setSelectedItem( model.get( 0 ) );
else if (!model.contains( selectedItem ))
setSelectedItem( null );
if ((selectedItem == null) || !model.contains( selectedItem ))
setSelectedItem( getElementAt( 0 ) );
}
@Override
@ -114,15 +116,19 @@ public class CollectionListModel<E> extends AbstractListModel<E> implements Comb
this.list.setModel( this );
}
public CollectionListModel<E> selection(@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) {
public synchronized CollectionListModel<E> selection(@Nullable final Consumer<E> selectionConsumer) {
this.selectionConsumer = selectionConsumer;
if (selectionConsumer != null)
selectionConsumer.accept( selectedItem );
return this;
}
public synchronized CollectionListModel<E> selection(@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) {
this.selectionConsumer = null;
setSelectedItem( selectedItem );
this.selectionConsumer = selectionConsumer;
if (this.selectionConsumer != null)
this.selectionConsumer.accept( selectedItem );
return this;
return selection( selectionConsumer );
}
@Override

View File

@ -18,8 +18,9 @@
package com.lyndir.masterpassword.gui.util;
import com.lyndir.masterpassword.gui.Res;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Collection;
import java.util.function.Consumer;
import java.util.function.Function;
@ -32,7 +33,7 @@ import javax.swing.border.CompoundBorder;
/**
* @author lhunath, 2014-06-08
*/
@SuppressWarnings("SerializableStoresNonSerializable")
@SuppressWarnings({ "SerializableStoresNonSerializable", "serial" })
public abstract class Components {
public static final float TEXT_SIZE_HEADING = 19f;
@ -45,7 +46,7 @@ public abstract class Components {
}
public static GradientPanel panel(final int axis, @Nullable final Color background, final Component... components) {
GradientPanel container = gradientPanel( background, null );
GradientPanel container = panel( null, background );
container.setLayout( new BoxLayout( container, axis ) );
for (final Component component : components)
container.add( component );
@ -74,14 +75,8 @@ public abstract class Components {
return box;
}
public static GradientPanel gradientPanel(@Nullable final Color color, @Nullable final LayoutManager layout) {
return new GradientPanel( layout, color ) {
{
setOpaque( color != null );
setBackground( color );
setAlignmentX( LEFT_ALIGNMENT );
}
};
public static GradientPanel panel(@Nullable final LayoutManager layout, @Nullable final Color color) {
return new GradientPanel( layout, color );
}
public static JDialog showDialog(@Nullable final Component owner, @Nullable final String title, final JOptionPane pane) {
@ -181,17 +176,40 @@ public abstract class Components {
};
}
public static JButton button(final String label) {
return button( label, null );
public static JButton button(final String label, @Nullable final ActionListener actionListener) {
return button( new AbstractAction( label ) {
@Override
public void actionPerformed(final ActionEvent e) {
if (actionListener != null)
actionListener.actionPerformed( e );
}
} );
}
public static JButton button(final String label, @Nullable final Action action) {
return new JButton( label ) {
public static JButton button(final Icon icon, @Nullable final ActionListener actionListener) {
JButton iconButton = button( new AbstractAction( null, icon ) {
@Override
public void actionPerformed(final ActionEvent e) {
if (actionListener != null)
actionListener.actionPerformed( e );
}
} );
iconButton.setFocusable( false );
return iconButton;
}
public static JButton button(final Action action) {
return new JButton( action ) {
{
setFont( Res.fonts().controlFont( TEXT_SIZE_CONTROL ) );
setAlignmentX( LEFT_ALIGNMENT );
if (action != null)
setAction( action );
if (getText() == null) {
setContentAreaFilled( false );
setBorderPainted( false );
setOpaque( false );
}
}
};
}
@ -323,13 +341,18 @@ public abstract class Components {
return comboBox( new DefaultComboBoxModel<>( values ), valueTransformer );
}
public static <E> JComboBox<E> comboBox(final E[] values, final Function<E, String> valueTransformer, final E selectedItem,
@Nullable final Consumer<E> selectionConsumer) {
public static <E> JComboBox<E> comboBox(final E[] values, final Function<E, String> valueTransformer,
@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) {
return comboBox( CollectionListModel.copy( values ).selection( selectedItem, selectionConsumer ), valueTransformer );
}
public static <E> JComboBox<E> comboBox(final Collection<E> values, final Function<E, String> valueTransformer, final E selectedItem,
public static <E> JComboBox<E> comboBox(final Collection<E> values, final Function<E, String> valueTransformer,
@Nullable final Consumer<E> selectionConsumer) {
return comboBox( CollectionListModel.copy( values ).selection( selectionConsumer ), valueTransformer );
}
public static <E> JComboBox<E> comboBox(final Collection<E> values, final Function<E, String> valueTransformer,
@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) {
return comboBox( CollectionListModel.copy( values ).selection( selectedItem, selectionConsumer ), valueTransformer );
}
@ -378,10 +401,15 @@ public abstract class Components {
this( null, gradientColor );
}
public GradientPanel(@Nullable final LayoutManager layout) {
this( layout, null );
}
public GradientPanel(@Nullable final LayoutManager layout, @Nullable final Color gradientColor) {
super( layout );
this.gradientColor = gradientColor;
setBackground( null );
setAlignmentX( LEFT_ALIGNMENT );
}
@Nullable

View File

@ -16,9 +16,8 @@
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
//==============================================================================
package com.lyndir.masterpassword.gui;
package com.lyndir.masterpassword.gui.util;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.collect.Maps;
@ -26,15 +25,13 @@ import com.google.common.io.Resources;
import com.google.common.util.concurrent.*;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.MPIdenticon;
import com.lyndir.masterpassword.gui.SwingExecutorService;
import java.awt.*;
import java.awt.image.ImageObserver;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.net.URL;
import java.util.Map;
import java.util.concurrent.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import javax.swing.*;
import org.jetbrains.annotations.NonNls;
@ -42,7 +39,7 @@ import org.jetbrains.annotations.NonNls;
/**
* @author lhunath, 2014-06-11
*/
@SuppressWarnings({ "HardcodedFileSeparator", "MethodReturnAlwaysConstant", "SpellCheckingInspection" })
@SuppressWarnings({ "HardcodedFileSeparator", "MethodReturnAlwaysConstant", "SpellCheckingInspection", "serial" })
public abstract class Res {
private static final int AVATAR_COUNT = 19;
@ -51,6 +48,7 @@ public abstract class Res {
private static final Executor immediateUiExecutor = new SwingExecutorService( true );
private static final Executor laterUiExecutor = new SwingExecutorService( false );
private static final Logger logger = Logger.get( Res.class );
private static final Icons icons = new Icons();
private static final Fonts fonts = new Fonts();
private static final Colors colors = new Colors();
@ -93,24 +91,8 @@ public abstract class Res {
return immediate? immediateUiExecutor: laterUiExecutor;
}
public static Icon iconAdd() {
return new RetinaIcon( Resources.getResource( "media/icon_add@2x.png" ) );
}
public static Icon iconDelete() {
return new RetinaIcon( Resources.getResource( "media/icon_delete@2x.png" ) );
}
public static Icon iconQuestion() {
return new RetinaIcon( Resources.getResource( "media/icon_question@2x.png" ) );
}
public static Icon avatar(final int index) {
return new RetinaIcon( Resources.getResource( strf( "media/avatar-%d@2x.png", index % avatars() ) ) );
}
public static int avatars() {
return AVATAR_COUNT;
public static Icons icons() {
return icons;
}
public static Fonts fonts() {
@ -121,64 +103,39 @@ public abstract class Res {
return colors;
}
private static final class RetinaIcon extends ImageIcon {
public static final class Icons {
private static final Pattern scalePattern = Pattern.compile( ".*@(\\d+)x.[^.]+$" );
private static final long serialVersionUID = 1L;
private final float scale;
private RetinaIcon(final URL url) {
super( url );
Matcher scaleMatcher = scalePattern.matcher( url.getPath() );
scale = scaleMatcher.matches()? Float.parseFloat( scaleMatcher.group( 1 ) ): 1;
public Icon add() {
return icon( "media/icon_add.png" );
}
//private static URL retinaURL(final URL url) {
// try {
// final boolean[] isRetina = new boolean[1];
// new apple.awt.CImage.HiDPIScaledImage(1,1, BufferedImage.TYPE_INT_ARGB) {
// @Override
// public void drawIntoImage(BufferedImage image, float v) {
// isRetina[0] = v > 1;
// }
// };
// return isRetina[0];
// } catch (Throwable e) {
// e.printStackTrace();
// return url;
// }
//}
@Override
public int getIconWidth() {
return (int) (super.getIconWidth() / scale);
public Icon delete() {
return icon( "media/icon_delete.png" );
}
@Override
public int getIconHeight() {
return (int) (super.getIconHeight() / scale);
public Icon question() {
return icon( "media/icon_question.png" );
}
@Override
public synchronized void paintIcon(final Component c, final Graphics g, final int x, final int y) {
ImageObserver observer = ifNotNullElse( getImageObserver(), c );
public Icon user() {
return icon( "media/icon_user.png" );
}
Image image = getImage();
int width = image.getWidth( observer );
int height = image.getHeight( observer );
Graphics2D g2d = (Graphics2D) g.create( x, y, width, height );
public Icon avatar(final int index) {
return icon( strf( "media/avatar-%d.png", index % avatars() ) );
}
g2d.scale( 1 / scale, 1 / scale );
g2d.drawImage( image, 0, 0, observer );
g2d.scale( 1, 1 );
g2d.dispose();
public int avatars() {
return AVATAR_COUNT;
}
private static Icon icon(@NonNls final String resourceName) {
return new ImageIcon( Toolkit.getDefaultToolkit().getImage( Res.class.getClassLoader().getResource( resourceName ) ) );
}
}
public static class Fonts {
public static final class Fonts {
public Font emoticonsFont(final float size) {
return emoticonsRegular().deriveFont( size );
@ -266,7 +223,7 @@ public abstract class Res {
}
public static class Colors {
public static final class Colors {
private final Color transparent = new Color( 0, 0, 0, 0 );
private final Color frameBg = Color.decode( "#5A5D6B" );

View File

@ -1,16 +1,13 @@
package com.lyndir.masterpassword.gui.view;
import com.google.common.collect.ImmutableList;
import com.lyndir.masterpassword.MPAlgorithm;
import com.lyndir.masterpassword.MPResultType;
import com.lyndir.masterpassword.gui.Res;
import static com.lyndir.masterpassword.util.Utilities.*;
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.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.Collection;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.annotation.Nullable;
@ -21,15 +18,16 @@ import javax.swing.*;
* @author lhunath, 2018-07-14
*/
@SuppressWarnings("serial")
public class FilesPanel extends JPanel implements ItemListener {
public class FilesPanel extends JPanel {
private final Collection<Listener> listeners = new CopyOnWriteArraySet<>();
private final JLabel avatarLabel = new JLabel();
private final CollectionListModel<MPUser<?>> usersModel = new CollectionListModel<>();
private final JComboBox<MPUser<?>> userField =
Components.comboBox( usersModel, user -> (user != null)? user.getFullName(): null );
private final JButton preferencesButton = Components.button( "..." );
private final JButton avatarButton = Components.button( Res.icons().avatar( 0 ), event -> setAvatar() );
private final CollectionListModel<MPUser<?>> usersModel =
CollectionListModel.<MPUser<?>>copy( MPFileUserManager.get().getFiles() ).selection( this::setUser );
private final JComboBox<? extends MPUser<?>> userField =
Components.comboBox( usersModel, user -> ifNotNull( user, MPUser::getFullName ) );
protected FilesPanel() {
setOpaque( false );
@ -40,66 +38,45 @@ public class FilesPanel extends JPanel implements ItemListener {
add( Box.createVerticalGlue() );
// Avatar
add( avatarLabel );
avatarLabel.setHorizontalAlignment( SwingConstants.CENTER );
avatarLabel.setMaximumSize( new Dimension( Integer.MAX_VALUE, 0 ) );
avatarLabel.setToolTipText( "The avatar for your user. Click to change it." );
add( avatarButton );
avatarButton.setHorizontalAlignment( SwingConstants.CENTER );
avatarButton.setMaximumSize( new Dimension( Integer.MAX_VALUE, 0 ) );
avatarButton.setToolTipText( "The avatar for your user. Click to change it." );
// -
add( Components.strut( Components.margin() ) );
// User Selection
add( Components.panel( BoxLayout.LINE_AXIS, userField, preferencesButton ) );
preferencesButton.setAction( new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
MPUser<?> user = usersModel.getSelectedItem();
if (user == null)
return;
MPFileUser fileUser = (user instanceof MPFileUser)? (MPFileUser) user: null;
ImmutableList.Builder<Component> components = ImmutableList.builder();
if (fileUser != null)
components.add( Components.label( "Default Password Type:" ),
Components.comboBox( MPResultType.values(), MPResultType::getLongName,
fileUser.getDefaultType(), fileUser::setDefaultType ),
Components.strut() );
components.add( Components.label( "Default Algorithm:" ),
Components.comboBox( MPAlgorithm.Version.values(), MPAlgorithm.Version::name,
user.getAlgorithm().version(),
version -> user.setAlgorithm( version.getAlgorithm() ) ) );
Components.showDialog( preferencesButton, user.getFullName(), new JOptionPane( Components.panel(
BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) );
}
} );
userField.addItemListener( this );
add( userField );
}
public void reload() {
MPFileUserManager.get().reload();
usersModel.set( MPFileUserManager.get().getFiles() );
// TODO: Should we use a listener here instead?
usersModel.set( MPFileUserManager.get().reload() );
}
public boolean addListener(final Listener listener) {
return listeners.add( listener );
}
@Override
public void itemStateChanged(final ItemEvent e) {
if (e.getStateChange() != ItemEvent.SELECTED)
private void setAvatar() {
MPUser<?> selectedUser = usersModel.getSelectedItem();
if (selectedUser == null)
return;
MPUser<?> selectedUser = usersModel.getSelectedItem();
avatarLabel.setIcon( Res.avatar( (selectedUser == null)? 0: selectedUser.getAvatar() ) );
selectedUser.setAvatar( (selectedUser.getAvatar() + 1) % Res.icons().avatars() );
avatarButton.setIcon( Res.icons().avatar( selectedUser.getAvatar() ) );
}
public void setUser(@Nullable final MPUser<?> user) {
avatarButton.setIcon( Res.icons().avatar( (user == null)? 0: user.getAvatar() ) );
for (final Listener listener : listeners)
listener.onUserSelected( selectedUser );
listener.onUserSelected( user );
}
public interface Listener {
void onUserSelected(@Nullable MPUser<?> selectedUser);
void onUserSelected(@Nullable MPUser<?> user);
}
}

View File

@ -1,7 +1,7 @@
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.Res;
import com.lyndir.masterpassword.gui.util.Components;
import com.lyndir.masterpassword.model.MPUser;
import java.awt.*;
@ -50,8 +50,8 @@ public class MasterPasswordFrame extends JFrame implements FilesPanel.Listener,
}
@Override
public void onUserSelected(@Nullable final MPUser<?> selectedUser) {
userPanel.setUser( selectedUser );
public void onUserSelected(@Nullable final MPUser<?> user) {
userPanel.setUser( user );
}
@Override

View File

@ -3,10 +3,11 @@ 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.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.Res;
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.model.*;
@ -30,6 +31,7 @@ import javax.swing.event.*;
/**
* @author lhunath, 2018-07-14
*/
@SuppressWarnings("SerializableStoresNonSerializable")
public class UserPanel extends Components.GradientPanel implements MPUser.Listener {
private static final Logger logger = Logger.get( UserPanel.class );
@ -212,8 +214,12 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen
this.user = user;
add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
add( Components.strut() );
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( passwordLabel );
add( passwordField );
@ -237,6 +243,25 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen
add( Box.createGlue() );
}
public void showPreferences() {
ImmutableList.Builder<Component> components = ImmutableList.builder();
MPFileUser fileUser = (user instanceof MPFileUser)? (MPFileUser) user: null;
if (fileUser != null)
components.add( Components.label( "Default Password Type:" ),
Components.comboBox( MPResultType.values(), MPResultType::getLongName,
fileUser.getDefaultType(), fileUser::setDefaultType ),
Components.strut() );
components.add( Components.label( "Default Algorithm:" ),
Components.comboBox( MPAlgorithm.Version.values(), MPAlgorithm.Version::name,
user.getAlgorithm().version(),
version -> user.setAlgorithm( version.getAlgorithm() ) ) );
Components.showDialog( this, user.getFullName(), new JOptionPane( Components.panel(
BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) );
}
@Override
public void actionPerformed(final ActionEvent event) {
MPSite<?> site = sitesList.getSelectedValue();

View File

@ -67,13 +67,13 @@ public class MPFileUserManager {
this.path = path;
}
public void reload() {
public ImmutableSortedSet<MPFileUser> reload() {
userByName.clear();
File[] pathFiles;
if ((!path.exists() && !path.mkdirs()) || ((pathFiles = path.listFiles()) == null)) {
logger.err( "Couldn't create directory for user files: %s", path );
return;
return getFiles();
}
for (final File file : pathFiles)
@ -89,6 +89,8 @@ public class MPFileUserManager {
catch (final IOException | MPMarshalException e) {
logger.err( e, "Couldn't read user from: %s", file );
}
return getFiles();
}
public MPFileUser add(final String fullName) {