diff --git a/gui/griffon-app/i18n/messages.properties b/gui/griffon-app/i18n/messages.properties index e0aca9b4..12da9f5c 100644 --- a/gui/griffon-app/i18n/messages.properties +++ b/gui/griffon-app/i18n/messages.properties @@ -669,7 +669,7 @@ FOLDER_CONFIRM_DELETE=This folder is not empty. Are you sure you want to delete ## New message window UNREAD=Unread RECIPIENTS=Recipients -RECIPIENTS_TITLE=Drag and drop contacts from your contact list +RECIPIENTS_TITLE=You can drag and drop contacts from your contact list here SEND=Send MESSAGE_VERB=Message MESSAGE_NOUN=Message @@ -681,6 +681,9 @@ SUBJECT_TOO_LONG_BODY=Your subject is {0} characters. The maximum length is {1} NO_RECIPIENTS=No Recipients NO_RECIPIENTS_BODY=Please add at least one recipient. +## Contact chooser +CONTACT_CHOOSER_SELECT_CONTACT=Select Contact + ## Add Contact dialog ADD_CONTACT_SPECIFIC=Add Contact ADD_CONTACT_TITLE=Add a new contact diff --git a/gui/griffon-app/models/com/muwire/gui/contacts/ContactSelectorModel.groovy b/gui/griffon-app/models/com/muwire/gui/contacts/ContactSelectorModel.groovy index 84c48316..4142f255 100644 --- a/gui/griffon-app/models/com/muwire/gui/contacts/ContactSelectorModel.groovy +++ b/gui/griffon-app/models/com/muwire/gui/contacts/ContactSelectorModel.groovy @@ -1,5 +1,6 @@ package com.muwire.gui.contacts +import com.muwire.core.Core import com.muwire.core.Persona import griffon.core.artifact.GriffonModel import griffon.inject.MVCMember @@ -9,5 +10,6 @@ import javax.annotation.Nonnull @ArtifactProviderFor(GriffonModel) class ContactSelectorModel { + Core core Set contacts } diff --git a/gui/griffon-app/views/com/muwire/gui/NewMessageView.groovy b/gui/griffon-app/views/com/muwire/gui/NewMessageView.groovy index b09e8623..7ed2ea00 100644 --- a/gui/griffon-app/views/com/muwire/gui/NewMessageView.groovy +++ b/gui/griffon-app/views/com/muwire/gui/NewMessageView.groovy @@ -62,6 +62,7 @@ class NewMessageView { def params = [:] params.contacts = model.recipients + params.core = model.core contactSelector = mvcGroup.createMVCGroup("contact-selector", UUID.randomUUID().toString(), params) window = builder.frame(visible : false, locationRelativeTo : mainFrame, diff --git a/gui/griffon-app/views/com/muwire/gui/WatchedDirectoryView.groovy b/gui/griffon-app/views/com/muwire/gui/WatchedDirectoryView.groovy index 13386ff1..4911be76 100644 --- a/gui/griffon-app/views/com/muwire/gui/WatchedDirectoryView.groovy +++ b/gui/griffon-app/views/com/muwire/gui/WatchedDirectoryView.groovy @@ -43,6 +43,7 @@ class WatchedDirectoryView { mainFrame = application.windowManager.findWindow("main-frame") def params = [:] + params.core = model.core params.contacts = model.allowedContacts contactSelector = mvcGroup.createMVCGroup("contact-selector", UUID.randomUUID().toString(), params) diff --git a/gui/griffon-app/views/com/muwire/gui/contacts/ContactSelectorView.groovy b/gui/griffon-app/views/com/muwire/gui/contacts/ContactSelectorView.groovy index 121bd2a9..c216190f 100644 --- a/gui/griffon-app/views/com/muwire/gui/contacts/ContactSelectorView.groovy +++ b/gui/griffon-app/views/com/muwire/gui/contacts/ContactSelectorView.groovy @@ -23,6 +23,10 @@ import javax.swing.border.TitledBorder import java.awt.BorderLayout import java.awt.datatransfer.DataFlavor import java.awt.datatransfer.Transferable +import java.awt.event.ItemEvent +import java.awt.event.ItemListener +import java.awt.event.KeyAdapter +import java.awt.event.KeyEvent import java.awt.event.MouseAdapter import java.awt.event.MouseEvent @@ -39,23 +43,37 @@ class ContactSelectorView { DefaultListModel contactsModel JList contactsList + ContactChooser contactChooser + ContactChooserModel contactChooserModel JPanel component + private UISettings settings + + private Contact lastSelectedContact + void initUI() { + settings = application.context.get("ui-settings") + contactsModel = new DefaultListModel() model.contacts.each { - contactsModel.addElement(new Contact(it)) + contactsModel.addElement(new Contact(it, settings)) } contactsList = new JList(contactsModel) contactsList.setVisibleRowCount(2) - component = builder.panel(border : builder.titledBorder(title : trans("RECIPIENTS_TITLE"), - border : builder.etchedBorder(), titlePosition : TitledBorder.TOP)) { + contactChooserModel= new ContactChooserModel(model.core.trustService.getGood().values()) + contactChooser = new ContactChooser(settings, contactChooserModel) + + component = builder.panel { borderLayout() - scrollPane(constraints : BorderLayout.CENTER) { + scrollPane(constraints : BorderLayout.CENTER, border : builder.titledBorder(title : trans("RECIPIENTS_TITLE"), + border : builder.etchedBorder(), titlePosition : TitledBorder.TOP)) { widget(contactsList) } + widget(contactChooser, constraints: BorderLayout.SOUTH, + border : builder.titledBorder(title : trans("CONTACT_CHOOSER_SELECT_CONTACT"), + border : builder.etchedBorder(), titlePosition : TitledBorder.TOP)) } } @@ -79,6 +97,34 @@ class ContactSelectorView { contactsMenu.show(e.getComponent(), e.getX(), e.getY()) } }) + + contactChooser.addItemListener(new ItemListener() { + @Override + void itemStateChanged(ItemEvent e) { + if (e.getStateChange() != ItemEvent.SELECTED) + return + Object item = e.getItem() + if (item == null || item instanceof String) + return + ContactChooserPOP ccp = (ContactChooserPOP) item + if (ccp.getPersona() == null) + return + + lastSelectedContact = new Contact(ccp.getPersona(), settings) + } + }) + + contactChooser.getEditor().getEditorComponent().addKeyListener(new KeyAdapter() { + @Override + void keyReleased(KeyEvent e) { + if (e.getKeyCode() != KeyEvent.VK_ENTER) + return + if (lastSelectedContact != null) { + if (model.contacts.add(lastSelectedContact.persona)) + contactsModel << lastSelectedContact + } + } + }) } void removeSelectedContacts() { @@ -110,7 +156,7 @@ class ContactSelectorView { items.each { if (model.contacts.add(it)) - contactsModel.insertElementAt(new Contact(it),0) + contactsModel.insertElementAt(new Contact(it, settings),0) } return true } @@ -119,8 +165,9 @@ class ContactSelectorView { private static class Contact { private final UISettings settings private final Persona persona - Contact(Persona persona) { + Contact(Persona persona, UISettings settings) { this.persona = persona + this.settings = settings } public String toString() { diff --git a/gui/src/main/groovy/com/muwire/gui/contacts/ContactChooser.groovy b/gui/src/main/groovy/com/muwire/gui/contacts/ContactChooser.groovy new file mode 100644 index 00000000..bf1fcb48 --- /dev/null +++ b/gui/src/main/groovy/com/muwire/gui/contacts/ContactChooser.groovy @@ -0,0 +1,18 @@ +package com.muwire.gui.contacts + +import com.muwire.gui.UISettings +import com.muwire.gui.profile.PersonaOrProfileCellRenderer + +import javax.swing.JComboBox +import javax.swing.JTextField + +class ContactChooser extends JComboBox{ + ContactChooser(UISettings settings, ContactChooserModel model) { + super() + setEditable(true) + setModel(model) + setEditor(new ContactChooserEditor(model, this)) + setRenderer(new PersonaOrProfileListCellRenderer(settings)) + + } +} diff --git a/gui/src/main/groovy/com/muwire/gui/contacts/ContactChooserEditor.groovy b/gui/src/main/groovy/com/muwire/gui/contacts/ContactChooserEditor.groovy new file mode 100644 index 00000000..e03888e6 --- /dev/null +++ b/gui/src/main/groovy/com/muwire/gui/contacts/ContactChooserEditor.groovy @@ -0,0 +1,47 @@ +package com.muwire.gui.contacts + +import javax.swing.SwingUtilities +import javax.swing.event.DocumentEvent +import javax.swing.event.DocumentListener +import javax.swing.plaf.basic.BasicComboBoxEditor +import java.awt.event.ActionEvent +import java.awt.event.ActionListener +import java.awt.event.KeyAdapter +import java.awt.event.KeyEvent + +class ContactChooserEditor extends BasicComboBoxEditor{ + + private final ContactChooserModel model + private final ContactChooser field + + ContactChooserEditor(ContactChooserModel model, ContactChooser field) { + super() + this.model = model + this.field = field + + editor.addKeyListener(new KeyAdapter() { + @Override + void keyPressed(KeyEvent e) { + int keyCode = e.getKeyCode() + if (keyCode == KeyEvent.VK_ENTER) + editor.setText("") + else if (keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_DOWN) { + return + } else { + SwingUtilities.invokeLater { + field.hidePopup() + if (model.onKeyStroke(editor.text)) + field.showPopup() + } + } + } + }) + } + + public void setItem(Object o) { + if (o == null || o instanceof ContactChooserPOP) + super.setItem(o) + else + editor.setText("") + } +} diff --git a/gui/src/main/groovy/com/muwire/gui/contacts/ContactChooserModel.groovy b/gui/src/main/groovy/com/muwire/gui/contacts/ContactChooserModel.groovy new file mode 100644 index 00000000..ef1a3a2e --- /dev/null +++ b/gui/src/main/groovy/com/muwire/gui/contacts/ContactChooserModel.groovy @@ -0,0 +1,73 @@ +package com.muwire.gui.contacts + +import com.muwire.core.trust.TrustService +import com.muwire.core.trust.TrustService.TrustEntry +import com.muwire.gui.profile.PersonaOrProfile +import com.muwire.gui.profile.PersonaOrProfileComparator +import com.muwire.gui.profile.TrustPOP + +import javax.swing.ComboBoxModel +import javax.swing.DefaultComboBoxModel +import javax.swing.DefaultListModel +import javax.swing.MutableComboBoxModel + +class ContactChooserModel extends DefaultComboBoxModel implements MutableComboBoxModel { + private final List allObjects + private final Comparator popComparator = new PersonaOrProfileComparator() + PersonaOrProfile selectedPOP + + ContactChooserModel(Collection entries) { + allObjects = entries.collect {new ContactChooserPOP(it)} + addAll(allObjects) + } + + boolean onKeyStroke(String selected) { + + if (selected == null || selected.length() == 0) { + removeAllElements() + addAll(allObjects) + return true + } + + removeAllElements() + setSelectedItem(new ContactChooserPOP(selected)) + + List matches = [] + for(PersonaOrProfile pop : allObjects) { + if (justName(pop).containsIgnoreCase(selected)) + matches << pop + } + if (matches.isEmpty()) + return false + + Collections.sort(matches, popComparator) + addAll(matches) + true + } + + private static String justName(PersonaOrProfile pop) { + String name = pop.getPersona().getHumanReadableName() + name.substring(0, name.indexOf("@")).toLowerCase() + } + + @Override + public void setSelectedItem(Object anObject) { + if (anObject instanceof String) { + if (anObject == selectedPOP?.getPersona()?.getHumanReadableName()) + super.setSelectedItem(selectedPOP) + return + } + if (anObject == null) + selectedPOP = null + else { + if (!(anObject instanceof ContactChooserPOP)) + throw new Exception("invalid type $anObject") + + ContactChooserPOP ccp = (ContactChooserPOP) anObject + if (ccp.getPersona() != null) + selectedPOP = ccp + } + super.setSelectedItem(anObject) + } + +} diff --git a/gui/src/main/groovy/com/muwire/gui/contacts/ContactChooserPOP.groovy b/gui/src/main/groovy/com/muwire/gui/contacts/ContactChooserPOP.groovy new file mode 100644 index 00000000..64b754a6 --- /dev/null +++ b/gui/src/main/groovy/com/muwire/gui/contacts/ContactChooserPOP.groovy @@ -0,0 +1,65 @@ +package com.muwire.gui.contacts + +import com.muwire.core.Persona +import com.muwire.core.profile.MWProfile +import com.muwire.core.profile.MWProfileHeader +import com.muwire.core.trust.TrustService.TrustEntry +import com.muwire.gui.profile.PersonaOrProfile +import com.muwire.gui.profile.ThumbnailIcon + +import javax.swing.Icon + +class ContactChooserPOP implements PersonaOrProfile { + private final TrustEntry trustEntry + private final String text + private Icon icon + + ContactChooserPOP(TrustEntry trustEntry) { + this.trustEntry = trustEntry + this.text = null + } + + ContactChooserPOP(String text) { + this.text = text + this.trustEntry = null + } + + Persona getPersona() { + trustEntry?.getPersona() + } + + @Override + Icon getThumbnail() { + MWProfileHeader header = getHeader() + if (header == null) + return null + if (icon == null) + icon = new ThumbnailIcon(header.getThumbNail()) + return icon + } + + @Override + MWProfileHeader getHeader() { + trustEntry.getProfileHeader() + } + + @Override + MWProfile getProfile() { + trustEntry.getProfile() + } + + public boolean equals(Object o) { + if (!(o instanceof ContactChooserPOP)) + return false + ContactChooserPOP other = (ContactChooserPOP)o + Objects.equals(text, other.text) && + Objects.equals(trustEntry, other.trustEntry) + } + + String toString() { + if (text != null) + return text + else + return getPersona().getHumanReadableName() + } +} diff --git a/gui/src/main/groovy/com/muwire/gui/contacts/PersonaOrProfileListCellRenderer.groovy b/gui/src/main/groovy/com/muwire/gui/contacts/PersonaOrProfileListCellRenderer.groovy new file mode 100644 index 00000000..fedeeebc --- /dev/null +++ b/gui/src/main/groovy/com/muwire/gui/contacts/PersonaOrProfileListCellRenderer.groovy @@ -0,0 +1,54 @@ +package com.muwire.gui.contacts + +import com.muwire.gui.PersonaCellRenderer +import com.muwire.gui.UISettings +import com.muwire.gui.profile.PersonaOrProfile + +import javax.swing.JLabel +import javax.swing.JList +import javax.swing.ListCellRenderer +import java.awt.Component + +import static com.muwire.gui.Translator.trans + +class PersonaOrProfileListCellRenderer implements ListCellRenderer{ + + private final UISettings settings + + PersonaOrProfileListCellRenderer(UISettings settings) { + this.settings = settings + } + + @Override + Component getListCellRendererComponent(JList list, PersonaOrProfile value, + int index, boolean isSelected, boolean cellHasFocus) { + + JLabel rv = new JLabel() + String text + if (settings.personaRendererIds) + text = "" + PersonaCellRenderer.htmlize(value.getPersona()) + "" + else + text = PersonaCellRenderer.justName(value.getPersona()) + + rv.setText(text) + + if (value.getThumbnail() != null) + rv.setIcon(value.getThumbnail()) + else + rv.setIcon(null) + + if (value.getTitle() != null) + rv.setToolTipText(value.getTitle()) + else + rv.setToolTipText(trans("NO_PROFILE")) + + if (!isSelected) { + rv.setForeground(list.getForeground()) + rv.setBackground(list.getBackground()) + } else { + rv.setForeground(list.getSelectionForeground()) + rv.setBackground(list.getSelectionBackground()) + } + rv + } +}