Compare commits
13 Commits
2.7-java-5
...
2.7-java-8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af768329a3 | ||
|
|
9a04c28054 | ||
|
|
ec9c55ec4d | ||
|
|
d8a735e1b1 | ||
|
|
a1eee88a54 | ||
|
|
ac5286853a | ||
|
|
39f6893742 | ||
|
|
7bf7b8981c | ||
|
|
09abe21fed | ||
|
|
6fae0fe425 | ||
|
|
0558176847 | ||
|
|
c553201cda | ||
|
|
665be9494b |
@@ -2,7 +2,7 @@ allprojects {
|
||||
apply plugin: 'findbugs'
|
||||
|
||||
group = 'com.lyndir.masterpassword'
|
||||
version = '2.7.5'
|
||||
version = '2.7.8'
|
||||
|
||||
tasks.withType( JavaCompile ) {
|
||||
options.encoding = 'UTF-8'
|
||||
|
||||
@@ -64,7 +64,7 @@ _initialize_needs() {
|
||||
if [[ $platform = windows ]]; then
|
||||
needs cmd
|
||||
export VSINSTALLDIR="${VSINSTALLDIR:-$(cd "$(cygpath -F 0x002a)/Microsoft Visual Studio"/*/*/Common7/.. && pwd)}"
|
||||
[[ -e "$VSINSTALLDIR/Common7/Tools/VsMSBuildCmd.bat" ]] || { echo >&2 "Missing: msbuild. Please install 'Build Tools for Visual Studio'."; return 1; }
|
||||
[[ -e "$VSINSTALLDIR/Common7/Tools/VsMSBuildCmd.bat" ]] || { echo >&2 "Missing: msbuild. Please install 'Build Tools for Visual Studio'. See https://visualstudio.microsoft.com/downloads/?q=build+tools"; return 1; }
|
||||
else
|
||||
needs libtool:libtoolize,glibtoolize automake autoconf make
|
||||
fi
|
||||
@@ -202,7 +202,7 @@ _target_build() {
|
||||
|
||||
if [[ $platform = windows ]]; then
|
||||
# I cannot for the life of me figure out how to pass this command directly into cmd.
|
||||
printf '"%%VSINSTALLDIR%%\Common7\Tools\VsMSBuildCmd.bat" && msbuild /t:Rebuild /p:Configuration=ReleaseDLL;Platform=%s;OutDir=%s' "$arch" "$(cygpath -w "${prefix##$PWD/}/$arch/")" > .build.bat
|
||||
printf '"%%VSINSTALLDIR%%\Common7\Tools\VsMSBuildCmd.bat" && msbuild /t:Rebuild /p:Configuration=Release;Platform=%s;OutDir=%s' "$arch" "$(cygpath -w "${prefix##$PWD/}/$arch/")" > .build.bat
|
||||
cmd //c .build.bat
|
||||
rm -f .build.bat
|
||||
else
|
||||
@@ -278,7 +278,10 @@ _finalize_merge() {
|
||||
# By default, this will run `make clean`.
|
||||
finalize_clean() { _finalize_clean "$@"; }
|
||||
_finalize_clean() {
|
||||
[[ ! -e Makefile ]] || make -s clean
|
||||
if [[ $platform = windows ]]; then :
|
||||
else
|
||||
[[ ! -e Makefile ]] || make -s clean
|
||||
fi
|
||||
}
|
||||
|
||||
# build <name> [<platform>]
|
||||
|
||||
BIN
platform-independent/c/core/lib/windows/x86/mpw.dll
Executable file → Normal file
BIN
platform-independent/c/core/lib/windows/x86/mpw.dll
Executable file → Normal file
Binary file not shown.
Binary file not shown.
@@ -27,6 +27,13 @@ public final class Utilities {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static String ifNotNullOrEmptyElse(@Nullable final String value, @Nonnull final String emptyValue) {
|
||||
if ((value == null) || value.isEmpty())
|
||||
return emptyValue;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static <T, R> R ifNotNullElse(@Nullable final T value, final Function<T, R> consumer, @Nonnull final R nullValue) {
|
||||
if (value == null)
|
||||
|
||||
@@ -31,7 +31,7 @@ shadowJar {
|
||||
storepass: System.getenv( 'STORE_PW' ),
|
||||
keypass: System.getenv( 'KEY_PW_DESKTOP' ),
|
||||
preservelastmodified: 'true',
|
||||
destdir: '.' )
|
||||
signedJar: "${rootDir}/../public/site/${project.name}-${project.version}.jar" )
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package com.lyndir.masterpassword.gui.util;
|
||||
|
||||
import static com.google.common.base.Preconditions.*;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import javax.annotation.Nullable;
|
||||
@@ -15,6 +21,8 @@ import javax.swing.event.ListSelectionListener;
|
||||
public class CollectionListModel<E> extends AbstractListModel<E>
|
||||
implements ComboBoxModel<E>, ListSelectionListener, Selectable<E, CollectionListModel<E>> {
|
||||
|
||||
private static final Logger logger = Logger.get( CollectionListModel.class );
|
||||
|
||||
private final List<E> model = new LinkedList<>();
|
||||
@Nullable
|
||||
private JList<E> list;
|
||||
@@ -51,16 +59,11 @@ public class CollectionListModel<E> extends AbstractListModel<E>
|
||||
* This operation will mutate the internal model to reflect the given model.
|
||||
* The given model will remain untouched and independent from this object.
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "SuspiciousToArrayCall" })
|
||||
public synchronized void set(final Collection<? extends E> elements) {
|
||||
set( (E[]) elements.toArray( new Object[0] ) );
|
||||
}
|
||||
|
||||
@SuppressWarnings("AssignmentToForLoopParameter")
|
||||
public synchronized void set(final E... elements) {
|
||||
@SuppressWarnings({ "Guava", "AssignmentToForLoopParameter" })
|
||||
public synchronized void set(final Iterable<? extends E> elements) {
|
||||
ListIterator<E> oldIt = model.listIterator();
|
||||
for (int from = 0; oldIt.hasNext(); ++from) {
|
||||
int to = Arrays.binarySearch( elements, oldIt.next() );
|
||||
int to = Iterables.indexOf( elements, Predicates.equalTo( oldIt.next() ) );
|
||||
|
||||
if (to != from) {
|
||||
oldIt.remove();
|
||||
@@ -69,33 +72,46 @@ public class CollectionListModel<E> extends AbstractListModel<E>
|
||||
}
|
||||
}
|
||||
|
||||
for (int to = 0; to < elements.length; ++to) {
|
||||
E newSite = elements[to];
|
||||
|
||||
int to = 0;
|
||||
for (final E newSite : elements) {
|
||||
if ((to >= model.size()) || !Objects.equals( model.get( to ), newSite )) {
|
||||
model.add( to, newSite );
|
||||
fireIntervalAdded( this, to, to );
|
||||
}
|
||||
|
||||
++to;
|
||||
}
|
||||
|
||||
if ((selectedItem == null) || !model.contains( selectedItem ))
|
||||
setSelectedItem( getElementAt( 0 ) );
|
||||
selectItem( getElementAt( 0 ) );
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public final synchronized void set(final E... elements) {
|
||||
set( ImmutableList.copyOf( elements ) );
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({ "unchecked", "SuspiciousMethodCalls" })
|
||||
public synchronized void setSelectedItem(@Nullable final Object newSelectedItem) {
|
||||
if (!Objects.equals( selectedItem, newSelectedItem )) {
|
||||
selectedItem = (E) newSelectedItem;
|
||||
@Deprecated
|
||||
@SuppressWarnings("unchecked")
|
||||
public synchronized void setSelectedItem(@Nullable final Object/* E */ newSelectedItem) {
|
||||
selectItem( (E) newSelectedItem );
|
||||
}
|
||||
|
||||
fireContentsChanged( this, -1, -1 );
|
||||
//noinspection ObjectEquality
|
||||
if ((list != null) && (list.getModel() == this))
|
||||
list.setSelectedValue( selectedItem, true );
|
||||
public synchronized CollectionListModel<E> selectItem(@Nullable final E newSelectedItem) {
|
||||
if (Objects.equals( selectedItem, newSelectedItem ))
|
||||
return this;
|
||||
|
||||
if (selectionConsumer != null)
|
||||
selectionConsumer.accept( selectedItem );
|
||||
}
|
||||
selectedItem = newSelectedItem;
|
||||
|
||||
fireContentsChanged( this, -1, -1 );
|
||||
//noinspection ObjectEquality
|
||||
if ((list != null) && (list.getModel() == this))
|
||||
list.setSelectedValue( selectedItem, true );
|
||||
|
||||
if (selectionConsumer != null)
|
||||
selectionConsumer.accept( selectedItem );
|
||||
return this;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -104,11 +120,6 @@ public class CollectionListModel<E> extends AbstractListModel<E>
|
||||
return selectedItem;
|
||||
}
|
||||
|
||||
public CollectionListModel<E> select(final E selectedItem) {
|
||||
setSelectedItem( selectedItem );
|
||||
return this;
|
||||
}
|
||||
|
||||
public synchronized void registerList(final JList<E> list) {
|
||||
// TODO: This class should probably implement ListSelectionModel instead.
|
||||
if (this.list != null)
|
||||
@@ -131,7 +142,7 @@ public class CollectionListModel<E> extends AbstractListModel<E>
|
||||
@Override
|
||||
public synchronized CollectionListModel<E> selection(@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) {
|
||||
this.selectionConsumer = null;
|
||||
setSelectedItem( selectedItem );
|
||||
selectItem( selectedItem );
|
||||
|
||||
return selection( selectionConsumer );
|
||||
}
|
||||
@@ -139,7 +150,7 @@ public class CollectionListModel<E> extends AbstractListModel<E>
|
||||
@Override
|
||||
public synchronized void valueChanged(final ListSelectionEvent event) {
|
||||
//noinspection ObjectEquality
|
||||
if (!event.getValueIsAdjusting() && (event.getSource() == list) && (list.getModel() == this)) {
|
||||
if (!event.getValueIsAdjusting() && (event.getSource() == list) && (checkNotNull( list ).getModel() == this)) {
|
||||
selectedItem = list.getSelectedValue();
|
||||
|
||||
if (selectionConsumer != null)
|
||||
|
||||
@@ -243,6 +243,9 @@ public abstract class Components {
|
||||
return this;
|
||||
}
|
||||
} );
|
||||
Dimension cellSize = getCellRenderer().getListCellRendererComponent( this, null, 0, false, false ).getPreferredSize();
|
||||
setFixedCellWidth( cellSize.width );
|
||||
setFixedCellHeight( cellSize.height );
|
||||
|
||||
if (model instanceof CollectionListModel)
|
||||
((CollectionListModel<E>) model).registerList( this );
|
||||
|
||||
@@ -87,8 +87,11 @@ public class DocumentModel implements Selectable<String, DocumentModel> {
|
||||
|
||||
@Override
|
||||
public DocumentModel selection(@Nullable final String selectedItem, @Nullable final Consumer<String> selectionConsumer) {
|
||||
selection( selectionConsumer );
|
||||
setText( selectedItem );
|
||||
selection( selectionConsumer );
|
||||
|
||||
if (selectionConsumer != null)
|
||||
selectionConsumer.accept( selectedItem );
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -73,15 +73,6 @@ public abstract class Res {
|
||||
return job( job, 0, TimeUnit.MILLISECONDS );
|
||||
}
|
||||
|
||||
public static <V> void job(final Callable<V> job, final Consumer<V> callback) {
|
||||
Futures.addCallback( job( job, 0, TimeUnit.MILLISECONDS ), new FailableCallback<V>( logger ) {
|
||||
@Override
|
||||
public void onSuccess(@Nullable final V result) {
|
||||
callback.accept( result );
|
||||
}
|
||||
}, uiExecutor() );
|
||||
}
|
||||
|
||||
public static <V> ListenableFuture<V> job(final Callable<V> job, final long delay, final TimeUnit timeUnit) {
|
||||
return jobExecutor.schedule( job, delay, timeUnit );
|
||||
}
|
||||
@@ -90,6 +81,15 @@ public abstract class Res {
|
||||
ui( true, job );
|
||||
}
|
||||
|
||||
public static <V> void ui(final ListenableFuture<V> future, final Consumer<V> job) {
|
||||
Futures.addCallback( future, new FailableCallback<V>( logger ) {
|
||||
@Override
|
||||
public void onSuccess(@Nullable final V result) {
|
||||
job.accept( result );
|
||||
}
|
||||
}, uiExecutor() );
|
||||
}
|
||||
|
||||
public static void ui(final boolean immediate, final Runnable job) {
|
||||
uiExecutor( immediate ).execute( job );
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ public class FilesPanel extends JPanel implements MPFileUserManager.Listener, Ma
|
||||
|
||||
@Override
|
||||
public void onUserSelected(@Nullable final MPUser<?> user) {
|
||||
usersModel.setSelectedItem( user );
|
||||
usersModel.selectItem( user );
|
||||
avatarButton.setIcon( Res.icons().avatar( (user == null)? 0: user.getAvatar() ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
||||
import com.google.common.base.*;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.primitives.UnsignedInteger;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import com.lyndir.lhunath.opal.system.util.ObjectUtils;
|
||||
import com.lyndir.masterpassword.*;
|
||||
@@ -15,7 +16,6 @@ import com.lyndir.masterpassword.gui.util.*;
|
||||
import com.lyndir.masterpassword.gui.util.Platform;
|
||||
import com.lyndir.masterpassword.model.*;
|
||||
import com.lyndir.masterpassword.model.impl.*;
|
||||
import com.lyndir.masterpassword.util.Utilities;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.awt.datatransfer.Transferable;
|
||||
@@ -28,7 +28,6 @@ import java.util.*;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.swing.*;
|
||||
@@ -461,7 +460,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
}
|
||||
|
||||
|
||||
private final class AuthenticatedUserPanel extends JPanel implements KeyListener, MPUser.Listener {
|
||||
private final class AuthenticatedUserPanel extends JPanel implements KeyListener, MPUser.Listener, KeyEventDispatcher {
|
||||
|
||||
private final JButton userButton = Components.button( Res.icons().user(), event -> showUserPreferences(),
|
||||
"Show user preferences." );
|
||||
@@ -479,16 +478,18 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
"Delete the site from the user." );
|
||||
|
||||
@Nonnull
|
||||
private final MPUser<?> user;
|
||||
private final JLabel passwordLabel;
|
||||
private final JLabel passwordField;
|
||||
private final JLabel answerLabel;
|
||||
private final JLabel answerField;
|
||||
private final JLabel queryLabel;
|
||||
private final JTextField queryField;
|
||||
private final CollectionListModel<MPSite<?>> sitesModel;
|
||||
private final JList<MPSite<?>> sitesList;
|
||||
private final MPUser<?> user;
|
||||
private final JLabel resultLabel;
|
||||
private final JLabel resultField;
|
||||
private final JLabel answerLabel;
|
||||
private final JLabel answerField;
|
||||
private final JLabel queryLabel;
|
||||
private final JTextField queryField;
|
||||
private final CollectionListModel<MPQuery.Result<? extends MPSite<?>>> sitesModel;
|
||||
private final CollectionListModel<MPQuery.Result<? extends MPQuestion>> questionsModel;
|
||||
private final JList<MPQuery.Result<? extends MPSite<?>>> sitesList;
|
||||
|
||||
private boolean showLogin;
|
||||
private Future<?> updateSitesJob;
|
||||
|
||||
private AuthenticatedUserPanel(@Nonnull final MPUser<?> user) {
|
||||
@@ -517,13 +518,14 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
answerField = Components.heading( SwingConstants.CENTER );
|
||||
answerField.setForeground( Res.colors().highlightFg() );
|
||||
answerField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
|
||||
questionsModel = new CollectionListModel<MPQuery.Result<? extends MPQuestion>>().selection( this::showQuestionItem );
|
||||
|
||||
add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
|
||||
|
||||
add( passwordLabel = Components.label( SwingConstants.CENTER ) );
|
||||
add( passwordField = Components.heading( SwingConstants.CENTER ) );
|
||||
passwordField.setForeground( Res.colors().highlightFg() );
|
||||
passwordField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
|
||||
add( resultLabel = Components.label( SwingConstants.CENTER ) );
|
||||
add( resultField = Components.heading( SwingConstants.CENTER ) );
|
||||
resultField.setForeground( Res.colors().highlightFg() );
|
||||
resultField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
|
||||
add( Box.createGlue() );
|
||||
add( Components.strut() );
|
||||
|
||||
@@ -538,8 +540,12 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
add( Components.strut() );
|
||||
|
||||
add( Components.scrollPane( sitesList = Components.list(
|
||||
sitesModel = new CollectionListModel<MPSite<?>>().selection( this::showSiteResult ),
|
||||
sitesModel = new CollectionListModel<MPQuery.Result<? extends MPSite<?>>>().selection( this::showSiteItem ),
|
||||
this::getSiteDescription ) ) );
|
||||
sitesList.registerKeyboardAction( this::useSite, KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, 0 ),
|
||||
JComponent.WHEN_FOCUSED );
|
||||
sitesList.registerKeyboardAction( this::useSite, KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK ),
|
||||
JComponent.WHEN_FOCUSED );
|
||||
add( Components.strut() );
|
||||
|
||||
add( Components.label( strf(
|
||||
@@ -549,10 +555,15 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
KeyEvent.getKeyText( copyLoginKeyStroke.getKeyCode() ) ) ) );
|
||||
|
||||
addHierarchyListener( e -> {
|
||||
if (null != SwingUtilities.windowForComponent( this ))
|
||||
user.addListener( this );
|
||||
else
|
||||
user.removeListener( this );
|
||||
if (HierarchyEvent.DISPLAYABILITY_CHANGED == (e.getChangeFlags() & HierarchyEvent.DISPLAYABILITY_CHANGED)) {
|
||||
if (null != SwingUtilities.windowForComponent( this )) {
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher( this );
|
||||
user.addListener( this );
|
||||
} else {
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager().removeKeyEventDispatcher( this );
|
||||
user.removeListener( this );
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
@@ -580,7 +591,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
}
|
||||
|
||||
public void showSiteSettings() {
|
||||
MPSite<?> site = sitesModel.getSelectedItem();
|
||||
MPSite<?> site = getSite();
|
||||
if (site == null)
|
||||
return;
|
||||
|
||||
@@ -627,22 +638,22 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
}
|
||||
|
||||
public void showSiteQuestions() {
|
||||
MPSite<?> site = sitesModel.getSelectedItem();
|
||||
MPSite<?> site = getSite();
|
||||
if (site == null)
|
||||
return;
|
||||
|
||||
CollectionListModel<MPQuestion> questionsModel = new CollectionListModel<MPQuestion>().selection( this::showQuestionResult );
|
||||
JList<MPQuestion> questionsList = Components.list(
|
||||
questionsModel, question -> Strings.isNullOrEmpty( question.getKeyword() )? "<site>": question.getKeyword() );
|
||||
JTextField queryField = Components.textField( null, query -> Res.job( () -> {
|
||||
Collection<MPQuestion> questions = new LinkedList<>( site.findQuestions( query ) );
|
||||
if (questions.stream().noneMatch( question -> question.getKeyword().equalsIgnoreCase( query ) ))
|
||||
questions.add( new MPNewQuestion( site, Utilities.ifNotNullElse( query, "" ) ) );
|
||||
JList<MPQuery.Result<? extends MPQuestion>> questionsList =
|
||||
Components.list( questionsModel, this::getQuestionDescription );
|
||||
JTextField queryField = Components.textField( null, queryText -> Res.job( () -> {
|
||||
MPQuery query = new MPQuery( queryText );
|
||||
Collection<MPQuery.Result<? extends MPQuestion>> questionItems = new LinkedList<>( site.findQuestions( query ) );
|
||||
if (questionItems.stream().noneMatch( MPQuery.Result::isExact ))
|
||||
questionItems.add( MPQuery.Result.allOf( new MPNewQuestion( site, query.getQuery() ), query.getQuery() ) );
|
||||
|
||||
Res.ui( () -> questionsModel.set( questions ) );
|
||||
Res.ui( () -> questionsModel.set( questionItems ) );
|
||||
} ) );
|
||||
queryField.putClientProperty( "JTextField.variant", "search" );
|
||||
queryField.addActionListener( event -> useQuestion( questionsModel.getSelectedItem() ) );
|
||||
queryField.addActionListener( this::useQuestion );
|
||||
queryField.addKeyListener( new KeyAdapter() {
|
||||
@Override
|
||||
public void keyPressed(final KeyEvent event) {
|
||||
@@ -672,7 +683,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
}
|
||||
|
||||
public void showSiteValues() {
|
||||
MPSite<?> site = sitesModel.getSelectedItem();
|
||||
MPSite<?> site = getSite();
|
||||
if (site == null)
|
||||
return;
|
||||
|
||||
@@ -717,7 +728,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
}
|
||||
|
||||
public void showSiteKeys() {
|
||||
MPSite<?> site = sitesModel.getSelectedItem();
|
||||
MPSite<?> site = getSite();
|
||||
if (site == null)
|
||||
return;
|
||||
|
||||
@@ -787,7 +798,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
}
|
||||
|
||||
public void deleteSite() {
|
||||
MPSite<?> site = sitesModel.getSelectedItem();
|
||||
MPSite<?> site = getSite();
|
||||
if (site == null)
|
||||
return;
|
||||
|
||||
@@ -797,61 +808,87 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
user.deleteSite( site );
|
||||
}
|
||||
|
||||
private String getSiteDescription(@Nonnull final MPSite<?> site) {
|
||||
private String getSiteDescription(@Nullable final MPQuery.Result<? extends MPSite<?>> item) {
|
||||
MPSite<?> site = (item != null)? item.getOption(): null;
|
||||
if (site == null)
|
||||
return " ";
|
||||
if (site instanceof MPNewSite)
|
||||
return strf( "<html><strong>%s</strong> <Add new site></html>", queryField.getText() );
|
||||
return strf( "<html><strong>%s</strong> <Add new site></html>", item.getKeyAsHTML() );
|
||||
|
||||
ImmutableList.Builder<Object> parameters = ImmutableList.builder();
|
||||
try {
|
||||
MPFileSite fileSite = (site instanceof MPFileSite)? (MPFileSite) site: null;
|
||||
if (fileSite != null)
|
||||
parameters.add( Res.format( fileSite.getLastUsed() ) );
|
||||
parameters.add( site.getAlgorithm().version() );
|
||||
parameters.add( strf( "#%d", site.getCounter().longValue() ) );
|
||||
parameters.add( strf( "<em>%s</em>", site.getLogin() ) );
|
||||
if ((fileSite != null) && (fileSite.getUrl() != null))
|
||||
parameters.add( fileSite.getUrl() );
|
||||
}
|
||||
catch (final MPAlgorithmException | MPKeyUnavailableException e) {
|
||||
logger.err( e, "While generating site description." );
|
||||
parameters.add( e.getLocalizedMessage() );
|
||||
}
|
||||
MPFileSite fileSite = (site instanceof MPFileSite)? (MPFileSite) site: null;
|
||||
if (fileSite != null)
|
||||
parameters.add( Res.format( fileSite.getLastUsed() ) );
|
||||
parameters.add( site.getAlgorithm().version() );
|
||||
parameters.add( strf( "#%d", site.getCounter().longValue() ) );
|
||||
if ((fileSite != null) && (fileSite.getUrl() != null))
|
||||
parameters.add( fileSite.getUrl() );
|
||||
|
||||
return strf( "<html><strong>%s</strong> (%s)</html>", site.getSiteName(),
|
||||
return strf( "<html><strong>%s</strong> (%s)</html>", item.getKeyAsHTML(),
|
||||
Joiner.on( " - " ).skipNulls().join( parameters.build() ) );
|
||||
}
|
||||
|
||||
private String getQuestionDescription(@Nullable final MPQuery.Result<? extends MPQuestion> item) {
|
||||
MPQuestion question = (item != null)? item.getOption(): null;
|
||||
if (question == null)
|
||||
return "<site>";
|
||||
if (question instanceof MPNewQuestion)
|
||||
return strf( "<html>%s <Add new question></html>", item.getKeyAsHTML() );
|
||||
|
||||
return strf( "<html>%s</html>", item.getKeyAsHTML() );
|
||||
}
|
||||
|
||||
private void useSite(final ActionEvent event) {
|
||||
MPSite<?> site = sitesModel.getSelectedItem();
|
||||
MPSite<?> site = getSite();
|
||||
if (site instanceof MPNewSite) {
|
||||
if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(
|
||||
if (JOptionPane.YES_OPTION != JOptionPane.showConfirmDialog(
|
||||
this, strf( "<html>Remember the site <strong>%s</strong>?</html>", site.getSiteName() ),
|
||||
"New Site", JOptionPane.YES_NO_OPTION )) {
|
||||
sitesModel.setSelectedItem( user.addSite( site.getSiteName() ) );
|
||||
useSite( event );
|
||||
}
|
||||
return;
|
||||
"New Site", JOptionPane.YES_NO_OPTION ))
|
||||
return;
|
||||
|
||||
site = user.addSite( site.getSiteName() );
|
||||
}
|
||||
|
||||
boolean loginResult = (copyLoginKeyStroke.getModifiers() & event.getModifiers()) != 0;
|
||||
showSiteResult( site, loginResult, result -> {
|
||||
boolean loginResult = (copyLoginKeyStroke.getModifiers() & event.getModifiers()) != 0;
|
||||
MPSite<?> fsite = site;
|
||||
Res.ui( getSiteResult( site, loginResult ), result -> {
|
||||
if (result == null)
|
||||
return;
|
||||
|
||||
if (site instanceof MPFileSite)
|
||||
((MPFileSite) site).use();
|
||||
if (fsite instanceof MPFileSite)
|
||||
((MPFileSite) fsite).use();
|
||||
|
||||
copyResult( result );
|
||||
} );
|
||||
}
|
||||
|
||||
private void showSiteResult(@Nullable final MPSite<?> site) {
|
||||
showSiteResult( site, false, result -> {
|
||||
private void setShowLogin(final boolean showLogin) {
|
||||
if (showLogin == this.showLogin)
|
||||
return;
|
||||
|
||||
this.showLogin = showLogin;
|
||||
showSiteItem( sitesModel.getSelectedItem() );
|
||||
}
|
||||
|
||||
private void showSiteItem(@Nullable final MPQuery.Result<? extends MPSite<?>> item) {
|
||||
MPSite<?> site = (item != null)? item.getOption(): null;
|
||||
Res.ui( getSiteResult( site, showLogin ), result -> {
|
||||
if (!showLogin && (site != null))
|
||||
resultLabel.setText( (result != null)? strf( "Your password for %s:", site.getSiteName() ): " " );
|
||||
else if (showLogin && (site != null))
|
||||
resultLabel.setText( (result != null)? strf( "Your login for %s:", site.getSiteName() ): " " );
|
||||
|
||||
resultField.setText( (result != null)? result: " " );
|
||||
settingsButton.setEnabled( result != null );
|
||||
questionsButton.setEnabled( result != null );
|
||||
editButton.setEnabled( result != null );
|
||||
keyButton.setEnabled( result != null );
|
||||
deleteButton.setEnabled( result != null );
|
||||
} );
|
||||
}
|
||||
|
||||
private void showSiteResult(@Nullable final MPSite<?> site, final boolean loginResult, final Consumer<String> resultCallback) {
|
||||
Res.job( () -> {
|
||||
private ListenableFuture<String> getSiteResult(@Nullable final MPSite<?> site, final boolean loginResult) {
|
||||
return Res.job( () -> {
|
||||
try {
|
||||
if (site != null)
|
||||
return loginResult? site.getLogin(): site.getResult();
|
||||
@@ -861,57 +898,37 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
}
|
||||
|
||||
return null;
|
||||
}, resultCallback.andThen( result -> Res.ui( () -> {
|
||||
passwordLabel.setText( ((result != null) && (site != null))? strf( "Your password for %s:", site.getSiteName() ): " " );
|
||||
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 );
|
||||
} ) ) );
|
||||
} );
|
||||
}
|
||||
|
||||
private void useQuestion(@Nullable final MPQuestion question) {
|
||||
private void useQuestion(final ActionEvent event) {
|
||||
MPQuestion question = getQuestion();
|
||||
if (question instanceof MPNewQuestion) {
|
||||
if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(
|
||||
if (JOptionPane.YES_OPTION != JOptionPane.showConfirmDialog(
|
||||
this,
|
||||
strf( "<html>Remember the security question with keyword <strong>%s</strong>?</html>",
|
||||
Strings.isNullOrEmpty( question.getKeyword() )? "<empty>": question.getKeyword() ),
|
||||
"New Question", JOptionPane.YES_NO_OPTION )) {
|
||||
useQuestion( question.getSite().addQuestion( question.getKeyword() ) );
|
||||
}
|
||||
return;
|
||||
"New Question", JOptionPane.YES_NO_OPTION ))
|
||||
return;
|
||||
|
||||
question = question.getSite().addQuestion( question.getKeyword() );
|
||||
}
|
||||
|
||||
showQuestionResult( question, result -> {
|
||||
MPQuestion fquestion = question;
|
||||
Res.ui( getQuestionResult( question ), result -> {
|
||||
if (result == null)
|
||||
return;
|
||||
|
||||
if (question instanceof MPFileQuestion)
|
||||
((MPFileQuestion) question).use();
|
||||
if (fquestion instanceof MPFileQuestion)
|
||||
((MPFileQuestion) fquestion).use();
|
||||
|
||||
copyResult( result );
|
||||
} );
|
||||
}
|
||||
|
||||
private void showQuestionResult(@Nullable final MPQuestion question) {
|
||||
showQuestionResult( question, answer -> {
|
||||
} );
|
||||
}
|
||||
|
||||
private void showQuestionResult(@Nullable final MPQuestion question, final Consumer<String> resultCallback) {
|
||||
Res.job( () -> {
|
||||
try {
|
||||
if (question != null)
|
||||
return question.getAnswer();
|
||||
}
|
||||
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
|
||||
logger.err( e, "While resolving answer for: %s", question );
|
||||
}
|
||||
|
||||
return null;
|
||||
}, resultCallback.andThen( answer -> Res.ui( () -> {
|
||||
private void showQuestionItem(@Nullable final MPQuery.Result<? extends MPQuestion> item) {
|
||||
MPQuestion question = (item != null)? item.getOption(): null;
|
||||
Res.ui( getQuestionResult( question ), answer -> {
|
||||
if ((answer == null) || (question == null))
|
||||
answerLabel.setText( " " );
|
||||
else
|
||||
@@ -921,7 +938,21 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
strf( "<html>Answer for site <b>%s</b>, of question with keyword <b>%s</b>:",
|
||||
question.getSite().getSiteName(), question.getKeyword() ) );
|
||||
answerField.setText( (answer != null)? answer: " " );
|
||||
} ) ) );
|
||||
} );
|
||||
}
|
||||
|
||||
private ListenableFuture<String> getQuestionResult(@Nullable final MPQuestion question) {
|
||||
return Res.job( () -> {
|
||||
try {
|
||||
if (question != null)
|
||||
return question.getAnswer();
|
||||
}
|
||||
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
|
||||
logger.err( e, "While resolving answer for: %s", question );
|
||||
}
|
||||
|
||||
return null;
|
||||
} );
|
||||
}
|
||||
|
||||
private void copyResult(final String result) {
|
||||
@@ -936,9 +967,29 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
Window window = SwingUtilities.windowForComponent( UserContentPanel.this );
|
||||
if (window instanceof Frame)
|
||||
((Frame) window).setExtendedState( Frame.ICONIFIED );
|
||||
|
||||
setShowLogin( false );
|
||||
} );
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private MPSite<?> getSite() {
|
||||
MPQuery.Result<? extends MPSite<?>> selectedSite = sitesModel.getSelectedItem();
|
||||
if (selectedSite == null)
|
||||
return null;
|
||||
|
||||
return selectedSite.getOption();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private MPQuestion getQuestion() {
|
||||
MPQuery.Result<? extends MPQuestion> selectedQuestion = questionsModel.getSelectedItem();
|
||||
if (selectedQuestion == null)
|
||||
return null;
|
||||
|
||||
return selectedQuestion.getOption();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyTyped(final KeyEvent event) {
|
||||
}
|
||||
@@ -955,25 +1006,27 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
sitesList.dispatchEvent( event );
|
||||
}
|
||||
|
||||
private synchronized void updateSites(@Nullable final String query) {
|
||||
private synchronized void updateSites(@Nullable final String queryText) {
|
||||
if (updateSitesJob != null)
|
||||
updateSitesJob.cancel( true );
|
||||
|
||||
updateSitesJob = Res.job( () -> {
|
||||
Collection<MPSite<?>> sites = new LinkedList<>( user.findSites( query ) );
|
||||
MPQuery query = new MPQuery( queryText );
|
||||
Collection<MPQuery.Result<? extends MPSite<?>>> siteItems =
|
||||
new LinkedList<>( user.findSites( query ) );
|
||||
|
||||
if (!Strings.isNullOrEmpty( query ))
|
||||
if (sites.stream().noneMatch( site -> site.getSiteName().equalsIgnoreCase( query ) ))
|
||||
sites.add( new MPNewSite( user, query ) );
|
||||
if (!Strings.isNullOrEmpty( queryText ))
|
||||
if (siteItems.stream().noneMatch( MPQuery.Result::isExact ))
|
||||
siteItems.add( MPQuery.Result.allOf( new MPNewSite( user, query.getQuery() ), query.getQuery() ) );
|
||||
|
||||
Res.ui( () -> sitesModel.set( sites ) );
|
||||
Res.ui( () -> sitesModel.set( siteItems ) );
|
||||
} );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserUpdated(final MPUser<?> user) {
|
||||
updateSites( queryField.getText() );
|
||||
showSiteResult( sitesModel.getSelectedItem() );
|
||||
showSiteItem( sitesModel.getSelectedItem() );
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -983,5 +1036,13 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
|
||||
@Override
|
||||
public void onUserInvalidated(final MPUser<?> user) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(final KeyEvent e) {
|
||||
if (e.getKeyCode() == KeyEvent.VK_SHIFT)
|
||||
setShowLogin( e.isShiftDown() );
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.lyndir.masterpassword.model;
|
||||
|
||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.strf;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2018-09-11
|
||||
*/
|
||||
public class MPQuery {
|
||||
|
||||
@Nonnull
|
||||
private final String query;
|
||||
|
||||
public MPQuery(@Nullable final String query) {
|
||||
this.query = (query != null)? query: "";
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if this query is contained wholly inside the given {@code key}.
|
||||
*/
|
||||
@Nonnull
|
||||
public <T extends Comparable<? super T>> Optional<Result<T>> find(final T option, final Function<T, CharSequence> keyForOption) {
|
||||
CharSequence key = keyForOption.apply( option );
|
||||
Result<T> result = Result.noneOf( option, key );
|
||||
if (query.isEmpty())
|
||||
return Optional.of( result );
|
||||
if (key.length() == 0)
|
||||
return Optional.empty();
|
||||
|
||||
// Consume query and key characters until one of them runs out, recording any matches against the result's key.
|
||||
int q = 0, k = 0;
|
||||
while ((q < query.length()) && (k < key.length())) {
|
||||
if (query.charAt( q ) == key.charAt( k )) {
|
||||
result.keyMatchedAt( k );
|
||||
++q;
|
||||
}
|
||||
|
||||
++k;
|
||||
}
|
||||
|
||||
// If query is consumed, the result is a hit.
|
||||
return (q >= query.length())? Optional.of( result ): Optional.empty();
|
||||
}
|
||||
|
||||
public static class Result<T extends Comparable<? super T>> implements Comparable<Result<T>> {
|
||||
|
||||
private final T option;
|
||||
private final CharSequence key;
|
||||
private final boolean[] keyMatches;
|
||||
|
||||
Result(final T option, final CharSequence key) {
|
||||
this.option = option;
|
||||
this.key = key;
|
||||
|
||||
keyMatches = new boolean[key.length()];
|
||||
}
|
||||
|
||||
public static <T extends Comparable<? super T>> Result<T> noneOf(final T option, final CharSequence key) {
|
||||
return new Result<>( option, key );
|
||||
}
|
||||
|
||||
public static <T extends Comparable<? super T>> Result<T> allOf(final T option, final CharSequence key) {
|
||||
Result<T> result = noneOf( option, key );
|
||||
Arrays.fill( result.keyMatches, true );
|
||||
return result;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public T getOption() {
|
||||
return option;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CharSequence getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public String getKeyAsHTML() {
|
||||
return getKeyAsHTML( "u" );
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "MagicCharacter", "HardcodedFileSeparator" })
|
||||
public String getKeyAsHTML(final String mark) {
|
||||
String closeMark = mark.contains( " " )? mark.substring( 0, mark.indexOf( ' ' ) ): mark;
|
||||
StringBuilder html = new StringBuilder();
|
||||
boolean marked = false;
|
||||
|
||||
for (int i = 0; i < key.length(); ++i) {
|
||||
if (keyMatches[i] && !marked) {
|
||||
html.append( '<' ).append( mark ).append( '>' );
|
||||
marked = true;
|
||||
} else if (!keyMatches[i] && marked) {
|
||||
html.append( '<' ).append( '/' ).append( closeMark ).append( '>' );
|
||||
marked = false;
|
||||
}
|
||||
|
||||
html.append( key.charAt( i ) );
|
||||
}
|
||||
|
||||
if (marked)
|
||||
html.append( '<' ).append( '/' ).append( closeMark ).append( '>' );
|
||||
|
||||
return html.toString();
|
||||
}
|
||||
|
||||
public boolean[] getKeyMatches() {
|
||||
return keyMatches.clone();
|
||||
}
|
||||
|
||||
public boolean isExact() {
|
||||
for (final boolean keyMatch : keyMatches)
|
||||
if (!keyMatch)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void keyMatchedAt(final int k) {
|
||||
keyMatches[k] = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull final Result<T> o) {
|
||||
return getOption().compareTo( o.getOption() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (!(o instanceof Result))
|
||||
return false;
|
||||
|
||||
Result<?> r = (Result<?>) o;
|
||||
return Objects.equals( option, r.option ) && Objects.equals( key, r.key ) && Arrays.equals( keyMatches, r.keyMatches );
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getOption().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return strf( "{Result: %s}", key );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,5 +117,5 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
|
||||
Collection<Q> getQuestions();
|
||||
|
||||
@Nonnull
|
||||
ImmutableCollection<Q> findQuestions(@Nullable String query);
|
||||
ImmutableCollection<MPQuery.Result<Q>> findQuestions(MPQuery query);
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
|
||||
Collection<S> getSites();
|
||||
|
||||
@Nonnull
|
||||
ImmutableCollection<S> findSites(@Nullable String query);
|
||||
ImmutableCollection<MPQuery.Result<S>> findSites(MPQuery query);
|
||||
|
||||
void addListener(Listener listener);
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ package com.lyndir.masterpassword.model.impl;
|
||||
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
|
||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableCollection;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import com.google.common.primitives.UnsignedInteger;
|
||||
@@ -193,11 +192,10 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ImmutableCollection<Q> findQuestions(@Nullable final String query) {
|
||||
ImmutableSortedSet.Builder<Q> results = ImmutableSortedSet.naturalOrder();
|
||||
for (final Q question : getQuestions())
|
||||
if (Strings.isNullOrEmpty( query ) || question.getKeyword().startsWith( query ))
|
||||
results.add( question );
|
||||
public ImmutableCollection<MPQuery.Result<Q>> findQuestions(final MPQuery query) {
|
||||
ImmutableSortedSet.Builder<MPQuery.Result<Q>> results = ImmutableSortedSet.naturalOrder();
|
||||
for (final Q question : questions)
|
||||
query.find( question, MPQuestion::getKeyword ).ifPresent( results::add );
|
||||
|
||||
return results.build();
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ package com.lyndir.masterpassword.model.impl;
|
||||
|
||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableCollection;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import com.lyndir.lhunath.opal.system.CodeUtils;
|
||||
@@ -201,11 +200,10 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ImmutableCollection<S> findSites(@Nullable final String query) {
|
||||
ImmutableSortedSet.Builder<S> results = ImmutableSortedSet.naturalOrder();
|
||||
for (final S site : getSites())
|
||||
if (Strings.isNullOrEmpty( query ) || site.getSiteName().startsWith( query ))
|
||||
results.add( site );
|
||||
public ImmutableCollection<MPQuery.Result<S>> findSites(final MPQuery query) {
|
||||
ImmutableSortedSet.Builder<MPQuery.Result<S>> results = ImmutableSortedSet.naturalOrder();
|
||||
for (final S site : sites.values())
|
||||
query.find( site, MPSite::getSiteName ).ifPresent( results::add );
|
||||
|
||||
return results.build();
|
||||
}
|
||||
|
||||
Submodule public/site updated: 0623231ad5...914a60cd25
Reference in New Issue
Block a user