From d4f46be3b795a4d77c3c89b8345dd2371a4488f6 Mon Sep 17 00:00:00 2001 From: Zlatin Balevsky Date: Fri, 10 Jun 2022 00:19:37 +0100 Subject: [PATCH] Contact selector dropdown when the user types @ --- .../views/com/muwire/gui/ChatRoomView.groovy | 12 +- .../com/muwire/gui/chat/ChatEntryPane.groovy | 201 ++++++++++++++++++ .../com/muwire/gui/chat/ChatTextField.groovy | 32 --- 3 files changed, 206 insertions(+), 39 deletions(-) create mode 100644 gui/src/main/groovy/com/muwire/gui/chat/ChatEntryPane.groovy delete mode 100644 gui/src/main/groovy/com/muwire/gui/chat/ChatTextField.groovy diff --git a/gui/griffon-app/views/com/muwire/gui/ChatRoomView.groovy b/gui/griffon-app/views/com/muwire/gui/ChatRoomView.groovy index 075f6903..d550b2fc 100644 --- a/gui/griffon-app/views/com/muwire/gui/ChatRoomView.groovy +++ b/gui/griffon-app/views/com/muwire/gui/ChatRoomView.groovy @@ -2,7 +2,8 @@ package com.muwire.gui import com.muwire.core.trust.TrustLevel import com.muwire.gui.chat.ChatEntry -import com.muwire.gui.chat.ChatTextField +import com.muwire.gui.chat.ChatEntryPane + import com.muwire.gui.contacts.POPLabel import com.muwire.gui.profile.PersonaOrProfile import com.muwire.gui.profile.PersonaOrProfileCellRenderer @@ -20,23 +21,19 @@ import java.text.SimpleDateFormat import static com.muwire.gui.Translator.trans import griffon.inject.MVCMember import griffon.metadata.ArtifactProviderFor -import net.i2p.data.DataHelper import javax.swing.JMenuItem import javax.swing.JPopupMenu import javax.swing.JSplitPane import javax.swing.JTextPane import javax.swing.ListSelectionModel -import javax.swing.SwingConstants import javax.swing.text.Element import javax.swing.text.Style import javax.swing.text.StyleConstants import javax.swing.text.StyleContext import javax.swing.text.StyledDocument -import javax.swing.SpringLayout.Constraints import com.muwire.core.Persona -import com.muwire.core.chat.ChatConnectionAttemptStatus import java.awt.BorderLayout import java.awt.Color @@ -60,7 +57,7 @@ class ChatRoomView { def pane def parent - ChatTextField sayField + ChatEntryPane sayField JTextPane roomTextArea def textScrollPane def membersTable @@ -68,12 +65,13 @@ class ChatRoomView { UISettings settings private static final SimpleDateFormat SDF = new SimpleDateFormat("dd/MM hh:mm:ss") + void initUI() { settings = application.context.get("ui-settings") int rowHeight = application.context.get("row-height") def parentModel = mvcGroup.parentGroup.model - sayField = new ChatTextField() + sayField = new ChatEntryPane(settings, model.members) if (model.console || model.privateChat) { pane = builder.panel { diff --git a/gui/src/main/groovy/com/muwire/gui/chat/ChatEntryPane.groovy b/gui/src/main/groovy/com/muwire/gui/chat/ChatEntryPane.groovy new file mode 100644 index 00000000..3aa10998 --- /dev/null +++ b/gui/src/main/groovy/com/muwire/gui/chat/ChatEntryPane.groovy @@ -0,0 +1,201 @@ +package com.muwire.gui.chat + + +import com.muwire.gui.UISettings +import com.muwire.gui.contacts.POPLabel +import com.muwire.gui.profile.PersonaOrProfile +import com.muwire.gui.profile.ProfileConstants +import sun.swing.UIAction + +import javax.swing.* +import javax.swing.event.MenuKeyEvent +import javax.swing.event.MenuKeyListener +import javax.swing.text.* +import java.awt.* +import java.awt.event.ActionEvent +import java.awt.event.KeyAdapter +import java.awt.event.KeyEvent +import java.util.List +import java.util.stream.Collectors + +class ChatEntryPane extends JTextPane { + + private static final char AT = "@".toCharacter() + private static final char BACKSPACE = (char)8 + private static final int ENTER = 10 + + private final UISettings settings + private final List members + + private JPopupMenu popupMenu + private Point lastPoint + private Component lastComponent + private Action backspaceAction + + Closure actionPerformed + + ChatEntryPane(UISettings settings, List members) { + super() + this.settings = settings + this.members = members + + setEditable(true) + addKeyListener(new KeyAdapter() { + @Override + void keyTyped(KeyEvent e) { + if (e.getKeyChar() == AT) { + lastPoint = getCaret().getMagicCaretPosition() + lastComponent = e.getComponent() + if (lastPoint == null || lastComponent == null) + return + SwingUtilities.invokeLater { + showPopupMenu(false) + } + } else if (((int)e.getKeyChar()) == ENTER) { + SwingUtilities.invokeLater { + actionPerformed.call() + } + } + } + }) + + KeyStroke back = KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0) + InputMap inputMap = getInputMap() + ActionMap actionMap = getActionMap() + + Object backObject = inputMap.get(back) + backspaceAction = actionMap.get(backObject) + actionMap.put(backObject, new BackspaceAction(backspaceAction)) + } + + private String getTextSinceAt(){ + final int caretPosition = getCaret().getDot() + int startPosition = caretPosition + while (startPosition > 0) { + if (getText(startPosition - 1, 1) == "@") + break + startPosition -- + } + getText(startPosition, caretPosition - startPosition) + } + + private void showPopupMenu(boolean filter) { + popupMenu?.setVisible(false) + + String typedText = getTextSinceAt() + + List items = members.stream(). + filter({ + if (!filter) + return true + justName(it).containsIgnoreCase(typedText) + }). + map({new MemberAction(it)}). + map({new JMenuItem(it)}). + collect(Collectors.toList()) + if (items.isEmpty()) + return + + popupMenu = new JPopupMenu() + items.each {popupMenu.add(it)} + + popupMenu.addMenuKeyListener(new MenuKeyListener() { + @Override + void menuKeyTyped(MenuKeyEvent e) { + char keyChar = e.getKeyChar() + if (((int)keyChar) == ENTER) + return + if (keyChar == BACKSPACE) { + backspaceAction.actionPerformed(new ActionEvent(ChatEntryPane.this, 0, null)) + SwingUtilities.invokeLater { + showPopupMenu(true) + } + return + } + Document document = getDocument() + int position = getCaret().getDot() + document.insertString(position, "${e.getKeyChar()}", null) + SwingUtilities.invokeLater { + showPopupMenu(true) + } + } + + @Override + void menuKeyPressed(MenuKeyEvent e) { + } + + @Override + void menuKeyReleased(MenuKeyEvent e) { + } + }) + + popupMenu.show(lastComponent, (int)lastPoint.getX(), (int)lastPoint.getY()) + } + + private static String justName(PersonaOrProfile pop) { + String name = pop.getPersona().getHumanReadableName() + name.substring(0, name.indexOf("@")) + } + + private class MemberAction extends AbstractAction { + private final PersonaOrProfile personaOrProfile + MemberAction(PersonaOrProfile personaOrProfile) { + this.personaOrProfile = personaOrProfile + putValue(SMALL_ICON, personaOrProfile.getThumbnail()) + putValue(NAME, personaOrProfile.getPersona().getHumanReadableName()) + } + + @Override + void actionPerformed(ActionEvent e) { + final int position = getCaret().getDot() + int startPosition = position + while(startPosition > 0) { + if (getText(startPosition - 1, 1) == "@") + break + startPosition-- + } + startPosition = Math.max(startPosition, 0) + setSelectionStart(startPosition) + setSelectionEnd(position) + replaceSelection("") + + final String name = personaOrProfile.getPersona().getHumanReadableName() + + StyledDocument document = getStyledDocument() + def popLabel = new POPLabel(personaOrProfile, settings) + popLabel.setMaximumSize([200, ProfileConstants.MAX_THUMBNAIL_SIZE] as Dimension) + Style style = document.addStyle("newStyle", null) + StyleConstants.setComponent(style, popLabel) + document.insertString(startPosition, name, style) + + popupMenu?.setVisible(false) + popupMenu = null + } + } + + private class BackspaceAction extends UIAction { + private final Action delegate + BackspaceAction(Action delegate) { + super("backspace") + this.delegate = delegate + } + + @Override + void actionPerformed(ActionEvent e) { + StyledDocument document = getStyledDocument() + int position = getCaret().getDot() - 1 + Element element = document.getCharacterElement(position) + if (element.getAttributes().getAttribute(StyleConstants.ComponentAttribute) == null) { + delegate.actionPerformed(e) + return + } + while(position > 0) { + element = document.getCharacterElement(position) + if (element.getAttributes().getAttribute(StyleConstants.ComponentAttribute) == null) + break + delegate.actionPerformed(e) + position-- + } + } + } +} diff --git a/gui/src/main/groovy/com/muwire/gui/chat/ChatTextField.groovy b/gui/src/main/groovy/com/muwire/gui/chat/ChatTextField.groovy deleted file mode 100644 index c539ac65..00000000 --- a/gui/src/main/groovy/com/muwire/gui/chat/ChatTextField.groovy +++ /dev/null @@ -1,32 +0,0 @@ -package com.muwire.gui.chat - -import javax.swing.JTextField -import javax.swing.text.AttributeSet -import javax.swing.text.BadLocationException -import javax.swing.text.Document -import javax.swing.text.PlainDocument - -class ChatTextField extends JTextField { - ChatTextField() { - setEditable(true) - } - - protected Document createDefaultModel() { - return new ChatDocument() - } - - private static class ChatDocument extends PlainDocument { - - @Override - void insertString(int offs, String str, AttributeSet a) throws BadLocationException { - getDocumentProperties().put("filterNewlines", false) - super.insertString(offs, str, a) - } - - @Override - String getText(int offset, int length) throws BadLocationException { - String rv = super.getText(offset, length) - return rv - } - } -}