/* ****************************************************************************** * Copyright (c) 2006-2012 XMind Ltd. and others. * * This file is a part of XMind 3. XMind releases 3 and * above are dual-licensed under the Eclipse Public License (EPL), * which is available at http://www.eclipse.org/legal/epl-v10.html * and the GNU Lesser General Public License (LGPL), * which is available at http://www.gnu.org/licenses/lgpl.html * See http://www.xmind.net/license.html for details. * * Contributors: * XMind Ltd. - initial API and implementation *******************************************************************************/ package org.xmind.ui.internal.notes; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import org.eclipse.draw2d.geometry.Rectangle; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.IToolBarManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.bindings.Trigger; import org.eclipse.jface.bindings.TriggerSequence; import org.eclipse.jface.bindings.keys.KeyStroke; import org.eclipse.jface.bindings.keys.SWTKeySupport; import org.eclipse.jface.dialogs.IDialogSettings; import org.eclipse.jface.dialogs.PopupDialog; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocumentListener; import org.eclipse.jface.text.ITextOperationTarget; import org.eclipse.jface.text.TextViewer; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchCommandConstants; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.actions.ActionFactory; import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction; import org.eclipse.ui.contexts.IContextActivation; import org.eclipse.ui.contexts.IContextService; import org.eclipse.ui.keys.IBindingService; import org.xmind.core.Core; import org.xmind.core.INotes; import org.xmind.core.INotesContent; import org.xmind.core.ITopic; import org.xmind.core.IWorkbook; import org.xmind.core.event.ICoreEventListener; import org.xmind.core.event.ICoreEventRegistration; import org.xmind.core.event.ICoreEventSource2; import org.xmind.gef.EditDomain; import org.xmind.gef.IGraphicalViewer; import org.xmind.gef.IViewer; import org.xmind.gef.ZoomManager; import org.xmind.gef.command.CompoundCommand; import org.xmind.gef.command.ICommandStack; import org.xmind.ui.commands.CommandMessages; import org.xmind.ui.commands.ModifyNotesCommand; import org.xmind.ui.internal.MindMapMessages; import org.xmind.ui.internal.MindMapUIPlugin; import org.xmind.ui.internal.dialogs.DialogMessages; import org.xmind.ui.internal.e4models.IModelConstants; import org.xmind.ui.internal.editor.MindMapEditor; import org.xmind.ui.internal.spellsupport.SpellingSupport; import org.xmind.ui.internal.utils.E4Utils; import org.xmind.ui.mindmap.ITopicPart; import org.xmind.ui.mindmap.MindMapUI; import org.xmind.ui.richtext.Hyperlink; import org.xmind.ui.richtext.IRichDocument; import org.xmind.ui.richtext.IRichDocumentListener; import org.xmind.ui.richtext.IRichTextAction; import org.xmind.ui.richtext.IRichTextEditViewer; import org.xmind.ui.richtext.ImagePlaceHolder; import org.xmind.ui.richtext.LineStyle; import org.xmind.ui.richtext.SimpleRichTextActionBarContributor; import org.xmind.ui.richtext.TextActionConstants; import org.xmind.ui.texteditor.IMenuContributor; import org.xmind.ui.texteditor.ISpellingActivation; public class NotesPopup extends PopupDialog implements IDocumentListener, IRichDocumentListener, ISelectionChangedListener { private static final String CONTEXT_ID = "org.xmind.ui.context.notesPopup"; //$NON-NLS-1$ private static final String CMD_GOTO_NOTES_VIEW = "org.xmind.ui.command.gotoNotesView"; //$NON-NLS-1$ private static final String CMD_COMMIT_NOTES = "org.xmind.ui.command.commitNotes"; //$NON-NLS-1$ private class TextAction extends Action { private int op; private TextViewer textViewer; public TextAction(int op) { this.op = op; } public void run() { if (textViewer == null) { textViewer = notesViewer.getImplementation().getTextViewer(); } if (textViewer != null) { if (textViewer.canDoOperation(op)) { textViewer.doOperation(op); } } } public void update(TextViewer textViewer) { setEnabled(textViewer.canDoOperation(op)); } } private class NotesPopupActionBarContributor extends SimpleRichTextActionBarContributor { private Map<String, TextAction> textActions = new HashMap<String, TextAction>( 10); private Map<String, IAction> actionHandlers = new HashMap<String, IAction>( 10); private Collection<String> textCommandIds = new HashSet<String>(10); private class GotoNotesPartAction extends Action { public GotoNotesPartAction() { super(MindMapMessages.EditInNotesView_text); setToolTipText(MindMapMessages.EditInNotesView_toolTip); setImageDescriptor( MindMapUI.getImages().get("notes_part.png", true)); //$NON-NLS-1$ setDisabledImageDescriptor( MindMapUI.getImages().get("notes_part.png", false)); //$NON-NLS-1$ } public void run() { gotoNotesPart(); } } protected void makeActions(IRichTextEditViewer viewer) { super.makeActions(viewer); addWorkbenchAction(ActionFactory.UNDO, ITextOperationTarget.UNDO); addWorkbenchAction(ActionFactory.REDO, ITextOperationTarget.REDO); addWorkbenchAction(ActionFactory.CUT, ITextOperationTarget.CUT); addWorkbenchAction(ActionFactory.COPY, ITextOperationTarget.COPY); addWorkbenchAction(ActionFactory.PASTE, ITextOperationTarget.PASTE); addWorkbenchAction(ActionFactory.SELECT_ALL, ITextOperationTarget.SELECT_ALL); registerTextCommand(TextActionConstants.BOLD_ID, "org.xmind.ui.command.text.bold"); //$NON-NLS-1$ registerTextCommand(TextActionConstants.ITALIC_ID, "org.xmind.ui.command.text.italic"); //$NON-NLS-1$ registerTextCommand(TextActionConstants.UNDERLINE_ID, "org.xmind.ui.command.text.underline"); //$NON-NLS-1$ registerTextCommand(TextActionConstants.LEFT_ALIGN_ID, "org.xmind.ui.command.text.leftAlign"); //$NON-NLS-1$ registerTextCommand(TextActionConstants.CENTER_ALIGN_ID, "org.xmind.ui.command.text.centerAlign"); //$NON-NLS-1$ registerTextCommand(TextActionConstants.RIGHT_ALIGN_ID, "org.xmind.ui.command.text.rightAlign"); //$NON-NLS-1$ } private void addWorkbenchAction(ActionFactory factory, int textOp) { IWorkbenchAction action = factory.create(window); TextAction textAction = new TextAction(textOp); textAction.setId(action.getId()); textAction.setActionDefinitionId(action.getActionDefinitionId()); textAction.setText(action.getText()); textAction.setToolTipText(action.getToolTipText()); textAction.setDescription(action.getDescription()); textAction.setImageDescriptor(action.getImageDescriptor()); textAction.setDisabledImageDescriptor( action.getDisabledImageDescriptor()); textAction .setHoverImageDescriptor(action.getHoverImageDescriptor()); action.dispose(); actionHandlers.put(action.getActionDefinitionId(), textAction); textActions.put(textAction.getId(), textAction); } private void registerTextCommand(String actionId, String commandId) { IRichTextAction action = getRichTextAction(actionId); if (action != null) { action.setActionDefinitionId(commandId); actionHandlers.put(commandId, action); textCommandIds.add(commandId); } } public void fillToolBar(IToolBarManager toolbar) { super.fillToolBar(toolbar); if (showGotoNotesPart) { toolbar.add(new Separator()); toolbar.add(new GotoNotesPartAction()); } } public void fillContextMenu(IMenuManager menu) { menu.add(getTextAction(ActionFactory.UNDO.getId())); menu.add(getTextAction(ActionFactory.REDO.getId())); menu.add(new Separator()); menu.add(getTextAction(ActionFactory.CUT.getId())); menu.add(getTextAction(ActionFactory.COPY.getId())); menu.add(getTextAction(ActionFactory.PASTE.getId())); menu.add(new Separator()); menu.add(getTextAction(ActionFactory.SELECT_ALL.getId())); menu.add(new Separator()); super.fillContextMenu(menu); if (spellingActivation != null) { IMenuContributor contributor = (IMenuContributor) spellingActivation .getAdapter(IMenuContributor.class); if (contributor != null) { menu.add(new Separator()); contributor.fillMenu(menu); } } } @Override public void dispose() { actionHandlers.clear(); textActions.clear(); super.dispose(); } public void update(TextViewer textViewer) { for (TextAction action : textActions.values()) { action.update(textViewer); } } public IAction getActionHandler(String commandId) { return actionHandlers.get(commandId); } public IAction getTextAction(String actionId) { return textActions.get(actionId); } public Collection<String> getTextCommandIds() { return textCommandIds; } } private class PopupKeyboardListener implements Listener { private List<TriggerSequence> currentSequences = null; private int nextKeyIndex = -1; public void hook(Control control) { control.getDisplay().addFilter(SWT.KeyDown, this); control.getShell().addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent e) { if (!e.display.isDisposed()) { e.display.removeFilter(SWT.KeyDown, PopupKeyboardListener.this); } } }); } public void handleEvent(Event event) { if (event.type == SWT.KeyDown) { handleKeyDown(event); } update(); } private void handleKeyDown(Event event) { if (triggerableCommands.isEmpty()) return; List<KeyStroke> keys = generateKeyStrokes(event); if (currentSequences == null) { nextKeyIndex = -1; for (TriggerSequence ts : triggerableCommands.keySet()) { if (matches(keys, ts.getTriggers()[0])) { if (currentSequences == null) currentSequences = new ArrayList<TriggerSequence>( triggerableCommands.size()); currentSequences.add(ts); } } if (currentSequences == null) return; } if (nextKeyIndex < 0) nextKeyIndex = 0; Iterator<TriggerSequence> it = currentSequences.iterator(); while (it.hasNext()) { TriggerSequence ts = it.next(); Trigger[] triggers = ts.getTriggers(); if (nextKeyIndex >= triggers.length) { it.remove(); } else { if (matches(keys, triggers[nextKeyIndex])) { if (nextKeyIndex == triggers.length - 1) { if (triggerFound(ts)) { event.doit = false; } return; } } else { it.remove(); } } } if (currentSequences != null && currentSequences.isEmpty()) { nextKeyIndex++; } else { currentSequences = null; nextKeyIndex = -1; } } private boolean triggerFound(TriggerSequence triggerSequence) { currentSequences = null; nextKeyIndex = -1; String commandId = triggerableCommands.get(triggerSequence); if (commandId != null) { return handleCommand(commandId); } return false; } private boolean matches(List<KeyStroke> keys, Trigger expected) { for (KeyStroke key : keys) { if (key.equals(expected)) return true; } return false; } private List<KeyStroke> generateKeyStrokes(Event event) { final List<KeyStroke> keyStrokes = new ArrayList<KeyStroke>(3); /* * If this is not a keyboard event, then there are no key strokes. * This can happen if we are listening to focus traversal events. */ if ((event.stateMask == 0) && (event.keyCode == 0) && (event.character == 0)) { return keyStrokes; } // Add each unique key stroke to the list for consideration. final int firstAccelerator = SWTKeySupport .convertEventToUnmodifiedAccelerator(event); keyStrokes.add(SWTKeySupport .convertAcceleratorToKeyStroke(firstAccelerator)); // We shouldn't allow delete to undergo shift resolution. if (event.character == SWT.DEL) { return keyStrokes; } final int secondAccelerator = SWTKeySupport .convertEventToUnshiftedModifiedAccelerator(event); if (secondAccelerator != firstAccelerator) { keyStrokes.add(SWTKeySupport .convertAcceleratorToKeyStroke(secondAccelerator)); } final int thirdAccelerator = SWTKeySupport .convertEventToModifiedAccelerator(event); if ((thirdAccelerator != secondAccelerator) && (thirdAccelerator != firstAccelerator)) { keyStrokes.add(SWTKeySupport .convertAcceleratorToKeyStroke(thirdAccelerator)); } return keyStrokes; } } private IWorkbenchWindow window; private ITopicPart topicPart; private TopicNotesViewer notesViewer; private NotesPopupActionBarContributor contributor; private RichDocumentNotesAdapter notesAdapter; private Map<TriggerSequence, String> triggerableCommands = new HashMap<TriggerSequence, String>( 3); private IContextActivation contextActivation; private IContextService contextService; private IBindingService bindingService; private boolean showGotoNotesPart; private boolean editable; private boolean updating = false; private ISpellingActivation spellingActivation; private ICoreEventRegistration saveNotesReg; public NotesPopup(IWorkbenchWindow window, ITopicPart topicPart, boolean editable, boolean showGotoNotesView) { super(window.getShell(), SWT.RESIZE, true, true, true, false, false, null, showGotoNotesView ? "" : null); //$NON-NLS-1$ this.window = window; this.topicPart = topicPart; this.showGotoNotesPart = showGotoNotesView; this.editable = editable; } public NotesPopup(Shell parentShell, ITopicPart topicPart, boolean editable) { super(parentShell, SWT.Resize, true, true, true, false, false, null, null); this.topicPart = topicPart; this.window = null; this.showGotoNotesPart = false; this.editable = editable; } protected Control createDialogArea(Composite parent) { Composite composite = (Composite) super.createDialogArea(parent); notesViewer = new TopicNotesViewer(); if (editable) { notesViewer.setContributor( contributor = new NotesPopupActionBarContributor()); } int style = IRichTextEditViewer.DEFAULT_CONTROL_STYLE; if (!editable) { style |= SWT.READ_ONLY; } notesViewer.createControl(composite, style); GridData gridData = new GridData(GridData.FILL_BOTH); gridData.widthHint = 400; notesViewer.getControl().setLayoutData(gridData); ITopic topic = topicPart.getTopic(); notesAdapter = new RichDocumentNotesAdapter(topic); notesViewer.setInput(notesAdapter); notesViewer.getControl().addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent e) { if (notesAdapter != null) { notesAdapter.dispose(); notesAdapter = null; } } }); notesViewer.getImplementation().addSelectionChangedListener(this); notesViewer.getImplementation().getDocument().addDocumentListener(this); notesViewer.getImplementation().getDocument() .addRichDocumentListener(this); new PopupKeyboardListener() .hook(notesViewer.getImplementation().getFocusControl()); update(); addSpellCheck(); return composite; } private void addSpellCheck() { spellingActivation = SpellingSupport.getInstance().activateSpelling( notesViewer.getImplementation().getTextViewer()); } public TopicNotesViewer getNotesViewer() { return notesViewer; } public RichDocumentNotesAdapter getNotesAdapter() { return notesAdapter; } public ITopicPart getTopicPart() { return topicPart; } protected Control getFocusControl() { return notesViewer.getImplementation().getFocusControl(); } @Override protected Point getInitialLocation(Point initialSize) { IViewer viewer = topicPart.getSite().getViewer(); Rectangle bounds = topicPart.getFigure().getBounds().getCopy(); return calcInitialLocation((IGraphicalViewer) viewer, bounds); } private Point calcInitialLocation(IGraphicalViewer viewer, Rectangle bounds) { ZoomManager zoom = viewer.getZoomManager(); bounds = bounds.scale(zoom.getScale()).expand(1, 1) .translate(viewer.getScrollPosition().getNegated()); return viewer.getControl().toDisplay(bounds.x, bounds.y + bounds.height); } @SuppressWarnings("unchecked") protected List getBackgroundColorExclusions() { List list = super.getBackgroundColorExclusions(); collectBackgroundColorExclusions(notesViewer.getControl(), list); return list; } @SuppressWarnings("unchecked") private void collectBackgroundColorExclusions(Control control, List list) { list.add(control); if (control instanceof Composite) { for (Control child : ((Composite) control).getChildren()) { collectBackgroundColorExclusions(child, list); } } } protected IDialogSettings getDialogSettings() { return MindMapUIPlugin.getDefault() .getDialogSettings(MindMapUI.POPUP_DIALOG_SETTINGS_ID); } public int open() { IWorkbench workbench = window.getWorkbench(); bindingService = (IBindingService) workbench .getAdapter(IBindingService.class); contextService = (IContextService) workbench .getAdapter(IContextService.class); if (bindingService != null) { registerWorkbenchCommands(); } int ret = super.open(); if (ret == OK) { if (contextService != null) { contextActivation = contextService.activateContext(CONTEXT_ID); } if (bindingService != null) { registerDialogCommands(); } } activateJob(); return ret; } private void activateJob() { if (saveNotesReg != null && saveNotesReg.isValid()) return; saveNotesReg = null; if (window != null) { IEditorPart editorPart = window.getActivePage().getActiveEditor(); if (editorPart != null && editorPart instanceof MindMapEditor) { IWorkbook workbook = ((MindMapEditor) editorPart).getWorkbook(); if (workbook instanceof ICoreEventSource2) { saveNotesReg = ((ICoreEventSource2) workbook) .registerOnceCoreEventListener( Core.WorkbookPreSaveOnce, ICoreEventListener.NULL); } } } } protected void registerDialogCommands() { if (showGotoNotesPart) { TriggerSequence key = registerCommand(CMD_GOTO_NOTES_VIEW); if (key != null) { setInfoText( NLS.bind(DialogMessages.NotesPopup_GotoNotesView_text, key.format())); } } registerCommand(CMD_COMMIT_NOTES); for (String commandId : contributor.getTextCommandIds()) { registerCommand(commandId); } } protected void registerWorkbenchCommands() { registerCommand(IWorkbenchCommandConstants.FILE_SAVE); registerCommand(IWorkbenchCommandConstants.EDIT_UNDO); registerCommand(IWorkbenchCommandConstants.EDIT_REDO); registerCommand(IWorkbenchCommandConstants.EDIT_CUT); registerCommand(IWorkbenchCommandConstants.EDIT_COPY); registerCommand(IWorkbenchCommandConstants.EDIT_PASTE); registerCommand(IWorkbenchCommandConstants.EDIT_SELECT_ALL); } protected TriggerSequence registerCommand(String commandId) { if (bindingService == null) return null; TriggerSequence key = bindingService.getBestActiveBindingFor(commandId); if (key != null) { triggerableCommands.put(key, commandId); } return key; } protected boolean handleCommand(String commandId) { if (CMD_GOTO_NOTES_VIEW.equals(commandId)) { if (showGotoNotesPart) { gotoNotesPart(); } return true; } else if (CMD_COMMIT_NOTES.equals(commandId)) { // Display.getCurrent().asyncExec(new Runnable() { // public void run() { setReturnCode(OK); close(); // } // }); return true; } else if (IWorkbenchCommandConstants.FILE_SAVE.equals(commandId)) { saveNotes(); return true; } IAction action = contributor.getActionHandler(commandId); if (action != null && action.isEnabled()) { if (action.getStyle() == IAction.AS_CHECK_BOX) { action.setChecked(!action.isChecked()); } action.run(); return true; } return false; } public boolean close() { if (contextActivation != null && contextService != null) { contextService.deactivateContext(contextActivation); contextActivation = null; } deactivateJob(); if (getReturnCode() == OK) saveNotes(); return super.close(); } private void deactivateJob() { if (saveNotesReg != null) { saveNotesReg.unregister(); saveNotesReg = null; } } private void saveNotes() { if (notesAdapter == null || notesViewer == null || notesViewer.getControl().isDisposed() || !notesViewer.hasModified()) return; doSaveNotes(); notesViewer.resetModified(); } private void doSaveNotes() { INotesContent html = notesAdapter.makeNewHtmlContent(); INotesContent plain = notesAdapter.makeNewPlainContent(); ITopic topic = topicPart.getTopic(); EditDomain domain = topicPart.getSite().getViewer().getEditDomain(); if (domain != null) { ICommandStack cs = domain.getCommandStack(); if (cs != null) { ModifyNotesCommand modifyHtml = new ModifyNotesCommand(topic, html, INotes.HTML); ModifyNotesCommand modifyPlain = new ModifyNotesCommand(topic, plain, INotes.PLAIN); CompoundCommand cmd = new CompoundCommand(modifyHtml, modifyPlain); cmd.setLabel(CommandMessages.Command_ModifyNotes); cs.execute(cmd); return; } } INotes notes = topic.getNotes(); notes.setContent(INotes.HTML, html); notes.setContent(INotes.PLAIN, plain); } private void gotoNotesPart() { Display.getCurrent().asyncExec(new Runnable() { public void run() { if (window == null) { return; } close(); E4Utils.showPart(IModelConstants.COMMAND_SHOW_MODEL_PART, window, IModelConstants.PART_ID_NOTES, null, IModelConstants.PART_STACK_ID_RIGHT); } }); } public void documentAboutToBeChanged(DocumentEvent event) { } public void documentChanged(DocumentEvent event) { update(); } public void hyperlinkChanged(IRichDocument document, Hyperlink[] oldHyperlinks, Hyperlink[] newHyperlinks) { update(); } public void imageChanged(IRichDocument document, ImagePlaceHolder[] oldImages, ImagePlaceHolder[] newImages) { update(); } public void lineStyleChanged(IRichDocument document, LineStyle[] oldLineStyles, LineStyle[] newLineStyles) { update(); } public void textStyleChanged(IRichDocument document, StyleRange[] oldTextStyles, StyleRange[] newTextStyles) { update(); } public void selectionChanged(SelectionChangedEvent event) { update(); } private void update() { if (updating) return; updating = true; Display.getCurrent().asyncExec(new Runnable() { public void run() { updateTextActions(); updating = false; } }); doSaveNotes(); } private void updateTextActions() { if (notesViewer == null || notesViewer.getControl().isDisposed() || contributor == null) return; TextViewer textViewer = notesViewer.getImplementation().getTextViewer(); if (textViewer != null) { contributor.update(textViewer); } } }