/**************************************************************************
 OmegaT - Computer Assisted Translation (CAT) tool
          with fuzzy matching, translation memory, keyword search,
          glossaries, and translation leveraging into updated projects.

 Copyright (C) 2009 Didier Briel
               2010 Wildrich Fourie
               2011 Alex Buloichik, Didier Briel
               2012 Thomas Cordonnier
               2015 Aaron Madlon-Kay, Thomas Cordonnier
               2017 Thomas Cordonnier
               Home page: http://www.omegat.org/
               Support center: http://groups.yahoo.com/group/OmegaT/

 This file is part of OmegaT.

 OmegaT is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 OmegaT is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 **************************************************************************/

package org.omegat.gui.editor;

import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.lang.reflect.Constructor;

import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;

import org.omegat.core.Core;
import org.omegat.core.data.SourceTextEntry;
import org.omegat.core.data.RealProject;
import org.omegat.core.spellchecker.SpellCheckerMarker;
import org.omegat.filehooks.IFileHook;
import org.omegat.core.segmentation.SRX;
import org.omegat.core.segmentation.MapRule;
import org.omegat.core.segmentation.Rule;
import org.omegat.core.segmentation.LanguageCodes;
import org.omegat.tokenizer.ITokenizer.StemmingMode;
import org.omegat.util.Log;
import org.omegat.util.OStrings;
import org.omegat.util.StaticUtils;
import org.omegat.util.StringUtil;
import org.omegat.util.TagUtil;
import org.omegat.util.TagUtil.Tag;
import org.omegat.util.Token;
import org.omegat.util.gui.UIThreadsUtil;
import org.omegat.util.PatternConsts;
import org.omegat.util.Preferences;
import org.omegat.filters2.master.PluginUtils;

/**
 * Some standard editor popups.
 * 
 * @author Alex Buloichik (alex73mail@gmail.com)
 * @author Wildrich Fourie
 * @author Didier Briel
 * @author Aaron Madlon-Kay
 * @author Thomas Cordonnier
 */
public class EditorPopups {
    public static void init(EditorController ec) {
        ec.registerPopupMenuConstructors(100, new SpellCheckerPopup(ec));
        ec.registerPopupMenuConstructors(200, new GoToSegmentPopup(ec));
        ec.registerPopupMenuConstructors(400, new DefaultPopup());
        ec.registerPopupMenuConstructors(450, new SegmentationPopup());
        ec.registerPopupMenuConstructors(500, new DuplicateSegmentsPopup(ec));
        ec.registerPopupMenuConstructors(600, new EmptyNoneTranslationPopup(ec));
        ec.registerPopupMenuConstructors(700, new InsertTagsPopup());
        ec.registerPopupMenuConstructors(700, new InsertBidiPopup(ec));

        int i = 0;
        for (Class<? extends IPopupMenuConstructor> plugin0: PluginUtils.getEditorPopupClasses()) 
            try {
                Constructor<? extends IPopupMenuConstructor> cons = plugin0.getDeclaredConstructor(EditorController.class);
                IPopupMenuConstructor instance = cons.newInstance (ec);
                ec.registerPopupMenuConstructors(700 + i * 100, instance); i++;
            } catch (Exception e) {
                e.printStackTrace();
            }
    }

    private static interface IMenuReceiver {
        public JMenuItem addItem (String label, ActionListener listener);
    }
    
    private static class JSubMenuReceiver implements IMenuReceiver {
        private JMenu menu;
        public JSubMenuReceiver (JMenu menu) { this.menu = menu; }
        public JMenuItem addItem (String label, ActionListener listener) {
            JMenuItem item = menu.add (label);
            item.addActionListener (listener);
            return item;
        }
    }
    
    private static class JPopupMenuReceiver implements IMenuReceiver {
        private JPopupMenu menu;
        public JPopupMenuReceiver (JPopupMenu menu) { this.menu = menu; }
        public JMenuItem addItem (String label, ActionListener listener) {
            JMenuItem item = menu.add (label);
            item.addActionListener (listener);
            return item;
        }
    }
    
    private static IMenuReceiver createReceiver (JPopupMenu popup, int size, String label) {
        if (size <= 5) return new JPopupMenuReceiver(popup);
        else {
            JMenu subMenu = new JMenu (label);
            popup.add (subMenu); return new JSubMenuReceiver(subMenu);
        }
    }

    /**
     * create the spell checker popup menu - suggestions for a wrong word, add
     * and ignore. Works only for the active segment, for the translation
     * 
     * @param point
     *            : where should the popup be shown
     */
    public static class SpellCheckerPopup implements IPopupMenuConstructor {
        protected final EditorController ec;

        public SpellCheckerPopup(EditorController ec) {
            this.ec = ec;
        }

        public void addItems(JPopupMenu menu, final JTextComponent comp, int mousepos,
                boolean isInActiveEntry, boolean isInActiveTranslation, SegmentBuilder sb) {
            if (!ec.getSettings().isAutoSpellChecking()) {
                // spellchecker disabled
                return;
            }
            if (!isInActiveTranslation) {
                // there is no need to display suggestions
                return;
            }

            // Use the project's target tokenizer to determine the word that was right-clicked.
            // EditorUtils.getWordEnd() and getWordStart() use Java's built-in BreakIterator
            // under the hood, which leads to inconsistent results when compared to other spell-
            // checking functionality in OmegaT.
            String translation = ec.getCurrentTranslation();
            Token tok = null;
            int relOffset = ec.getPositionInEntryTranslation(mousepos);
            for (Token t : Core.getProject().getTargetTokenizer().tokenizeWords(translation, StemmingMode.NONE)) {
                if (t.getOffset() <= relOffset && relOffset < t.getOffset() + t.getLength()) {
                    tok = t;
                    break;
                }
            }
            
            if (tok == null) {
                return;
            }
            
            final String word = tok.getTextFromString(translation);
            // The wordStart must be the absolute offset in the Editor document.
            final int wordStart = mousepos - relOffset + tok.getOffset();
            final int wordLength = tok.getLength();
            final AbstractDocument xlDoc = (AbstractDocument) comp.getDocument();

            if (!Core.getSpellChecker().isCorrect(word)) {
                // get the suggestions and create a menu
                List<String> suggestions = Core.getSpellChecker().suggest(word);
                
                IMenuReceiver receiver = createReceiver(menu, suggestions.size(), "Suggestions");

                // the suggestions
                    for (final String replacement : suggestions)
                        receiver.addItem(replacement, new ActionListener() {
                        // the action: replace the word with the selected
                        // suggestion
                        public void actionPerformed(ActionEvent e) {
                            try {
                                int pos = comp.getCaretPosition();
                                xlDoc.replace(wordStart, wordLength, replacement, null);
                                comp.setCaretPosition(pos);
                            } catch (BadLocationException exc) {
                                Log.log(exc);
                            }
                        }
                    });
                

                // what if no action is done?
                if (suggestions.isEmpty()) {
                    JMenuItem item = menu.add(OStrings.getString("SC_NO_SUGGESTIONS"));
                    item.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
                            // just hide the menu
                        }
                    });
                }

                menu.addSeparator();

                // let us ignore it
                JMenuItem item = menu.add(OStrings.getString("SC_IGNORE_ALL"));
                item.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        addIgnoreWord(word, wordStart, false);
                    }
                });

                // or add it to the dictionary
                item = menu.add(OStrings.getString("SC_ADD_TO_DICTIONARY"));
                item.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        addIgnoreWord(word, wordStart, true);
                    }
                });
                
                menu.addSeparator();

            }
        }

        /**
         * add a new word to the spell checker or ignore a word
         * 
         * @param word
         *            : the word in question
         * @param offset
         *            : the offset of the word in the editor
         * @param add
         *            : true for add, false for ignore
         */
        protected void addIgnoreWord(final String word, final int offset, final boolean add) {
            UIThreadsUtil.mustBeSwingThread();

            if (add) {
                Core.getSpellChecker().learnWord(word);
            } else {
                Core.getSpellChecker().ignoreWord(word);
            }

            ec.remarkOneMarker(SpellCheckerMarker.class.getName());
        }
    }

    public static class DefaultPopup implements IPopupMenuConstructor {
        /**
         * Creates the Cut, Copy and Paste menu items
         */
        public void addItems(JPopupMenu menu, final JTextComponent comp, int mousepos,
                boolean isInActiveEntry, boolean isInActiveTranslation, SegmentBuilder sb) {
            final String selText = comp.getSelectedText();
            final Clipboard omClipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
            Transferable contents = omClipboard.getContents(this);

            boolean cutEnabled = false;
            boolean copyEnabled = false;
            boolean pasteEnabled = false;

            // Calc enabled/disabled
            if (selText != null && comp.getSelectionStart() <= mousepos && mousepos <= comp.getSelectionEnd()) {
                // only on selected text
                if (isInActiveTranslation) {
                    // cut only in editable zone
                    cutEnabled = true;
                }
                // copy in any place
                copyEnabled = true;
            }
            if (contents != null && isInActiveTranslation) {
                pasteEnabled = true;
            }

            // Cut
            JMenuItem cutContextItem = menu.add(OStrings.getString("CCP_CUT"));
            if (cutEnabled) {
                cutContextItem.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        comp.cut();
                    }
                });
            } else {
                cutContextItem.setEnabled(false);
            }

            // Copy
            JMenuItem copyContextItem = menu.add(OStrings.getString("CCP_COPY"));
            if (copyEnabled) {
                copyContextItem.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        comp.copy();
                    }
                });
            } else {
                copyContextItem.setEnabled(false);
            }

            // Paste
            JMenuItem pasteContextItem = menu.add(OStrings.getString("CCP_PASTE"));
            if (pasteEnabled) {
                pasteContextItem.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        comp.paste();
                    }
                });
            } else {
                pasteContextItem.setEnabled(false);
            }

            menu.addSeparator();
            
			// Add glossary entry
			JMenuItem item = menu.add(OStrings.getString("GUI_GLOSSARYWINDOW_addentry"));
            item.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                Core.getGlossary().showCreateGlossaryEntryDialog();
                 }
            });

            menu.addSeparator();

        }
    }

    public static class SegmentationPopup implements IPopupMenuConstructor {
        /** Creates split/merge menu items */
        public void addItems(JPopupMenu menu, final JTextComponent comp, int mousepos,
                boolean isInActiveEntry, boolean isInActiveTranslation, SegmentBuilder sb) {
            if (isInActiveTranslation) return; // do not cut inside translation! 
            if (! isInActiveEntry) return; // may be possible, but for the moment causes errors.
			
            menu.add(OStrings.getString("SEG_CUT")).addActionListener(e -> { 
                String before = sb.getSourceText().substring(0, mousepos - sb.getStartSourcePosition());
                String after = sb.getSourceText().substring(mousepos - sb.getStartSourcePosition());				
                addRule(true, before, after);
            });
            
            final int segId = ((EditorController) Core.getEditor()).getSegmentIndexAtLocation(mousepos);
            final SourceTextEntry curSte = Core.getProject().getAllEntries().get (segId);
            
            if (! curSte.isParagraphStart())
                menu.add(OStrings.getString("SEG_MERGE_PREV")).addActionListener(e -> { 
                    String before = Core.getProject().getAllEntries().get (segId - 1).getSrcText();
                    String after = curSte.getSrcText();
                    addRule(false, before,  "#OptSpace#" + after);
                });
			
            if (segId < Core.getProject().getAllEntries().size() - 1) {
                final SourceTextEntry nextSte = Core.getProject().getAllEntries().get (segId + 1);
                if (! nextSte.isParagraphStart())
                    menu.add(OStrings.getString("SEG_MERGE_NEXT")).addActionListener(e -> { 
                        String before = curSte.getSrcText(), after = nextSte.getSrcText();
                        addRule(false, before,  "#OptSpace#" + after);			
                    });
			}

            menu.addSeparator();
        }
        
        private static void addRule (boolean isBreak, String before, String after) {
            if (before.contains(">")) before = before.substring(before.lastIndexOf(">") + 1);
            if (before.length() == 0) { javax.swing.JOptionPane.showMessageDialog(null, OStrings.getString("POPUP_SEGMENTATION_MSG_ERR_SPLIT_BEGIN"), OStrings.getString("TF_WARNING"), javax.swing.JOptionPane.WARNING_MESSAGE); return; }
            if (after.contains("<")) after = after.substring(0, after.indexOf("<"));
            if (after.length() == 0) { javax.swing.JOptionPane.showMessageDialog(null, OStrings.getString("POPUP_SEGMENTATION_MSG_ERR_SPLIT_END"), OStrings.getString("TF_WARNING"), javax.swing.JOptionPane.WARNING_MESSAGE); return; }
            if (after.startsWith(">")) { javax.swing.JOptionPane.showMessageDialog(null, OStrings.getString("POPUP_SEGMENTATION_MSG_ERR_SPLIT_INSIDE"), OStrings.getString("TF_WARNING"), javax.swing.JOptionPane.WARNING_MESSAGE); return; }			
            
            javax.swing.JDialog dial = new javax.swing.JDialog ((java.awt.Frame) Core.getMainWindow(), true);
            dial.setLayout (new javax.swing.BoxLayout(dial.getContentPane(), javax.swing.BoxLayout.Y_AXIS));
            javax.swing.JPanel LINE_FIELDS = new javax.swing.JPanel(), LINE_EMPTY = new javax.swing.JPanel(), LINE_BTN = new javax.swing.JPanel();
            dial.add(LINE_FIELDS); dial.add(LINE_EMPTY); dial.add(LINE_BTN);
            final javax.swing.JLabel labelText = new javax.swing.JLabel("<html>" + before + "<font color=red>*</font>" + after + "</html>"); LINE_EMPTY.add (labelText);
            LINE_BTN.setLayout (new java.awt.FlowLayout(java.awt.FlowLayout.RIGHT));
            LINE_FIELDS.setLayout (new javax.swing.BoxLayout(LINE_FIELDS,javax.swing.BoxLayout.X_AXIS));
            javax.swing.Box COL_LABEL = new javax.swing.Box(javax.swing.BoxLayout.Y_AXIS), COL_FIELD = new javax.swing.Box(javax.swing.BoxLayout.Y_AXIS), 
                COL_BTN1 = new javax.swing.Box(javax.swing.BoxLayout.Y_AXIS), COL_BTN2 = new javax.swing.Box(javax.swing.BoxLayout.Y_AXIS), COL_BTN3 = new javax.swing.Box(javax.swing.BoxLayout.Y_AXIS);
            LINE_FIELDS.add(COL_LABEL); LINE_FIELDS.add(COL_FIELD); LINE_FIELDS.add(COL_BTN1); LINE_FIELDS.add(COL_BTN2); LINE_FIELDS.add(COL_BTN3);
			
            before = StaticUtils.escapeNonRegex(before,null); after = StaticUtils.escapeNonRegex(after,null);
            before = before.replaceAll("\\s", "\\\\s"); after = after.replaceAll("\\s", "\\\\s");
			after = after.replace("#OptSpace#", "\\s*"); // after all substitutions, so that it is not affected
            
            final javax.swing.JTextField fBefore = new javax.swing.JTextField(before), fAfter = new javax.swing.JTextField(after);
			javax.swing.JButton btnMoveUp, btnMoveDown, btnBeforeRemoveFirst, btnBeforeKeepOnlyLast, btnAfterRemoveLast, btnAfterKeepOnlyFirst;
            COL_LABEL.add (new javax.swing.JLabel(OStrings.getString("CORE_SRX_TABLE_COLUMN_Before_Break"))); COL_FIELD.add (fBefore);
            COL_BTN1.add (btnMoveDown = new javax.swing.JButton()); org.openide.awt.Mnemonics.setLocalizedText(btnMoveDown, "\u2199 " + OStrings.getString("PF_MOVE_DOWN"));
			btnMoveDown.addActionListener (ev -> {
                Matcher m = Pattern.compile ("\\\\?.\\z").matcher (fBefore.getText());
                if (m.find()) { fBefore.setText(fBefore.getText().substring(0, m.start())); fAfter.setText(m.group() + fAfter.getText()); }
			});
            COL_BTN2.add (btnBeforeRemoveFirst = new javax.swing.JButton(OStrings.getString("POPUP_SEGMENTATION_BTN_REMOVE_FIRST")));
            btnBeforeRemoveFirst.addActionListener (ev -> {
                fBefore.setText(fBefore.getText().replaceFirst ("\\A(\\\\.|\\P{L})*\\p{L}+(\\\\.|\\P{L})*", "")); 
            });
            COL_BTN3.add (btnBeforeKeepOnlyLast = new javax.swing.JButton(OStrings.getString("POPUP_SEGMENTATION_BTN_KEEP_LAST")));
            btnBeforeKeepOnlyLast.addActionListener (ev -> {
                Matcher m = Pattern.compile ("\\p{L}+(\\\\.|\\P{L})*\\z").matcher(fBefore.getText());
                if (m.find()) fBefore.setText("\\b" + m.group()); 
            });
            COL_LABEL.add (new javax.swing.JLabel(OStrings.getString("CORE_SRX_TABLE_COLUMN_After_Break"))); COL_FIELD.add (fAfter);
            COL_BTN1.add (btnMoveUp = new javax.swing.JButton()); org.openide.awt.Mnemonics.setLocalizedText(btnMoveUp, "\u2197 " + OStrings.getString("PF_MOVE_UP"));
			btnMoveUp.addActionListener (ev -> {
                Matcher m = Pattern.compile ("\\A\\\\?.").matcher (fAfter.getText());
                if (m.find()) { fAfter.setText(fAfter.getText().substring(m.end())); fBefore.setText(fBefore.getText() + m.group()); }
			});
            COL_BTN2.add (btnAfterRemoveLast = new javax.swing.JButton(OStrings.getString("POPUP_SEGMENTATION_BTN_REMOVE_LAST")));
            btnAfterRemoveLast.addActionListener (ev -> {
                fAfter.setText(fAfter.getText().replaceAll("(\\\\.)*\\p{L}+(\\\\.|\\P{L})*\\z", ""));
            });
            COL_BTN3.add (btnAfterKeepOnlyFirst = new javax.swing.JButton(OStrings.getString("POPUP_SEGMENTATION_BTN_KEEP_FIRST")));
            btnAfterKeepOnlyFirst.addActionListener (ev -> { 
                Matcher m = Pattern.compile ("\\A(\\\\.|\\P{L})*\\p{L}+(\\\\.|\\P{L})*").matcher(fAfter.getText());
                if (m.find()) fAfter.setText(m.group());
            });
			javax.swing.event.DocumentListener refresh = new javax.swing.event.DocumentListener() {
				public void changedUpdate(javax.swing.event.DocumentEvent documentEvent) {
					
				}
				public void insertUpdate(javax.swing.event.DocumentEvent documentEvent) {
					refreshLabel(labelText, fBefore, fAfter);
				}
				public void removeUpdate(javax.swing.event.DocumentEvent documentEvent) {
					refreshLabel(labelText, fBefore, fAfter);
				}
			};
			fBefore.getDocument().addDocumentListener(refresh); fAfter.getDocument().addDocumentListener(refresh);
            fBefore.getDocument().addDocumentListener(new org.omegat.util.gui.RegexHighlightListener(fBefore, org.omegat.util.gui.RegexHighlightListener.MODE_REGEX_FIND));
            fAfter.getDocument().addDocumentListener(new org.omegat.util.gui.RegexHighlightListener(fAfter, org.omegat.util.gui.RegexHighlightListener.MODE_REGEX_FIND));
            javax.swing.JButton btnOk = new javax.swing.JButton(), btnCancel = new javax.swing.JButton();
            LINE_BTN.add (btnOk); LINE_BTN.add (btnCancel);
			org.openide.awt.Mnemonics.setLocalizedText(btnOk, OStrings.getString("BUTTON_OK"));
			org.openide.awt.Mnemonics.setLocalizedText(btnCancel, OStrings.getString("BUTTON_CANCEL"));
            btnCancel.addActionListener (e -> dial.dispose());
            btnOk.addActionListener (ev -> {
                SRX current = ((RealProject) Core.getProject()).getProjectProperties().getProjectSRX(); if (current == null) current = Preferences.getSRX();
                List<MapRule> mapRules = current.getMappingRules();
                MapRule mapRule0 = mapRules.get(0); 
                if (! mapRule0.getLanguageCode().equals(LanguageCodes.F_SPECIFIC_KEY))
                    mapRules.add (0, mapRule0 = new MapRule(LanguageCodes.F_SPECIFIC_KEY, ".*", new java.util.ArrayList<Rule>())); // add in the beginning, not in the end!
                List<Rule> rules0 = mapRule0.getRules();
                rules0.add (0, new Rule(isBreak, fBefore.getText(), fAfter.getText())); // add in the beginning, not in the end!
                try {
                    SRX.saveTo (current, new java.io.File(Core.getProject().getProjectProperties().getProjectInternal()));
					((RealProject) Core.getProject()).getProjectProperties().setProjectSRX(current);	// else, will be lost during reload
                    org.omegat.gui.main.ProjectUICommands.projectReload();
                } catch (Exception ex) {
                    javax.swing.JOptionPane.showMessageDialog(null, ex.getMessage(), OStrings.getString("TF_ERROR"), javax.swing.JOptionPane.ERROR_MESSAGE);
                }
                dial.dispose();
            });
            java.awt.Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
            dial.pack(); dial.setLocation((screenSize.width - dial.getWidth()) / 2, (screenSize.height - dial.getHeight()) /2); dial.setVisible(true);
        }
        
        private static void refreshLabel (javax.swing.JLabel labelText, javax.swing.JTextField fBefore, javax.swing.JTextField fAfter) {
			String beforeText = fBefore.getText().replaceAll("\\\\s", " ").replaceAll("\\\\(.)", "$1");
			String afterText = fAfter.getText().replaceAll("\\\\s", " ").replaceAll("\\\\(.)", "$1");
			labelText.setText ("<html>" + beforeText + "<font color=red>*</font>" + afterText + "</html>");
        }
    }
	
    public static class GoToSegmentPopup implements IPopupMenuConstructor {
        protected final EditorController ec;

        public GoToSegmentPopup(EditorController ec) {
            this.ec = ec;
        }

        /**
         * creates a popup menu for inactive segments - with an item allowing to
         * go to the given segment.
         */
        public void addItems(JPopupMenu menu, final JTextComponent comp, final int mousepos,
                boolean isInActiveEntry, boolean isInActiveTranslation, SegmentBuilder sb) {
            if (isInActiveEntry) {
                return;
            }

            JMenuItem item = menu.add(OStrings.getString("MW_PROMPT_SEG_NR_TITLE"));
            item.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    comp.setCaretPosition(mousepos);
                    ec.goToSegmentAtLocation(comp.getCaretPosition());
                }
            });
            menu.addSeparator();
        }
    }
    
    public static class DuplicateSegmentsPopup implements IPopupMenuConstructor {
        protected final EditorController ec;
        private static int MAX_ITEMS_PER_PAGE = 10;
        
        public DuplicateSegmentsPopup(EditorController ec) {
            this.ec = ec;
        }
        
        @Override
        public void addItems(JPopupMenu menu, JTextComponent comp,
                int mousepos, boolean isInActiveEntry,
                boolean isInActiveTranslation, SegmentBuilder sb) {
            if (!isInActiveEntry) {
                return;
            }
            SourceTextEntry ste = ec.getCurrentEntry();
            List<SourceTextEntry> dups = ste.getDuplicates();
            if (dups.isEmpty()) {
                return;
            }
            JMenuItem header = menu.add(StringUtil.format(OStrings.getString("MW_GO_TO_DUPLICATE_HEADER"), dups.size()));
            header.setEnabled(false);
            JMenu submenu = null;
            for (int i = 0; i < dups.size(); i++) {
                final int entryNum = dups.get(i).entryNum();
                // Put the first n items in the main popup, but put every subsequent batch of n in
                // nested submenus to prevent any single menu from getting too tall.
                if (i > 0 && i % MAX_ITEMS_PER_PAGE == 0) {
                    JMenu newSubmenu = new JMenu(OStrings.getString("MW_MORE_SUBMENU"));
                    if (submenu == null) {
                        menu.add(newSubmenu);
                    } else {
                        submenu.add(newSubmenu);
                    }
                    submenu = newSubmenu;
                }
                String label = StringUtil.format(OStrings.getString("MW_GO_TO_DUPLICATE_ITEM"), entryNum);
                JMenuItem item = submenu == null ? menu.add(label) : submenu.add(label);
                item.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        ec.gotoEntry(entryNum);
                    }
                });
            }
            menu.addSeparator();
        }
    }

    public static class EmptyNoneTranslationPopup implements IPopupMenuConstructor {
        protected final EditorController ec;

        public EmptyNoneTranslationPopup(EditorController ec) {
            this.ec = ec;
        }

        /**
         * creates a popup menu to remove translation or set empty translation
         */
        public void addItems(JPopupMenu menu, final JTextComponent comp, final int mousepos,
                boolean isInActiveEntry, boolean isInActiveTranslation, SegmentBuilder sb) {
            if (!isInActiveEntry) {
                return;
            }

            menu.add(OStrings.getString("TRANS_POP_EMPTY_TRANSLATION")).addActionListener(
                    new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
                            ec.registerEmptyTranslation();
                        }
                    });
            menu.add(OStrings.getString("TRANS_POP_REMOVE_TRANSLATION")).addActionListener(
                    new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
                            ec.registerUntranslated();
                        }
                    });
            menu.add(OStrings.getString("TRANS_POP_IDENTICAL_TRANSLATION")).addActionListener(
                    new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
                            ec.registerIdenticalTranslation();
                        }
                    });
            menu.addSeparator();
        }
    }

    public static class InsertTagsPopup implements IPopupMenuConstructor {
        private static final char START_ITALICS = '\u2460', END_ITALICS = '\u2776';
        private static final String[] names = { "IT","BOLD","UL","SUP","SUB" };
        
        public void addItems(JPopupMenu menu, final JTextComponent comp, final int mousepos, boolean isInActiveEntry,
                boolean isInActiveTranslation, SegmentBuilder sb) {
            if ((sb != null) && (!isInActiveTranslation)) {
                return;
            }

            if (comp instanceof EditorTextArea3) {
				List<Tag> allTags = TagUtil.getAllTagsMissingFromTarget();
				IMenuReceiver receiver = createReceiver(menu, allTags.size(), StringUtil.format(OStrings.getString("TF_MENU_EDIT_TAG_INSERT_N"), ""));
				for (final Tag tag0 : allTags)
					receiver.addItem(StringUtil.format(OStrings.getString("TF_MENU_EDIT_TAG_INSERT_N"), tag0.tag), 
						e -> Core.getEditor().insertTag(tag0.tag));
			}
            
            if (IFileHook.hookForFile(Core.getEditor().getCurrentFile()).supportsPseudoTags()) {    // even if used inside search window, let editor decide if supported or not
                JMenu format = new JMenu (OStrings.getString("EDITOR_PSEUDOTAGS_MAIN")); menu.add (format);
                for (int i = 0; i <=4; i++)
                    if (comp.getSelectedText() == null) { 
                        JMenuItem item = format.add(StringUtil.format(OStrings.getString("EDITOR_PSEUDOTAGS_START"), OStrings.getString("EDITOR_PSEUDOTAGS_" + names[i])) + (char) (START_ITALICS + i));
                        final String tagStart = "" + (char) (START_ITALICS + i);
					    item.addActionListener(createInsertActionListener(comp, tagStart));
                        item = format.add(StringUtil.format(OStrings.getString("EDITOR_PSEUDOTAGS_END"), OStrings.getString("EDITOR_PSEUDOTAGS_" + names[i])) + (char) (END_ITALICS + i));
                        final String tagEnd = "" + (char) (END_ITALICS + i);
                        item.addActionListener(createInsertActionListener(comp, tagEnd));
                    } else {
                        String text = comp.getSelectedText();
                        if (text.startsWith("" + (char)(START_ITALICS + i)) && text.endsWith("" + (char)(END_ITALICS + i))) {
                            text = text.substring(1); text = text.substring(0, text.length() - 1);
                            String label = (text.length() < 50) ? text : (text.substring(0,40) + "...");
                            JMenuItem item = format.add(StringUtil.format(OStrings.getString("EDITOR_PSEUDOTAGS_UNSET"), OStrings.getString("EDITOR_PSEUDOTAGS_" + names[i])) + label);
                            final String text1 = text;
                            item.addActionListener(createInsertActionListener(comp, text1));
                        } else {
                            String label = (text.length() < 50) ? ("" + (char)(START_ITALICS + i) + text + (char)(END_ITALICS + i)) : ("" + (char)(START_ITALICS+i) + "..." + (char)(END_ITALICS+i));
                            JMenuItem item = format.add(StringUtil.format(OStrings.getString("EDITOR_PSEUDOTAGS_SET"), OStrings.getString("EDITOR_PSEUDOTAGS_" + names[i])) + label);
                            final String text1 = "" + (char)(START_ITALICS+i) + text + (char)(END_ITALICS+i);
                            item.addActionListener(createInsertActionListener(comp, text1));				
                        }
                    }			
			}
            
            menu.addSeparator();
        }
		
		private ActionListener createInsertActionListener (JTextComponent comp, String text) {
			if (comp instanceof EditorTextArea3) return e -> Core.getEditor().insertTag(text); // with bidi marks
			else return e -> comp.replaceSelection (text);
		}
    }

    public static class InsertBidiPopup implements IPopupMenuConstructor {
        protected final EditorController ec;
        protected String[] names = new String[] { "TF_MENU_EDIT_INSERT_CHARS_LRM",
                "TF_MENU_EDIT_INSERT_CHARS_RLM", "TF_MENU_EDIT_INSERT_CHARS_LRE",
                "TF_MENU_EDIT_INSERT_CHARS_RLE", "TF_MENU_EDIT_INSERT_CHARS_PDF" };
        protected String[] inserts = new String[] { "\u200E", "\u200F", "\u202A", "\u202B", "\u202C" };

        public InsertBidiPopup(EditorController ec) {
            this.ec = ec;
        }

        public void addItems(JPopupMenu menu, final JTextComponent comp, final int mousepos,
                boolean isInActiveEntry, boolean isInActiveTranslation, SegmentBuilder sb) {
            if (!isInActiveTranslation) {
                return;
            }
            JMenu submenu = new JMenu(OStrings.getString("TF_MENU_EDIT_INSERT_CHARS"));
            for (int i = 0; i < names.length; i++) {
                JMenuItem item = new JMenuItem(OStrings.getString(names[i]));
                final String insertText = inserts[i];
                item.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        Core.getEditor().insertText(insertText);
                    }
                });
                submenu.add(item);
            }
            menu.add(submenu);
        }
    }
}
