From 7455fba55ec6447381f068b02e1bcc0382e108ec Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sat, 28 Jul 2018 17:52:43 -0400 Subject: [PATCH] Adding and deleting users and sites. --- .../masterpassword/gui/util/Components.java | 21 +- .../lyndir/masterpassword/gui/util/Res.java | 4 + .../masterpassword/gui/view/FilesPanel.java | 14 +- .../gui/view/MasterPasswordFrame.java | 60 +++--- .../{UserPanel.java => UserContentPanel.java} | 195 ++++++++++++------ .../main/resources/media/icon_lock-open.png | Bin 0 -> 1715 bytes .../resources/media/icon_lock-open@2x.png | Bin 0 -> 2630 bytes .../src/main/resources/media/icon_lock.png | Bin 0 -> 1692 bytes .../src/main/resources/media/icon_lock@2x.png | Bin 0 -> 2599 bytes .../lyndir/masterpassword/model/MPSite.java | 4 +- .../lyndir/masterpassword/model/MPUser.java | 6 +- .../model/impl/MPBasicQuestion.java | 4 +- .../model/impl/MPBasicSite.java | 28 ++- .../model/impl/MPBasicUser.java | 29 ++- .../masterpassword/model/impl/MPFileSite.java | 6 +- .../masterpassword/model/impl/MPFileUser.java | 15 +- .../model/impl/MPFileUserManager.java | 40 +++- 17 files changed, 285 insertions(+), 141 deletions(-) rename platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/{UserPanel.java => UserContentPanel.java} (75%) create mode 100644 platform-independent/java/gui/src/main/resources/media/icon_lock-open.png create mode 100644 platform-independent/java/gui/src/main/resources/media/icon_lock-open@2x.png create mode 100644 platform-independent/java/gui/src/main/resources/media/icon_lock.png create mode 100644 platform-independent/java/gui/src/main/resources/media/icon_lock@2x.png 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 e5136125..1eb68e5a 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 @@ -83,6 +83,10 @@ public abstract class Components { return box; } + public static GradientPanel panel(@Nullable final LayoutManager layout) { + return panel( layout, null ); + } + public static GradientPanel panel(@Nullable final LayoutManager layout, @Nullable final Color color) { return new GradientPanel( layout, color ); } @@ -223,6 +227,11 @@ public abstract class Components { if (actionListener != null) actionListener.actionPerformed( e ); } + + @Override + public boolean isEnabled() { + return actionListener != null; + } } ); } @@ -233,6 +242,11 @@ public abstract class Components { if (actionListener != null) actionListener.actionPerformed( e ); } + + @Override + public boolean isEnabled() { + return actionListener != null; + } } ); iconButton.setFocusable( false ); @@ -441,6 +455,9 @@ public abstract class Components { public GradientPanel(@Nullable final LayoutManager layout, @Nullable final Color gradientColor) { super( layout ); + if (getLayout() == null) + setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) ); + setGradientColor( gradientColor ); setBackground( null ); setAlignmentX( LEFT_ALIGNMENT ); @@ -459,7 +476,7 @@ public abstract class Components { @Override public void setBackground(@Nullable final Color bg) { super.setBackground( bg ); - updatePaint(); + setOpaque( bg != null ); } @Override @@ -469,8 +486,6 @@ public abstract class Components { } private void updatePaint() { - setOpaque( (getGradientColor() != null) || (getBackground() != null) ); - if (gradientColor == null) { paint = null; return; 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 bd707a9c..cffcc92f 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 @@ -128,6 +128,10 @@ public abstract class Res { return icon( "media/icon_user.png" ); } + public Icon lock() { + return icon( "media/icon_lock.png" ); + } + public Icon settings() { return icon( "media/icon_settings.png" ); } 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 eadbc75c..18937afc 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 @@ -2,10 +2,12 @@ package com.lyndir.masterpassword.gui.view; import static com.lyndir.masterpassword.util.Utilities.*; +import com.google.common.collect.ImmutableSortedSet; 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.util.Collection; @@ -18,7 +20,7 @@ import javax.swing.*; * @author lhunath, 2018-07-14 */ @SuppressWarnings("serial") -public class FilesPanel extends JPanel { +public class FilesPanel extends JPanel implements MPFileUserManager.Listener { private final Collection listeners = new CopyOnWriteArraySet<>(); @@ -48,11 +50,8 @@ public class FilesPanel extends JPanel { // User Selection add( userField ); - } - public void reload() { - // TODO: Should we use a listener here instead? - usersModel.set( MPFileUserManager.get().reload() ); + MPFileUserManager.get().addListener( this ); } public boolean addListener(final Listener listener) { @@ -75,6 +74,11 @@ public class FilesPanel extends JPanel { listener.onUserSelected( user ); } + @Override + public void onFilesUpdated(final ImmutableSortedSet files) { + usersModel.set( files ); + } + public interface Listener { void onUserSelected(@Nullable MPUser user); 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 50da1ea9..2ac3698f 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/MasterPasswordFrame.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/MasterPasswordFrame.java @@ -1,13 +1,12 @@ package com.lyndir.masterpassword.gui.view; import com.lyndir.lhunath.opal.system.logging.Logger; -import com.lyndir.masterpassword.gui.util.Res; import com.lyndir.masterpassword.gui.util.Components; -import com.lyndir.masterpassword.model.MPUser; +import com.lyndir.masterpassword.gui.util.Res; +import com.lyndir.masterpassword.model.impl.MPFileUserManager; import java.awt.*; +import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; -import java.awt.event.ComponentListener; -import javax.annotation.Nullable; import javax.swing.*; import javax.swing.border.BevelBorder; @@ -16,57 +15,48 @@ import javax.swing.border.BevelBorder; * @author lhunath, 2018-07-14 */ @SuppressWarnings("serial") -public class MasterPasswordFrame extends JFrame implements FilesPanel.Listener, ComponentListener { +public class MasterPasswordFrame extends JFrame { private static final Logger logger = Logger.get( MasterPasswordFrame.class ); @SuppressWarnings("FieldCanBeLocal") - private final Components.GradientPanel root; - private final FilesPanel filesPanel = new FilesPanel(); - private final UserPanel userPanel = new UserPanel(); + private final Components.GradientPanel root = Components.borderPanel( Res.colors().frameBg(), BoxLayout.PAGE_AXIS ); + private final FilesPanel filesPanel = new FilesPanel(); + private final JPanel userPanel = Components.panel( new BorderLayout( 0, 0 ) ); + private final UserContentPanel userContent = new UserContentPanel(); @SuppressWarnings("MagicNumber") public MasterPasswordFrame() { super( "Master Password" ); - setDefaultCloseOperation( DISPOSE_ON_CLOSE ); - setContentPane( root = Components.borderPanel( Res.colors().frameBg(), BoxLayout.PAGE_AXIS ) ); + setContentPane( root ); root.add( filesPanel ); root.add( Components.strut() ); - root.add( Components.borderPanel( + root.add( userPanel ); + + userPanel.add( userContent.getUserToolbar(), BorderLayout.LINE_START ); + userPanel.add( userContent.getSiteToolbar(), BorderLayout.LINE_END ); + userPanel.add( Components.borderPanel( BorderFactory.createBevelBorder( BevelBorder.RAISED, Res.colors().controlBorder(), Res.colors().frameBg() ), - Res.colors().controlBg(), BoxLayout.PAGE_AXIS, userPanel ) ); + Res.colors().controlBg(), BoxLayout.PAGE_AXIS, userContent ), BorderLayout.CENTER ); - filesPanel.addListener( this ); - filesPanel.reload(); + filesPanel.addListener( userContent ); - addComponentListener( this ); - setPreferredSize( new Dimension( 640, 480 ) ); + addComponentListener( new ComponentHandler() ); + setPreferredSize( new Dimension( 800, 560 ) ); + setDefaultCloseOperation( DISPOSE_ON_CLOSE ); pack(); setLocationRelativeTo( null ); setLocationByPlatform( true ); } - @Override - public void onUserSelected(@Nullable final MPUser user) { - userPanel.setUser( user ); - } + private class ComponentHandler extends ComponentAdapter { - @Override - public void componentResized(final ComponentEvent e) { - } - - @Override - public void componentMoved(final ComponentEvent e) { - } - - @Override - public void componentShown(final ComponentEvent e) { - userPanel.transferFocus(); - } - - @Override - public void componentHidden(final ComponentEvent e) { + @Override + public void componentShown(final ComponentEvent e) { + MPFileUserManager.get().reload(); + userContent.transferFocus(); + } } } 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/UserContentPanel.java similarity index 75% rename from platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserPanel.java rename to platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserContentPanel.java index 8b0e64b6..8734f5b6 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/UserContentPanel.java @@ -11,8 +11,7 @@ import com.lyndir.masterpassword.*; 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; +import com.lyndir.masterpassword.model.impl.*; import java.awt.*; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; @@ -32,44 +31,38 @@ import javax.swing.event.DocumentListener; * @author lhunath, 2018-07-14 */ @SuppressWarnings("SerializableStoresNonSerializable") -public class UserPanel extends Components.GradientPanel implements MPUser.Listener { +public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPUser.Listener { - private static final Logger logger = Logger.get( UserPanel.class ); + private static final Random random = new Random(); + private static final Logger logger = Logger.get( UserContentPanel.class ); + private static final JButton iconButton = Components.button( Res.icons().user(), null ); + + private final JPanel userToolbar = Components.panel( BoxLayout.PAGE_AXIS ); + private final JPanel siteToolbar = Components.panel( BoxLayout.PAGE_AXIS ); @Nullable - private MPUser user; + private MPUser listeningUser; - public UserPanel() { - super( new BorderLayout( Components.margin(), Components.margin() ), null ); + public UserContentPanel() { + userToolbar.setPreferredSize( iconButton.getPreferredSize() ); + siteToolbar.setPreferredSize( iconButton.getPreferredSize() ); + + setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) ); setBorder( Components.marginBorder() ); setUser( null ); } - public void setUser(@Nullable final MPUser user) { - if ((this.user != null) && !Objects.equals( this.user, user )) - this.user.removeListener( this ); + protected JComponent getUserToolbar() { + return userToolbar; + } - this.user = user; + protected JComponent getSiteToolbar() { + return siteToolbar; + } - if (this.user != null) - this.user.addListener( this ); - - Res.ui( () -> { - removeAll(); - if (this.user == null) - add( new NoUserPanel(), BorderLayout.CENTER ); - - else { - if (!this.user.isMasterKeyAvailable()) - add( new AuthenticateUserPanel( this.user ), BorderLayout.CENTER ); - - else - add( new AuthenticatedUserPanel( this.user ), BorderLayout.CENTER ); - } - - revalidate(); - transferFocus(); - } ); + @Override + public void onUserSelected(@Nullable final MPUser user) { + setUser( user ); } @Override @@ -82,7 +75,40 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen setUser( user ); } - private static final class NoUserPanel extends JPanel { + @Override + public void onUserInvalidated(final MPUser user) { + setUser( user ); + } + + private void setUser(@Nullable final MPUser user) { + Res.ui( () -> { + if (listeningUser != null) + listeningUser.removeListener( this ); + listeningUser = user; + + userToolbar.removeAll(); + siteToolbar.removeAll(); + removeAll(); + + if (user == null) + add( new NoUserPanel() ); + + else { + user.addListener( this ); + + if (!user.isMasterKeyAvailable()) + add( new AuthenticateUserPanel( user ) ); + + else + add( new AuthenticatedUserPanel( user ) ); + } + + revalidate(); + transferFocus(); + } ); + } + + private final class NoUserPanel extends JPanel { private NoUserPanel() { setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) ); @@ -94,13 +120,14 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen } - private static final class AuthenticateUserPanel extends JPanel implements ActionListener, DocumentListener { - - private static final Random random = new Random(); + private final class AuthenticateUserPanel extends JPanel implements ActionListener, DocumentListener { @Nonnull private final MPUser user; + private final JButton addButton = Components.button( Res.icons().add(), event -> addUser() ); + private final JButton deleteButton = Components.button( Res.icons().delete(), event -> deleteUser() ); + private final JPasswordField masterPasswordField = Components.passwordField(); private final JLabel errorLabel = Components.label(); private final JLabel identiconLabel = Components.label( SwingConstants.CENTER ); @@ -112,7 +139,14 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen this.user = user; + userToolbar.add( addButton ); + userToolbar.add( deleteButton ); + add( Components.heading( user.getFullName(), SwingConstants.CENTER ) ); + add( Components.strut() ); + + add( identiconLabel ); + identiconLabel.setFont( Res.fonts().emoticonsFont( Components.TEXT_SIZE_CONTROL ) ); add( Box.createGlue() ); add( Components.label( "Master Password:" ) ); @@ -122,14 +156,31 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen masterPasswordField.getDocument().addDocumentListener( this ); add( errorLabel ); errorLabel.setForeground( Res.colors().errorFg() ); - - add( Components.strut() ); - add( identiconLabel ); - identiconLabel.setFont( Res.fonts().emoticonsFont( Components.TEXT_SIZE_CONTROL ) ); - add( Box.createGlue() ); } + private void addUser() { + Object fullName = JOptionPane.showInputDialog( + this, strf( "Enter your full legal name:" ), "Add User", + JOptionPane.QUESTION_MESSAGE, null, null, "Robert Lee Mitchell" ); + if (fullName == null) + return; + + setUser( MPFileUserManager.get().add( fullName.toString() ) ); + } + + private void deleteUser() { + MPFileUser fileUser = (user instanceof MPFileUser)? (MPFileUser) user: null; + if (fileUser == null) + return; + + if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog( + this, strf( "Delete the user %s?

%s", + fileUser.getFullName(), fileUser.getFile().getName() ), + "Delete User", JOptionPane.YES_NO_OPTION )) + MPFileUserManager.get().delete( fileUser ); + } + @Override public void actionPerformed(final ActionEvent event) { updateIdenticon(); @@ -193,21 +244,25 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen } - private static final class AuthenticatedUserPanel extends JPanel implements KeyListener { + private final class AuthenticatedUserPanel extends JPanel implements KeyListener { public static final int SIZE_RESULT = 48; + private final JButton userButton = Components.button( Res.icons().user(), event -> showUserPreferences() ); + private final JButton logoutButton = Components.button( Res.icons().lock(), event -> logoutUser() ); + private final JButton settingsButton = Components.button( Res.icons().settings(), event -> showSiteSettings() ); + private final JButton questionsButton = Components.button( Res.icons().question(), null ); + private final JButton deleteButton = Components.button( Res.icons().delete(), event -> deleteSite() ); + @Nonnull private final MPUser user; - 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 = + 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( null, this::updateSites ); + private final CollectionListModel> sitesModel = new CollectionListModel>().selection( this::showSiteResult ); - private final JList> sitesList = + private final JList> sitesList = Components.list( sitesModel, this::getSiteDescription ); private Future updateSitesJob; @@ -217,23 +272,20 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen this.user = user; - add( Components.panel( - Components.heading( user.getFullName(), SwingConstants.CENTER ), - Components.panel( - BoxLayout.LINE_AXIS, - Box.createGlue(), - Components.button( Res.icons().user(), event -> showUserPreferences() ) ) ) ); + userToolbar.add( userButton ); + userToolbar.add( logoutButton ); + + siteToolbar.add( settingsButton ); + siteToolbar.add( questionsButton ); + siteToolbar.add( deleteButton ); + settingsButton.setEnabled( false ); + + add( Components.heading( user.getFullName(), SwingConstants.CENTER ) ); add( passwordLabel ); - add( Components.panel( - passwordField, - Components.panel( - BoxLayout.LINE_AXIS, - Box.createGlue(), - passwordButton ) ) ); + add( passwordField ); passwordField.setForeground( Res.colors().highlightFg() ); passwordField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) ); - passwordButton.setVisible( false ); add( Box.createGlue() ); add( Components.strut() ); @@ -269,13 +321,16 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) ); } - public void showSiteSettings() { - ImmutableList.Builder components = ImmutableList.builder(); + public void logoutUser() { + user.invalidate(); + } + public void showSiteSettings() { MPSite site = sitesModel.getSelectedItem(); if (site == null) return; + ImmutableList.Builder components = ImmutableList.builder(); components.add( Components.label( "Algorithm:" ), Components.comboBox( MPAlgorithm.Version.values(), MPAlgorithm.Version::name, site.getAlgorithm().version(), @@ -306,6 +361,14 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) ); } + public void deleteSite() { + MPSite site = sitesModel.getSelectedItem(); + if (site == null) + return; + + user.deleteSite( site ); + } + private String getSiteDescription(@Nonnull final MPSite site) { if (site instanceof MPNewSite) return strf( "%s <Add new site>", queryField.getText() ); @@ -334,7 +397,7 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen MPSite site = sitesModel.getSelectedItem(); if (site instanceof MPNewSite) { if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog( - this, strf( "Remember the site [%s]?", site.getSiteName() ), + this, strf( "Remember the site %s?", site.getSiteName() ), "New Site", JOptionPane.YES_NO_OPTION )) { sitesModel.setSelectedItem( user.addSite( site.getSiteName() ) ); useSite(); @@ -370,7 +433,7 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen Res.ui( () -> { passwordLabel.setText( " " ); passwordField.setText( " " ); - passwordButton.setVisible( false ); + settingsButton.setEnabled( false ); } ); return; } @@ -384,7 +447,7 @@ public class UserPanel extends Components.GradientPanel implements MPUser.Listen Res.ui( () -> { passwordLabel.setText( strf( "Your password for %s:", site.getSiteName() ) ); passwordField.setText( result ); - passwordButton.setVisible( true ); + settingsButton.setEnabled( true ); } ); } catch (final MPKeyUnavailableException | MPAlgorithmException e) { diff --git a/platform-independent/java/gui/src/main/resources/media/icon_lock-open.png b/platform-independent/java/gui/src/main/resources/media/icon_lock-open.png new file mode 100644 index 0000000000000000000000000000000000000000..1722d1e6eb830d1d1b353283325b4c0425d8955d GIT binary patch literal 1715 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=hEVFsEgPM3hAM`dB6B=jtV<?;Zqle1Gx6p~WYGxKbf-tXS8q>!0ns}yePYv5bpoSKp8QB{;0T;&&% zT$P<{nWAKGr(jcI1vDTxwIorYA~z?m*s8)-32d$vkPQ;nS5g2gDap1~as*kZ5aAo3 z;GAESs$i;Tpqp%9W}skZsAp(wVs37(qhMrUXrOOkq;F`XYiMp|Y-D9%pa2C*K--E^ z(yW49+@N*=dA3R!B_#z``ugSN<$C4Ddih1^`i7R4mLM~XjC6r2bc-wVN)jt{^NN*W zCb*;)Cl_TFlw{`TDS%8&Ov*1Uu~h=P6yk;40$*Ra!Fk2dfC2`Yennz|zM-Cher_(v zUtrb6B|)hOXJA!b98y`3svneEoL^d$oC;K~4ATq@JNy=b6armiHZ#*6J?HzcN%!QSvBc-a(sK1I@xu(!>g|^1+vCq~pLewD`uDFF#kpQJ^8eoZ z{`243`|t1V{S%j_q;z@ykuUZK;)NY@Wp5lwu`)9^FISW6zaK1;x-39r4f7R?cH4|A zd-`P>);>O%uwiwCPTCE_sa{Lt4;+#+xgaIQ&=$A;yV<&6ooN|`h8vy~?VN8Z)AyUf z^jsppRK{t=J{gAXR~~;f*%UqZ?rrT$SFWu7{wBu!G1p@y_EPh23mV02UzjR6Y(4*6 zT1(_xN3GuMv(Kd8%sH|lp}#rm(7gNa`HSBQeCy!|knhcF{q&RR>z6x~2l~#3N`+S_ zbzHf9`@2BYmj%~57P4FCqjuk|`SJQ?dQc&}n)V`K9>lC;rXLaC%-X^PSPl`$_`+4t1_ zw&U%YEpv7Lf~aXvkK5NR=UF=C%R&czn+Heb4}4nnP|=|`iXq!*s+UTTrV&%c!OL6D z2XSB6G1n*1W}*p8#lc{e29eu-6U5}x(!+0CTe&f2+1kqcu=5qzNp(*aGn>73{!2aP zD^*n$T&y>4bWIfVTfVvXrC-yvYuCzb_ZBpKJ*$zq@xf=`U3cI8die0+<%Iz+dgsMi zIHV*`-r}Lf#mmPRH*4O!eSPgd{`xWwakmw8pV`-T8q7VLv@s^)1N*kGUk)*bDP=D? zdg8{7>@$TK>T!E&mM<6Q6Nu}bAhvziu6Lgv7OmR6d9%O2zLY~0M~$!k>D620f7bka ze#U+6($?uOWHM^3{?(*qx~eHl%%4%CZN<5^w6s*#-_6bbT>ZB+=fs(O%vBE=^)Hq) z-it|>lrXd95nmiHApSo0aoklU##4%G{#^6?qbTI~Xm8ls?$vBR(*H}>F<-ICe7)f5 Rp=eNDvd$@?2>^3@jX(eZ literal 0 HcmV?d00001 diff --git a/platform-independent/java/gui/src/main/resources/media/icon_lock-open@2x.png b/platform-independent/java/gui/src/main/resources/media/icon_lock-open@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d7dad4cb9317b5177703fc10a5f1efb7b546b334 GIT binary patch literal 2630 zcmbVNdpMM78y}}}jy5DKrlDwKP7ZTuri>XAG4VMRBF4;%G3H=qoThwDVWhQ55*)EPC^HLyS5NYA%|2X>^qgd{jRHP|M=ebdfxkap8I#-zvsT6`@Y_EuWeg& zwM?`i5QwguE7cp6PKu!c1;6F&n5PhkN;uosU+ho!ATaq{1cSv70}u&Z0jP&Sh?^4x z3?>Hg6O5QZ03C1v zF$0#sjpm662_*O@zXWiu*ha!(pCDom2~JiJ!u;u8FbZD?z#I@LTPDg81#?6r(02A% zEY=2wL7`Dd6b6aL*rKrnI}8DZf_-`5Ae)dCPVlDEzHosn5~24f2%Y({uPC$vcH4gB{Q-$aXi69oX$8xZkhg-ig1TPzuax%+1# z1rR(9!BfZvFNG0Jt7-xG28X6D(V$1q}a)tyCgH#m9--=~v3#>rJ^lRyZ zi?7WC@W74{f~|3L{;37nR;$D={$d|~xHz661R$R~z>Uj@0Ly^EN{qP$5J8YBo9gKc zN@Z{;9vC>RzesSdl3>Bzp9)H<8gc5`ouJ=ie zz4P;0l^ac-{hM%cA45=u7rm_aRGP24wow>r^Wxp35+xZUU7$V zl{Z{XWgBfBd_!X-CJwa?4$5=GcKw_*gTt@ z*)TX)+i{jw8`-aWBruLk)ZJuOy4xFi%5z<|rfu#DqhcoWeO4we{~bayIHaH68kQY5 zMtbPHX>_d$Jc>{ZTsd`V_$ON1VLiXX82^;!{t1VAM^0sFuFi1ZZI;7@ls#9meoc_4 z`g3D{oNqm`3sblvK8m4UYi+*64<*gK;SNl1l(U?65V8&92CEt>w4f--?cw;uTT)(7 zZu2scVH7TvGjGv4DPQg(jfYv48~h@cm@mu9zGB5fR_H){`f6FP{Gp-sZ~M#*{Op|Q z<1O@?y8?ra8>^d7PBoSd#_e>0Mn^~6_gf7~Vl*c(O(U*uzgQ`As?vHFSLTh3j%la_ zmS{ftj#ur=k``IV&t`kKzOu|YY88qcO_!@3TApL9@9P)qaJD?m^It@@-C_LENlz2O zvW|NndEU!+rN}GWqKqfBA&|wz4EO1KV7o0S`e`hW?)rdX+J*)4&a1|x?VGrs!W=PT z#J+@yU9VntSJ&%zSyL69Qe>&8rs7iMWRUDnOG9algvo~1*n~L!cA3a}JwKmR6KXf} zcCN`nrz?4GEI{Rbqxu3dx#P|J-r4KKxpNSwE9RzKB@yb+Zutf{|88CU>cBvwh*Sch z*kq*$WtiJGjs6*jclI2*d*SI*B%Lm+R$cudC`$_kDhz(IlD6FFX!m6@2i(QF7J6ze z_SILfWet8Nh-Scrkq_-TRTfo?1x%vnaX4`LT53)@j_+K*pr~^AC;Sz z_x0(imUj8bd%0HvIqfSliM6r-x&%N=1l7X~D}FXX(AZ zFzXfz`kOo?)oQbwXxi+>hqEjDv}ICvcnz4&lR8gSJ2J zP9CmHIaRVFl*_#U-0A<|qZ6;GrJN{?96vi?it z-KvM?a$An)1WPAJnvdLh*`%gznN}}vfzIEE@p1g!oT`&~nwT^`k#NTJO!Tjt)7Qtw z#*%uCa+%Aph(Maxy4&mKimm4}`3@&Mir;9vPjZ85Obcy}rvI9rabK(Rfctsv>Wj>I zJ?OEB@Xcz~Pt0b__k1L-EYdleib;<)Ij%M;uj)9crk)kUQcm;>i)@d8Q*v*X_!*)O z1xf>LsN4{8UDig|z3H_Zx_O2Hm*FR;mEXx|fASA%g#LhrR>mc*$xR}wnOgmz=BxFy efuKQqMtyL*CdHPLV+a0eAZ{+(s1?qk`~LzG|1~-Q literal 0 HcmV?d00001 diff --git a/platform-independent/java/gui/src/main/resources/media/icon_lock.png b/platform-independent/java/gui/src/main/resources/media/icon_lock.png new file mode 100644 index 0000000000000000000000000000000000000000..2d15f8261ceca4fe17c00af31a45776e9883cb62 GIT binary patch literal 1692 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 extends Comparable> { @Nonnull MPUser getUser(); - void addQuestion(Q question); + boolean addQuestion(Q question); - void deleteQuestion(Q question); + boolean deleteQuestion(Q question); @Nonnull Collection getQuestions(); 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 5da8f400..7d352cca 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 @@ -77,6 +77,8 @@ public interface MPUser> extends Comparable> { void authenticate(MPMasterKey masterKey) throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException; + void invalidate(); + boolean isMasterKeyAvailable(); @Nonnull @@ -90,7 +92,7 @@ public interface MPUser> extends Comparable> { @Nonnull S addSite(S site); - void deleteSite(S site); + boolean deleteSite(MPSite site); @Nonnull Collection getSites(); @@ -107,5 +109,7 @@ public interface MPUser> extends Comparable> { void onUserUpdated(MPUser user); void onUserAuthenticated(MPUser user); + + void onUserInvalidated(MPUser user); } } 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 cba4538b..430f6ab7 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 @@ -55,8 +55,10 @@ public abstract class MPBasicQuestion extends Changeable implements MPQuestion { @Override public void setType(final MPResultType type) { - this.type = type; + if (Objects.equals(this.type, type)) + return; + this.type = type; setChanged(); } 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 a9aabfb5..f79d4c6d 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 @@ -73,8 +73,10 @@ public abstract class MPBasicSite, Q extends MPQuestion> ext @Override public void setAlgorithm(final MPAlgorithm algorithm) { - this.algorithm = algorithm; + if (Objects.equals(this.algorithm, algorithm)) + return; + this.algorithm = algorithm; setChanged(); } @@ -86,8 +88,10 @@ public abstract class MPBasicSite, Q extends MPQuestion> ext @Override public void setCounter(final UnsignedInteger counter) { - this.counter = counter; + if (Objects.equals(this.counter, counter)) + return; + this.counter = counter; setChanged(); } @@ -99,8 +103,10 @@ public abstract class MPBasicSite, Q extends MPQuestion> ext @Override public void setResultType(final MPResultType resultType) { - this.resultType = resultType; + if (Objects.equals(this.resultType, resultType)) + return; + this.resultType = resultType; setChanged(); } @@ -112,8 +118,10 @@ public abstract class MPBasicSite, Q extends MPQuestion> ext @Override public void setLoginType(@Nullable final MPResultType loginType) { - this.loginType = ifNotNullElse( loginType, getAlgorithm().mpw_default_login_type() ); + if (Objects.equals(this.loginType, loginType)) + return; + this.loginType = ifNotNullElse( loginType, getAlgorithm().mpw_default_login_type() ); setChanged(); } @@ -152,17 +160,21 @@ public abstract class MPBasicSite, Q extends MPQuestion> ext } @Override - public void addQuestion(final Q question) { - questions.add( question ); + public boolean addQuestion(final Q question) { + if (!questions.add( question )) + return false; setChanged(); + return true; } @Override - public void deleteQuestion(final Q question) { - questions.remove( question ); + public boolean deleteQuestion(final Q question) { + if (!questions.remove( question )) + return false; setChanged(); + return true; } @Nonnull 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 1e153bfa..3e7642a9 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 @@ -25,8 +25,7 @@ import com.google.common.collect.ImmutableSortedSet; import com.lyndir.lhunath.opal.system.CodeUtils; import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.masterpassword.*; -import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException; -import com.lyndir.masterpassword.model.MPUser; +import com.lyndir.masterpassword.model.*; import java.util.*; import java.util.concurrent.CopyOnWriteArraySet; import javax.annotation.Nonnull; @@ -66,8 +65,10 @@ public abstract class MPBasicUser> extends Changeabl @Override public void setAvatar(final int avatar) { - this.avatar = avatar; + if (Objects.equals(this.avatar, avatar)) + return; + this.avatar = avatar; setChanged(); } @@ -85,8 +86,10 @@ public abstract class MPBasicUser> extends Changeabl @Override public void setAlgorithm(final MPAlgorithm algorithm) { - this.algorithm = algorithm; + if (Objects.equals(this.algorithm, algorithm)) + return; + this.algorithm = algorithm; setChanged(); } @@ -136,6 +139,17 @@ public abstract class MPBasicUser> extends Changeabl listener.onUserAuthenticated( this ); } + @Override + public void invalidate() { + if (masterKey == null) + return; + + this.masterKey = null; + + for (final Listener listener : listeners) + listener.onUserInvalidated( this ); + } + @Override public boolean isMasterKeyAvailable() { return masterKey != null; @@ -151,6 +165,7 @@ public abstract class MPBasicUser> extends Changeabl return masterKey; } + @Nonnull @Override public S addSite(final S site) { sites.put( site.getSiteName(), site ); @@ -160,10 +175,12 @@ public abstract class MPBasicUser> extends Changeabl } @Override - public void deleteSite(final S site) { - sites.values().remove( site ); + public boolean deleteSite(final MPSite site) { + if (!sites.values().remove( site )) + return false; setChanged(); + return true; } @Nonnull 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 74ed7bad..974f89fa 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 @@ -21,6 +21,7 @@ package com.lyndir.masterpassword.model.impl; import com.google.common.primitives.UnsignedInteger; import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.model.MPSite; +import java.util.Objects; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.joda.time.Instant; @@ -75,8 +76,10 @@ public class MPFileSite extends MPBasicSite { } public void setUrl(@Nullable final String url) { - this.url = url; + if (Objects.equals( this.url, url)) + return; + this.url = url; setChanged(); } @@ -92,7 +95,6 @@ public class MPFileSite extends MPBasicSite { uses++; lastUsed = new Instant(); getUser().use(); - setChanged(); } 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 f3c8d7f9..a1c4b181 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 java.util.Objects; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.joda.time.Instant; @@ -98,8 +99,10 @@ public class MPFileUser extends MPBasicUser { } public void setFormat(final MPMarshalFormat format) { - this.format = format; + if (Objects.equals(this.format, format)) + return; + this.format = format; setChanged(); } @@ -108,8 +111,10 @@ public class MPFileUser extends MPBasicUser { } public void setContentMode(final MPMarshaller.ContentMode contentMode) { - this.contentMode = contentMode; + if (Objects.equals(this.contentMode, contentMode)) + return; + this.contentMode = contentMode; setChanged(); } @@ -118,8 +123,10 @@ public class MPFileUser extends MPBasicUser { } public void setDefaultType(final MPResultType defaultType) { - this.defaultType = defaultType; + if (Objects.equals(this.defaultType, defaultType)) + return; + this.defaultType = defaultType; setChanged(); } @@ -129,7 +136,6 @@ public class MPFileUser extends MPBasicUser { public void use() { lastUsed = new Instant(); - setChanged(); } @@ -159,7 +165,6 @@ public class MPFileUser extends MPBasicUser { if (keyID == null) { keyID = masterKey.getKeyID( getAlgorithm() ); - setChanged(); } } diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUserManager.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUserManager.java index c84abba2..d90eff13 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUserManager.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUserManager.java @@ -25,8 +25,8 @@ import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.masterpassword.model.MPConstants; import java.io.File; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; +import java.util.*; +import java.util.concurrent.CopyOnWriteArraySet; /** @@ -52,6 +52,7 @@ public class MPFileUserManager { } } + private final Collection listeners = new CopyOnWriteArraySet<>(); private final Map userByName = new HashMap<>(); private final File path; @@ -67,13 +68,13 @@ public class MPFileUserManager { this.path = path; } - public ImmutableSortedSet reload() { + public void 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 getFiles(); + return; } for (final File file : pathFiles) @@ -90,12 +91,14 @@ public class MPFileUserManager { logger.err( e, "Couldn't read user from: %s", file ); } - return getFiles(); + fireUpdated(); } public MPFileUser add(final String fullName) { MPFileUser user = new MPFileUser( fullName ); userByName.put( user.getFullName(), user ); + fireUpdated(); + return user; } @@ -104,8 +107,8 @@ public class MPFileUserManager { File userFile = user.getFile(); if (userFile.exists() && !userFile.delete()) logger.err( "Couldn't delete file: %s", userFile ); - else - userByName.values().remove( user ); + else if (userByName.values().remove( user )) + fireUpdated(); } public File getPath() { @@ -115,4 +118,27 @@ public class MPFileUserManager { public ImmutableSortedSet getFiles() { return ImmutableSortedSet.copyOf( userByName.values() ); } + + public boolean addListener(final Listener listener) { + return listeners.add( listener ); + } + + public boolean removeListener(final Listener listener) { + return listeners.remove( listener ); + } + + private void fireUpdated() { + if (listeners.isEmpty()) + return; + + ImmutableSortedSet files = getFiles(); + + for (final Listener listener : listeners) + listener.onFilesUpdated( files ); + } + + public interface Listener { + + void onFilesUpdated(ImmutableSortedSet files); + } }