diff options
author | Tom Schindl | 2016-02-19 18:24:20 +0000 |
---|---|---|
committer | Tom Schindl | 2016-02-19 18:24:20 +0000 |
commit | 04c4cc20b1e804b86f78e5b12ef6f30670b3fece (patch) | |
tree | 4f8f365d26029217be731f0e2f6c5bdf835423de /bundles | |
parent | df69dd3f67f07cab816343aeae962365c13be333 (diff) | |
parent | c82c996ca782fb04db7c2f281a07f7fa3fc796c5 (diff) | |
download | org.eclipse.efxclipse-04c4cc20b1e804b86f78e5b12ef6f30670b3fece.tar.gz org.eclipse.efxclipse-04c4cc20b1e804b86f78e5b12ef6f30670b3fece.tar.xz org.eclipse.efxclipse-04c4cc20b1e804b86f78e5b12ef6f30670b3fece.zip |
Merge branch 'codeeditor'
Diffstat (limited to 'bundles')
77 files changed, 5874 insertions, 1583 deletions
diff --git a/bundles/code/org.eclipse.fx.code.editor.configuration.text.fx/.settings/ca.ecliptical.pde.ds.prefs b/bundles/code/org.eclipse.fx.code.editor.configuration.text.fx/.settings/ca.ecliptical.pde.ds.prefs new file mode 100644 index 000000000..7818b873d --- /dev/null +++ b/bundles/code/org.eclipse.fx.code.editor.configuration.text.fx/.settings/ca.ecliptical.pde.ds.prefs @@ -0,0 +1,6 @@ +classpath=true +eclipse.preferences.version=1 +enabled=true +path=OSGI-INF/services +validationErrorLevel=error +validationErrorLevel.missingImplicitUnbindMethod=error diff --git a/bundles/code/org.eclipse.fx.code.editor.e4/META-INF/MANIFEST.MF b/bundles/code/org.eclipse.fx.code.editor.e4/META-INF/MANIFEST.MF index e43ea980f..55694c066 100644 --- a/bundles/code/org.eclipse.fx.code.editor.e4/META-INF/MANIFEST.MF +++ b/bundles/code/org.eclipse.fx.code.editor.e4/META-INF/MANIFEST.MF @@ -29,5 +29,6 @@ Service-Component: OSGI-INF/services/org.eclipse.fx.code.editor.e4.internal.Docu OSGI-INF/services/org.eclipse.fx.code.editor.e4.internal.DocumentContextFunction.xml, OSGI-INF/services/org.eclipse.fx.code.editor.e4.internal.InputContextFunction.xml, OSGI-INF/services/org.eclipse.fx.code.editor.e4.internal.ProposalComputerTypeProviderContextFunction.xml, - OSGI-INF/services/org.eclipse.fx.code.editor.e4.internal.HoverInformationProviderTypeProviderCF.xml + OSGI-INF/services/org.eclipse.fx.code.editor.e4.internal.HoverInformationProviderTypeProviderCF.xml, + OSGI-INF/services/org.eclipse.fx.code.editor.e4.internal.SearchProviderTypeProviderCF.xml Bundle-ActivationPolicy: lazy diff --git a/bundles/code/org.eclipse.fx.code.editor.e4/OSGI-INF/services/org.eclipse.fx.code.editor.e4.internal.SearchProviderTypeProviderCF.xml b/bundles/code/org.eclipse.fx.code.editor.e4/OSGI-INF/services/org.eclipse.fx.code.editor.e4.internal.SearchProviderTypeProviderCF.xml new file mode 100644 index 000000000..7a173f15b --- /dev/null +++ b/bundles/code/org.eclipse.fx.code.editor.e4/OSGI-INF/services/org.eclipse.fx.code.editor.e4.internal.SearchProviderTypeProviderCF.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.2.0" name="org.eclipse.fx.code.editor.e4.internal.SearchProviderTypeProviderCF"> + <property name="service.context.key" value="org.eclipse.fx.code.editor.services.SearchProvider"/> + <service> + <provide interface="org.eclipse.e4.core.contexts.IContextFunction"/> + </service> + <reference bind="registerService" cardinality="0..n" interface="org.eclipse.fx.code.editor.services.SearchProviderTypeProvider" name="registerService" policy="dynamic" policy-option="greedy" unbind="unregisterService"/> + <implementation class="org.eclipse.fx.code.editor.e4.internal.SearchProviderTypeProviderCF"/> +</scr:component>
\ No newline at end of file diff --git a/bundles/code/org.eclipse.fx.code.editor.e4/src/org/eclipse/fx/code/editor/e4/internal/SearchProviderTypeProviderCF.java b/bundles/code/org.eclipse.fx.code.editor.e4/src/org/eclipse/fx/code/editor/e4/internal/SearchProviderTypeProviderCF.java new file mode 100644 index 000000000..8a87499ba --- /dev/null +++ b/bundles/code/org.eclipse.fx.code.editor.e4/src/org/eclipse/fx/code/editor/e4/internal/SearchProviderTypeProviderCF.java @@ -0,0 +1,29 @@ +package org.eclipse.fx.code.editor.e4.internal; + +import java.util.Map; + +import org.eclipse.e4.core.contexts.IContextFunction; +import org.eclipse.fx.code.editor.e4.InputBasedContextFunction; +import org.eclipse.fx.code.editor.services.SearchProvider; +import org.eclipse.fx.code.editor.services.SearchProviderTypeProvider; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.osgi.service.component.annotations.ReferencePolicyOption; + +@SuppressWarnings("restriction") +@Component(service=IContextFunction.class,property={"service.context.key=org.eclipse.fx.code.editor.services.SearchProvider"}) +public class SearchProviderTypeProviderCF extends InputBasedContextFunction<SearchProvider, SearchProviderTypeProvider> { + + @Override + @Reference(cardinality=ReferenceCardinality.MULTIPLE,policy=ReferencePolicy.DYNAMIC,policyOption=ReferencePolicyOption.GREEDY) + public void registerService(SearchProviderTypeProvider service, Map<String, Object> properties) { + super.registerService(service, properties); + } + + @Override + public void unregisterService(SearchProviderTypeProvider service) { + super.unregisterService(service); + } +} diff --git a/bundles/code/org.eclipse.fx.code.editor.fx.e4/META-INF/MANIFEST.MF b/bundles/code/org.eclipse.fx.code.editor.fx.e4/META-INF/MANIFEST.MF index 8a2b4351b..a2d38101a 100644 --- a/bundles/code/org.eclipse.fx.code.editor.fx.e4/META-INF/MANIFEST.MF +++ b/bundles/code/org.eclipse.fx.code.editor.fx.e4/META-INF/MANIFEST.MF @@ -10,7 +10,8 @@ Require-Bundle: org.eclipse.e4.core.contexts;bundle-version="1.4.0", org.eclipse.e4.ui.workbench;bundle-version="1.3.0", org.eclipse.e4.core.di;bundle-version="1.5.0", org.eclipse.e4.core.services;bundle-version="2.0.0", - org.eclipse.fx.text.ui;bundle-version="2.2.0" + org.eclipse.fx.text.ui;bundle-version="2.2.0", + org.eclipse.text;bundle-version="3.5.400" Import-Package: javax.inject, org.eclipse.fx.code.editor;version="2.2.0", org.eclipse.fx.code.editor.e4;version="2.2.0", @@ -26,6 +27,7 @@ Service-Component: OSGI-INF/services/org.eclipse.fx.code.editor.fx.e4.internal.S OSGI-INF/services/org.eclipse.fx.code.editor.fx.e4.internal.ActiveOutlineContextFunction.xml, OSGI-INF/services/org.eclipse.fx.code.editor.fx.e4.internal.EditorOpenerContextFunction.xml, OSGI-INF/services/org.eclipse.fx.code.editor.fx.e4.internal.EditorContainerServiceContextFunction.xml, - OSGI-INF/services/org.eclipse.fx.code.editor.fx.e4.internal.CompletionProposalPresenterTypeProviderContextFunction.xml + OSGI-INF/services/org.eclipse.fx.code.editor.fx.e4.internal.CompletionProposalPresenterTypeProviderContextFunction.xml, + OSGI-INF/services/org.eclipse.fx.code.editor.fx.e4.internal.ContextInformationPresenterTypeProviderContextFunction.xml Bundle-Vendor: Eclipse.org Export-Package: org.eclipse.fx.code.editor.fx.e4 diff --git a/bundles/code/org.eclipse.fx.code.editor.fx.e4/OSGI-INF/services/org.eclipse.fx.code.editor.fx.e4.internal.ContextInformationPresenterTypeProviderContextFunction.xml b/bundles/code/org.eclipse.fx.code.editor.fx.e4/OSGI-INF/services/org.eclipse.fx.code.editor.fx.e4.internal.ContextInformationPresenterTypeProviderContextFunction.xml new file mode 100644 index 000000000..f59cd12df --- /dev/null +++ b/bundles/code/org.eclipse.fx.code.editor.fx.e4/OSGI-INF/services/org.eclipse.fx.code.editor.fx.e4.internal.ContextInformationPresenterTypeProviderContextFunction.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.2.0" name="org.eclipse.fx.code.editor.fx.e4.internal.ContextInformationPresenterTypeProviderContextFunction"> + <property name="service.context.key" value="org.eclipse.fx.code.editor.fx.services.ContextInformationPresenter"/> + <service> + <provide interface="org.eclipse.e4.core.contexts.IContextFunction"/> + </service> + <reference bind="registerService" cardinality="0..n" interface="org.eclipse.fx.code.editor.fx.services.ContextInformationPresenterTypeProvider" name="registerService" policy="dynamic" policy-option="greedy" unbind="unregisterService"/> + <implementation class="org.eclipse.fx.code.editor.fx.e4.internal.ContextInformationPresenterTypeProviderContextFunction"/> +</scr:component>
\ No newline at end of file diff --git a/bundles/code/org.eclipse.fx.code.editor.fx.e4/src/org/eclipse/fx/code/editor/fx/e4/internal/ContextInformationPresenterTypeProviderContextFunction.java b/bundles/code/org.eclipse.fx.code.editor.fx.e4/src/org/eclipse/fx/code/editor/fx/e4/internal/ContextInformationPresenterTypeProviderContextFunction.java new file mode 100644 index 000000000..1ec5549b0 --- /dev/null +++ b/bundles/code/org.eclipse.fx.code.editor.fx.e4/src/org/eclipse/fx/code/editor/fx/e4/internal/ContextInformationPresenterTypeProviderContextFunction.java @@ -0,0 +1,37 @@ +/******************************************************************************* +* Copyright (c) 2015 BestSolution.at and others. +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Eclipse Public License v1.0 +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v10.html +* +* Contributors: +* Tom Schindl<tom.schindl@bestsolution.at> - initial API and implementation +*******************************************************************************/ +package org.eclipse.fx.code.editor.fx.e4.internal; + +import java.util.Map; + +import org.eclipse.e4.core.contexts.IContextFunction; +import org.eclipse.fx.code.editor.e4.InputBasedContextFunction; +import org.eclipse.fx.code.editor.fx.services.ContextInformationPresenter; +import org.eclipse.fx.code.editor.fx.services.ContextInformationPresenterTypeProvider; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.osgi.service.component.annotations.ReferencePolicyOption; + +@SuppressWarnings("restriction") +@Component(service=IContextFunction.class,property={"service.context.key=org.eclipse.fx.code.editor.fx.services.ContextInformationPresenter"}) +public class ContextInformationPresenterTypeProviderContextFunction extends InputBasedContextFunction<ContextInformationPresenter, ContextInformationPresenterTypeProvider> { + + @Reference(cardinality=ReferenceCardinality.MULTIPLE,policy=ReferencePolicy.DYNAMIC,policyOption=ReferencePolicyOption.GREEDY) + public void registerService(ContextInformationPresenterTypeProvider service, Map<String, Object> properties) { + super.registerService(service, properties); + } + + public void unregisterService(ContextInformationPresenterTypeProvider service) { + super.unregisterService(service); + } +}
\ No newline at end of file diff --git a/bundles/code/org.eclipse.fx.code.editor.fx.e4/src/org/eclipse/fx/code/editor/fx/e4/internal/EditorOpenerContextFunction.java b/bundles/code/org.eclipse.fx.code.editor.fx.e4/src/org/eclipse/fx/code/editor/fx/e4/internal/EditorOpenerContextFunction.java index 033630534..2e14a2249 100644 --- a/bundles/code/org.eclipse.fx.code.editor.fx.e4/src/org/eclipse/fx/code/editor/fx/e4/internal/EditorOpenerContextFunction.java +++ b/bundles/code/org.eclipse.fx.code.editor.fx.e4/src/org/eclipse/fx/code/editor/fx/e4/internal/EditorOpenerContextFunction.java @@ -131,6 +131,7 @@ public class EditorOpenerContextFunction extends ServiceContextFunction<EditorOp .filter( e -> e.test(uri)).findFirst() .map( e -> e.getBundleClassURI(uri)) .orElse("bundleclass://org.eclipse.fx.code.editor.fx/org.eclipse.fx.code.editor.fx.TextEditor"); +// .orElse("bundleclass://at.bestsolution.set.fx/at.bestsolution.set.fx.editor.SetTextEditor"); part.setContributionURI(editorBundleURI); part.setContributorURI("platform:/plugin/org.eclipse.fx.code.editor.fx.e4"); String iconUri = fileIconProvider diff --git a/bundles/code/org.eclipse.fx.code.editor.fx.themes/css/dark-highlight.css b/bundles/code/org.eclipse.fx.code.editor.fx.themes/css/dark-highlight.css index 5b5cf583a..9ad8aa231 100644 --- a/bundles/code/org.eclipse.fx.code.editor.fx.themes/css/dark-highlight.css +++ b/bundles/code/org.eclipse.fx.code.editor.fx.themes/css/dark-highlight.css @@ -33,6 +33,10 @@ -fx-background-color: #414141; } +.styled-text-area .current-line { + -fx-background-color: #414141; +} + .styled-text-area .line-ruler-text { -fx-text-fill: #c7c7c7; } diff --git a/bundles/code/org.eclipse.fx.code.editor.fx/src/org/eclipse/fx/code/editor/fx/services/ContextInformationPresenter.java b/bundles/code/org.eclipse.fx.code.editor.fx/src/org/eclipse/fx/code/editor/fx/services/ContextInformationPresenter.java new file mode 100644 index 000000000..f4398d76d --- /dev/null +++ b/bundles/code/org.eclipse.fx.code.editor.fx/src/org/eclipse/fx/code/editor/fx/services/ContextInformationPresenter.java @@ -0,0 +1,9 @@ +package org.eclipse.fx.code.editor.fx.services; + +import org.eclipse.fx.code.editor.services.ContextInformation; +import org.eclipse.fx.text.ui.contentassist.IContextInformation; + + +public interface ContextInformationPresenter { + public IContextInformation createInformation(ContextInformation information); +} diff --git a/bundles/code/org.eclipse.fx.code.editor.fx/src/org/eclipse/fx/code/editor/fx/services/ContextInformationPresenterTypeProvider.java b/bundles/code/org.eclipse.fx.code.editor.fx/src/org/eclipse/fx/code/editor/fx/services/ContextInformationPresenterTypeProvider.java new file mode 100644 index 000000000..11000a28a --- /dev/null +++ b/bundles/code/org.eclipse.fx.code.editor.fx/src/org/eclipse/fx/code/editor/fx/services/ContextInformationPresenterTypeProvider.java @@ -0,0 +1,8 @@ +package org.eclipse.fx.code.editor.fx.services; + +import org.eclipse.fx.code.editor.services.InputDependentTypeProviderService; + +@SuppressWarnings("restriction") +public interface ContextInformationPresenterTypeProvider extends InputDependentTypeProviderService<ContextInformationPresenter> { + +} diff --git a/bundles/code/org.eclipse.fx.code.editor.fx/src/org/eclipse/fx/code/editor/fx/services/FXCompletionProposal.java b/bundles/code/org.eclipse.fx.code.editor.fx/src/org/eclipse/fx/code/editor/fx/services/FXCompletionProposal.java index 80084edbd..d545cd8b5 100644 --- a/bundles/code/org.eclipse.fx.code.editor.fx/src/org/eclipse/fx/code/editor/fx/services/FXCompletionProposal.java +++ b/bundles/code/org.eclipse.fx.code.editor.fx/src/org/eclipse/fx/code/editor/fx/services/FXCompletionProposal.java @@ -4,6 +4,7 @@ import java.util.function.Supplier; import org.eclipse.fx.code.editor.services.CompletionProposal; import org.eclipse.fx.text.ui.contentassist.ICompletionProposal; +import org.eclipse.fx.text.ui.contentassist.IContextInformation; import org.eclipse.fx.ui.controls.styledtext.TextSelection; import org.eclipse.jface.text.IDocument; @@ -14,15 +15,20 @@ public class FXCompletionProposal implements ICompletionProposal { private final CompletionProposal proposal; private final Supplier<Node> graphicSupplier; private final CharSequence label; + private final String fHoverInfo; - public FXCompletionProposal(CompletionProposal proposal, Supplier<Node> graphicSupplier) { - this(proposal,proposal.getLabel(),graphicSupplier); + private final IContextInformation fContextInformation; + + public FXCompletionProposal(CompletionProposal proposal, Supplier<Node> graphicSupplier, IContextInformation contextInformation, String hoverInfo) { + this(proposal, proposal.getLabel(), graphicSupplier, contextInformation, hoverInfo); } - public FXCompletionProposal(CompletionProposal proposal, CharSequence label, Supplier<Node> graphicSupplier) { + public FXCompletionProposal(CompletionProposal proposal, CharSequence label, Supplier<Node> graphicSupplier, IContextInformation contextInformation, String hoverInfo) { this.proposal = proposal; this.label = label; this.graphicSupplier = graphicSupplier; + this.fContextInformation = contextInformation; + this.fHoverInfo = hoverInfo; } @Override @@ -45,4 +51,14 @@ public class FXCompletionProposal implements ICompletionProposal { org.eclipse.fx.code.editor.services.CompletionProposal.TextSelection selection = proposal.getSelection(document); return new TextSelection(selection.offset, selection.length); } + + @Override + public IContextInformation getContextInformation() { + return fContextInformation; + } + + @Override + public String getHoverInfo() { + return this.fHoverInfo; + } } diff --git a/bundles/code/org.eclipse.fx.code.editor.fx/src/org/eclipse/fx/code/editor/fx/services/internal/DefaultSourceViewerConfiguration.java b/bundles/code/org.eclipse.fx.code.editor.fx/src/org/eclipse/fx/code/editor/fx/services/internal/DefaultSourceViewerConfiguration.java index 52f8aeee5..a3bd5cb69 100644 --- a/bundles/code/org.eclipse.fx.code.editor.fx/src/org/eclipse/fx/code/editor/fx/services/internal/DefaultSourceViewerConfiguration.java +++ b/bundles/code/org.eclipse.fx.code.editor.fx/src/org/eclipse/fx/code/editor/fx/services/internal/DefaultSourceViewerConfiguration.java @@ -20,9 +20,11 @@ import org.eclipse.fx.text.ui.contentassist.ContentAssistContextData; import org.eclipse.fx.text.ui.contentassist.ContentAssistant; import org.eclipse.fx.text.ui.contentassist.ICompletionProposal; import org.eclipse.fx.text.ui.contentassist.IContentAssistant; +import org.eclipse.fx.text.ui.contentassist.IContextInformation; import org.eclipse.fx.text.ui.presentation.IPresentationReconciler; import org.eclipse.fx.text.ui.presentation.PresentationReconciler; import org.eclipse.fx.text.ui.source.AnnotationPresenter; +import org.eclipse.fx.text.ui.source.ILineRulerAnnotationPresenter; import org.eclipse.fx.text.ui.source.ISourceViewer; import org.eclipse.fx.text.ui.source.SourceViewerConfiguration; import org.eclipse.fx.ui.controls.styledtext.TextSelection; @@ -38,7 +40,7 @@ public class DefaultSourceViewerConfiguration extends SourceViewerConfiguration private final PresentationReconciler reconciler; private final ProposalComputer proposalComputer; private final IAnnotationModel annotationModel; - private final List<AnnotationPresenter> annotationPresenters; + private final AnnotationPresenter annotationPresenter; private final HoverInformationProvider hoverInformationProvider; private final CompletionProposalPresenter proposalPresenter; private ContentAssistant contentAssistant; @@ -49,7 +51,7 @@ public class DefaultSourceViewerConfiguration extends SourceViewerConfiguration PresentationReconciler reconciler, @Optional ProposalComputer proposalComputer, @Optional IAnnotationModel annotationModel, - @Optional AnnotationPresenter presenter, + @Optional AnnotationPresenter annotationPresenter, @Optional HoverInformationProvider hoverInformationProvider, @Optional CompletionProposalPresenter proposalPresenter ) { @@ -59,11 +61,8 @@ public class DefaultSourceViewerConfiguration extends SourceViewerConfiguration this.proposalComputer = proposalComputer; this.annotationModel = annotationModel; this.proposalPresenter = proposalPresenter == null ? DefaultProposal::new : proposalPresenter; - if( presenter != null ) { - this.annotationPresenters = Collections.singletonList(presenter); - } else { - this.annotationPresenters = Collections.emptyList(); - } + + this.annotationPresenter = annotationPresenter; } @Override @@ -109,8 +108,8 @@ public class DefaultSourceViewerConfiguration extends SourceViewerConfiguration } @Override - public List<AnnotationPresenter> getAnnotationPresenters() { - return annotationPresenters; + public AnnotationPresenter getAnnotationPresenter() { + return annotationPresenter; } @Override @@ -160,5 +159,36 @@ public class DefaultSourceViewerConfiguration extends SourceViewerConfiguration org.eclipse.fx.code.editor.services.CompletionProposal.TextSelection selection = proposal.getSelection(document); return selection == null ? TextSelection.EMPTY : new TextSelection(selection.offset, selection.length); } + + @Override + public IContextInformation getContextInformation() { + return new IContextInformation() { + // TODO fix this here + @Override + public String getInformationDisplayString() { + return "InformationDisplayString"; + } + + @Override + public Node getGraphic() { + return null; + } + + @Override + public int getContextInformationPosition() { + return 0; + } + + @Override + public String getContextDisplayString() { + return "ContextDisplayString"; + } + }; + } + + @Override + public String getHoverInfo() { + return null; + } } } diff --git a/bundles/code/org.eclipse.fx.code.editor/src/org/eclipse/fx/code/editor/CodeReference.java b/bundles/code/org.eclipse.fx.code.editor/src/org/eclipse/fx/code/editor/CodeReference.java new file mode 100644 index 000000000..976fc9103 --- /dev/null +++ b/bundles/code/org.eclipse.fx.code.editor/src/org/eclipse/fx/code/editor/CodeReference.java @@ -0,0 +1,12 @@ +package org.eclipse.fx.code.editor; + +import org.eclipse.jface.text.IRegion; + +/** + * references a region inside a document. + * + */ +public interface CodeReference { + Input<?> getInput(); + IRegion getRegion(); +} diff --git a/bundles/code/org.eclipse.fx.code.editor/src/org/eclipse/fx/code/editor/services/CompletionProposal.java b/bundles/code/org.eclipse.fx.code.editor/src/org/eclipse/fx/code/editor/services/CompletionProposal.java index 34429fcac..f22b753c0 100644 --- a/bundles/code/org.eclipse.fx.code.editor/src/org/eclipse/fx/code/editor/services/CompletionProposal.java +++ b/bundles/code/org.eclipse.fx.code.editor/src/org/eclipse/fx/code/editor/services/CompletionProposal.java @@ -18,6 +18,8 @@ public interface CompletionProposal { public void apply(IDocument document); public TextSelection getSelection(IDocument document); + public ContextInformation getContextInformation(); + public static class BaseCompletetionProposal implements CompletionProposal { private final CharSequence label; @@ -26,13 +28,15 @@ public interface CompletionProposal { private final int replacementOffset; private final int replacementLength; private final int cursorPosition; + private final ContextInformation contextInformation; - public BaseCompletetionProposal(String replacementString, int replacementOffset, int replacementLength, CharSequence label) { + public BaseCompletetionProposal(String replacementString, int replacementOffset, int replacementLength, CharSequence label, ContextInformation contextInformation) { this.replacementString = replacementString; this.replacementOffset = replacementOffset; this.replacementLength = replacementLength; this.cursorPosition = replacementString.length(); this.label = label; + this.contextInformation = contextInformation; } @Override @@ -70,6 +74,11 @@ public interface CompletionProposal { public TextSelection getSelection(IDocument document) { return new TextSelection(replacementOffset+cursorPosition, 0); } + + @Override + public ContextInformation getContextInformation() { + return contextInformation; + } } } diff --git a/bundles/code/org.eclipse.fx.code.editor/src/org/eclipse/fx/code/editor/services/ContextInformation.java b/bundles/code/org.eclipse.fx.code.editor/src/org/eclipse/fx/code/editor/services/ContextInformation.java new file mode 100644 index 000000000..1b77728b1 --- /dev/null +++ b/bundles/code/org.eclipse.fx.code.editor/src/org/eclipse/fx/code/editor/services/ContextInformation.java @@ -0,0 +1,33 @@ +package org.eclipse.fx.code.editor.services; + +public interface ContextInformation { + + public CharSequence getText(); + public int getOffset(); + + public static class BaseContextInformation implements ContextInformation { + + private final CharSequence label; + + private final int offset; + + public BaseContextInformation(int offset, CharSequence label) { + this.offset = offset; + this.label = label; + } + + @Override + public CharSequence getText() { + return label; + } + + @Override + public int getOffset() { + return offset; + } + + } + + + +} diff --git a/bundles/code/org.eclipse.fx.code.editor/src/org/eclipse/fx/code/editor/services/SearchProvider.java b/bundles/code/org.eclipse.fx.code.editor/src/org/eclipse/fx/code/editor/services/SearchProvider.java new file mode 100644 index 000000000..5e937932e --- /dev/null +++ b/bundles/code/org.eclipse.fx.code.editor/src/org/eclipse/fx/code/editor/services/SearchProvider.java @@ -0,0 +1,19 @@ +package org.eclipse.fx.code.editor.services; + +import java.util.List; +import java.util.concurrent.Future; + +import org.eclipse.fx.code.editor.CodeReference; + +public interface SearchProvider { + + + + Future<CodeReference> findDeclaration(int offset); + + Future<List<CodeReference>> findImplementations(int offset); + + Future<List<CodeReference>> findReferences(int offset); + + +} diff --git a/bundles/code/org.eclipse.fx.code.editor/src/org/eclipse/fx/code/editor/services/SearchProviderTypeProvider.java b/bundles/code/org.eclipse.fx.code.editor/src/org/eclipse/fx/code/editor/services/SearchProviderTypeProvider.java new file mode 100644 index 000000000..bfb64af7d --- /dev/null +++ b/bundles/code/org.eclipse.fx.code.editor/src/org/eclipse/fx/code/editor/services/SearchProviderTypeProvider.java @@ -0,0 +1,5 @@ +package org.eclipse.fx.code.editor.services; + +public interface SearchProviderTypeProvider extends InputDependentTypeProviderService<SearchProvider> { + +} diff --git a/bundles/code/org.eclipse.fx.text.ui/META-INF/MANIFEST.MF b/bundles/code/org.eclipse.fx.text.ui/META-INF/MANIFEST.MF index 103034e93..7139dd936 100644 --- a/bundles/code/org.eclipse.fx.text.ui/META-INF/MANIFEST.MF +++ b/bundles/code/org.eclipse.fx.text.ui/META-INF/MANIFEST.MF @@ -18,3 +18,4 @@ Export-Package: org.eclipse.fx.text.ui, org.eclipse.fx.text.ui.rules;x-internal:=true, org.eclipse.fx.text.ui.source Bundle-Vendor: Eclipse.org +Import-Package: com.google.common.collect diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/DefaultDocumentAdapter.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/DefaultDocumentAdapter.java index 443173e3d..a608a2677 100644 --- a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/DefaultDocumentAdapter.java +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/DefaultDocumentAdapter.java @@ -273,7 +273,7 @@ class DefaultDocumentAdapter implements IDocumentAdapter, IDocumentListener, IDo } else { if (event.getOffset() < fRememberedLengthOfFirstLine) fLineDelimiter= null; - fireTextChanged(); + fireTextChanged(event.getOffset(), event.getLength(), event.getText()); } } @@ -319,12 +319,12 @@ class DefaultDocumentAdapter implements IDocumentAdapter, IDocumentListener, IDo /** * Sends a text changed event to all registered listeners. */ - private void fireTextChanged() { + private void fireTextChanged(int offset, int replaceLength, String newText) { if (!fIsForwarding) return; - TextChangedEvent event = TextChangedEvent.textChanged(this); + TextChangedEvent event = TextChangedEvent.textChanged(this, offset, replaceLength, newText); if (fTextChangeListeners != null && fTextChangeListeners.size() > 0) { Iterator e= new ArrayList(fTextChangeListeners).iterator(); diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/TextViewer.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/TextViewer.java index 5f064753b..e3335294c 100644 --- a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/TextViewer.java +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/TextViewer.java @@ -19,11 +19,10 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import javafx.scene.input.KeyCode; -import javafx.scene.layout.AnchorPane; - import org.eclipse.core.runtime.Assert; import org.eclipse.fx.core.Util; +import org.eclipse.fx.text.ui.internal.InvisibleCharSupport; +import org.eclipse.fx.text.ui.internal.LineNumberSupport; import org.eclipse.fx.ui.controls.styledtext.StyleRange; import org.eclipse.fx.ui.controls.styledtext.StyledTextArea; import org.eclipse.fx.ui.controls.styledtext.VerifyEvent; @@ -48,6 +47,9 @@ import org.eclipse.jface.text.TextUtilities; import org.eclipse.jface.text.projection.ChildDocument; import org.eclipse.jface.text.projection.ChildDocumentManager; +import javafx.scene.input.KeyCode; +import javafx.scene.layout.AnchorPane; + public class TextViewer extends AnchorPane implements ITextViewer, ITextViewerExtension, ITextViewerExtension2, ITextViewerExtension4, ITextViewerExtension6, ITextViewerExtension7, ITextViewerExtension8 /*, @@ -108,6 +110,9 @@ public class TextViewer extends AnchorPane implements getChildren().add(fTextWidget); fTextWidget.addEventHandler(VerifyEvent.VERIFY, this::onVerify); + + new LineNumberSupport(fTextWidget).install(); + new InvisibleCharSupport(fTextWidget).install(); } private void onVerify(VerifyEvent event) { @@ -1006,4 +1011,5 @@ public class TextViewer extends AnchorPane implements fStateMask= stateMask; } } + } diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/ContentAssistant.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/ContentAssistant.java index 65c80a573..046158db3 100644 --- a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/ContentAssistant.java +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/ContentAssistant.java @@ -21,8 +21,9 @@ import org.eclipse.fx.ui.controls.styledtext.VerifyEvent; public class ContentAssistant implements IContentAssistant { private final Function<ContentAssistContextData, List<ICompletionProposal>> proposalComputer; - private ITextViewer viewer; - private ContentProposalPopup popuop; + private ITextViewer fViewer; + private ContentProposalPopup fProposalPopup; + private ContextInformationPopup fContextInfoPopup; public ContentAssistant(Function<ContentAssistContextData, List<ICompletionProposal>> proposalComputer) { this.proposalComputer = proposalComputer; @@ -30,10 +31,12 @@ public class ContentAssistant implements IContentAssistant { @Override public void install(ITextViewer textViewer) { - if( this.viewer == null ) { - this.viewer = textViewer; - this.popuop = new ContentProposalPopup(textViewer,proposalComputer); + if( this.fViewer == null ) { + this.fViewer = textViewer; + this.fProposalPopup = new ContentProposalPopup(this, textViewer,proposalComputer); textViewer.getTextWidget().addEventHandler(VerifyEvent.VERIFY, this::handleVerify); + + fContextInfoPopup = new ContextInformationPopup(this, textViewer); } } @@ -43,18 +46,32 @@ public class ContentAssistant implements IContentAssistant { } event.consume(); - List<ICompletionProposal> proposals = proposalComputer.apply(new ContentAssistContextData(this.viewer.getTextWidget().getCaretOffset(),this.viewer.getDocument()/*,""*/)); + final int offset = this.fViewer.getTextWidget().getCaretOffset(); + + List<ICompletionProposal> proposals = proposalComputer.apply(new ContentAssistContextData(offset, this.fViewer.getDocument()/*,""*/)); if( proposals.size() == 1) { - proposals.get(0).apply(this.viewer.getDocument()); - this.viewer.getTextWidget().setSelection(proposals.get(0).getSelection(this.viewer.getDocument())); + ICompletionProposal completionProposal = proposals.get(0); + + completionProposal.apply(this.fViewer.getDocument()); + + showContextInformation(completionProposal.getContextInformation(), offset); + + //this.fViewer.getTextWidget().setSelection(proposals.get(0).getSelection(this.fViewer.getDocument())); } else if( ! proposals.isEmpty() ) { // System.err.println(this.viewer.getTextWidget().getCaretLocation()); System.err.println(); - Point2D p = this.viewer.getTextWidget().getLocationAtOffset(this.viewer.getTextWidget().getCaretOffset()); + Point2D p = this.fViewer.getTextWidget().getLocationAtOffset(this.fViewer.getTextWidget().getCaretOffset()); System.err.println(p); - this.popuop.displayProposals(proposals, this.viewer.getTextWidget().getCaretOffset(), this.viewer.getTextWidget().localToScreen(p)); + this.fProposalPopup.displayProposals(proposals, this.fViewer.getTextWidget().getCaretOffset(), this.fViewer.getTextWidget().localToScreen(p)); + } + + } + + void showContextInformation(IContextInformation contextInformation, int offset) { + if (this.fContextInfoPopup != null) { + this.fContextInfoPopup.showContextInformation(contextInformation, offset); } } } diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/ContentProposalPopup.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/ContentProposalPopup.java index ff78bf6fd..2ef7522fb 100644 --- a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/ContentProposalPopup.java +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/ContentProposalPopup.java @@ -12,35 +12,56 @@ package org.eclipse.fx.text.ui.contentassist; import java.util.Collections; import java.util.List; +import java.util.concurrent.BrokenBarrierException; import java.util.function.Function; import org.eclipse.fx.core.Util; import org.eclipse.fx.text.ui.ITextViewer; import org.eclipse.fx.ui.controls.list.SimpleListCell; +import org.eclipse.fx.ui.controls.styledtext.StyledTextContent.TextChangeListener; +import org.eclipse.fx.ui.controls.styledtext.TextChangedEvent; +import org.eclipse.fx.ui.controls.styledtext.TextChangingEvent; +import org.eclipse.fx.ui.controls.styledtext.VerifyEvent; import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IDocumentListener; + +import com.sun.javafx.collections.ImmutableObservableList; import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; +import javafx.event.Event; +import javafx.event.EventDispatchChain; +import javafx.event.EventDispatcher; import javafx.geometry.Point2D; import javafx.scene.Node; import javafx.scene.control.ListView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; +import javafx.scene.web.WebView; import javafx.stage.PopupWindow; -public class ContentProposalPopup { +public class ContentProposalPopup implements IContentAssistListener { private ITextViewer viewer; private PopupWindow stage; private ListView<ICompletionProposal> proposalList; + private WebView documentationView; private String prefix; private int offset; private Function<ContentAssistContextData, List<ICompletionProposal>> proposalComputer; - public ContentProposalPopup(ITextViewer viewer, Function<ContentAssistContextData, List<ICompletionProposal>> proposalComputer) { + private ContentAssistant fContentAssistant; + + public ContentProposalPopup(ContentAssistant assistant, ITextViewer viewer, Function<ContentAssistContextData, List<ICompletionProposal>> proposalComputer) { this.viewer = viewer; this.proposalComputer = proposalComputer; + this.fContentAssistant = assistant; } public void displayProposals(List<ICompletionProposal> proposalList, int offset, Point2D position) { @@ -55,37 +76,80 @@ public class ContentProposalPopup { this.stage.setHeight(200); this.stage.show(this.viewer.getTextWidget().getScene().getWindow()); this.stage.requestFocus(); + + this.stage.setOnShowing(this::subscribe); + this.stage.setOnHidden(this::unsubscribe); } - private void handleKeyTyped(KeyEvent event) { - if( event.getCharacter().length() == 0 ) { - return; + private void subscribe(Event e) { + this.viewer.getTextWidget().getContent().addTextChangeListener(this.textChangeListener); + this.viewer.getTextWidget().selectionProperty().addListener(this::onSelectionChange); + } + private void unsubscribe(Event e) { + this.viewer.getTextWidget().getContent().removeTextChangeListener(this.textChangeListener); + this.viewer.getTextWidget().selectionProperty().removeListener(this::onSelectionChange); + } + + // TODO calling updateProposals is way too slow + + private void onSelectionChange(Observable x) { + System.err.println("update trigger selection change"); + updateProposals(); + } + + private TextChangeListener textChangeListener = new TextChangeListener() { + @Override + public void textSet(TextChangedEvent event) { + System.err.println("update trigger textSet"); + updateProposals(); } - String character = event.getCharacter(); - if( character.length() == 0 ) { - return; + @Override + public void textChanging(TextChangingEvent event) { } - if (event.isControlDown() || event.isAltDown() || (Util.isMacOS() && event.isMetaDown())) { - if (!((event.isControlDown() || Util.isMacOS()) && event.isAltDown())) { - return; - } + @Override + public void textChanged(TextChangedEvent event) { + System.err.println("update trigger textChanged"); + updateProposals(); } + }; - if (character.charAt(0) > 0x1F - && character.charAt(0) != 0x7F ) { -// try { - this.prefix = this.prefix+character; -// viewer.getDocument().replace(offset, 0, character); - this.offset += event.getCharacter().length(); -// viewer.getTextWidget().setCaretOffset(offset); - updateProposals(); -// } catch (BadLocationException e) { -// // TODO Auto-generated catch block -// e.printStackTrace(); + + private void handleKeyTyped(KeyEvent event) { +// Event.fireEvent(this.viewer.getTextWidget(), event); + return; +// +// +// +// if( event.getCharacter().length() == 0 ) { +// return; +// } +// +// String character = event.getCharacter(); +// if( character.length() == 0 ) { +// return; +// } +// +// if (event.isControlDown() || event.isAltDown() || (Util.isMacOS() && event.isMetaDown())) { +// if (!((event.isControlDown() || Util.isMacOS()) && event.isAltDown())) { +// return; // } - } +// } +// +// if (character.charAt(0) > 0x1F +// && character.charAt(0) != 0x7F ) { +//// try { +// this.prefix = this.prefix+character; +//// viewer.getDocument().replace(offset, 0, character); +// this.offset += event.getCharacter().length(); +//// viewer.getTextWidget().setCaretOffset(offset); +// updateProposals(); +//// } catch (BadLocationException e) { +//// // TODO Auto-generated catch block +//// e.printStackTrace(); +//// } +// } } private void updateProposals() { @@ -99,49 +163,80 @@ public class ContentProposalPopup { } } - private void handleKeyPressed(KeyEvent event) { - if( event.getCode() == KeyCode.ESCAPE ) { - event.consume(); - this.stage.hide(); - } else if( event.getCode() == KeyCode.BACK_SPACE ) { - event.consume(); - this.offset -= 1; - try { - this.viewer.getDocument().replace(this.offset, 1, ""); //$NON-NLS-1$ - this.viewer.getTextWidget().setCaretOffset(this.offset); - updateProposals(); - } catch (BadLocationException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } else if( event.getCode() == KeyCode.ENTER ) { - event.consume(); - applySelection(); - } else if( event.getCode() == KeyCode.LEFT ) { - event.consume(); - this.offset -= 1; - this.offset = Math.max(0, this.offset); - this.viewer.getTextWidget().setCaretOffset(this.offset); - updateProposals(); - } else if( event.getCode() == KeyCode.RIGHT ) { - event.consume(); - this.offset += 1; - this.offset = Math.min(this.viewer.getDocument().getLength()-1, this.offset); - this.viewer.getTextWidget().setCaretOffset(this.offset); - updateProposals(); - } + private void cancelProposal() { + this.stage.hide(); } - private void applySelection() { + private void applySelectedProposal() { ICompletionProposal selectedItem = this.proposalList.getSelectionModel().getSelectedItem(); if( selectedItem != null ) { IDocument document = this.viewer.getDocument(); selectedItem.apply(document); + this.viewer.getTextWidget().setSelection(selectedItem.getSelection(document)); this.stage.hide(); + + this.fContentAssistant.showContextInformation(selectedItem.getContextInformation(), offset); + } + } + + private void handleMouseClicked(MouseEvent event) { + if(event.isStillSincePress() && event.getClickCount() == 2) { + applySelectedProposal(); } } + private void handleKeyPressed(KeyEvent event) { + switch (event.getCode()) { + case ESCAPE: + event.consume(); + cancelProposal(); + break; + case ENTER: + event.consume(); + applySelectedProposal(); + } + + +// if( event.getCode() == KeyCode.ESCAPE ) { +// event.consume(); +// this.stage.hide(); +// } else if( event.getCode() == KeyCode.BACK_SPACE ) { +// event.consume(); +// this.offset -= 1; +// try { +// this.viewer.getDocument().replace(this.offset, 1, ""); //$NON-NLS-1$ +// this.viewer.getTextWidget().setCaretOffset(this.offset); +// updateProposals(); +// } catch (BadLocationException e) { +// // TODO Auto-generated catch block +// e.printStackTrace(); +// } +// } else if( event.getCode() == KeyCode.ENTER ) { +// event.consume(); +// applySelection(); +// } else if( event.getCode() == KeyCode.LEFT ) { +// event.consume(); +// this.offset -= 1; +// this.offset = Math.max(0, this.offset); +// this.viewer.getTextWidget().setCaretOffset(this.offset); +// updateProposals(); +// } else if( event.getCode() == KeyCode.RIGHT ) { +// event.consume(); +// this.offset += 1; +// this.offset = Math.min(this.viewer.getDocument().getLength()-1, this.offset); +// this.viewer.getTextWidget().setCaretOffset(this.offset); +// updateProposals(); +// } + } + + private String getDocumentation(ICompletionProposal proposal) { + if (proposal != null) { + return "<html><body><pre>"+proposal.getHoverInfo()+"</pre></body></html>"; + } + return "<html></html>"; + } + private void setup() { if( this.stage == null ) { this.stage = new PopupWindow() { @@ -152,16 +247,22 @@ public class ContentProposalPopup { this.stage.setHeight(200); BorderPane p = new BorderPane(); p.setPrefHeight(200); - p.setPrefWidth(400); + p.setPrefWidth(600); this.stage.getScene().addEventFilter(KeyEvent.KEY_TYPED, this::handleKeyTyped); this.stage.getScene().addEventFilter(KeyEvent.KEY_PRESSED, this::handleKeyPressed); + this.stage.getScene().getStylesheets().addAll(this.viewer.getTextWidget().getScene().getStylesheets()); this.proposalList = new ListView<>(); + this.proposalList.setMinWidth(300); this.proposalList.getStyleClass().add("content-proposal-list"); //$NON-NLS-1$ - this.proposalList.setOnMouseClicked((e) -> { - if(e.getClickCount() == 1) { - applySelection(); - } + this.proposalList.setOnMouseClicked(this::handleMouseClicked); + + this.documentationView = new WebView(); + this.documentationView.setPrefWidth(300); + this.documentationView.getStyleClass().add("content-proposal-doc"); //$NON-NLS-1$ + + this.proposalList.getSelectionModel().selectedItemProperty().addListener((ChangeListener<ICompletionProposal>) (observable, oldValue, newValue) -> { + this.documentationView.getEngine().loadContent(getDocumentation(newValue)); }); Function<ICompletionProposal, CharSequence> label = (c) -> c.getLabel(); @@ -171,6 +272,7 @@ public class ContentProposalPopup { this.proposalList.setCellFactory((v) -> new SimpleListCell<ICompletionProposal>(label,graphic,css)); p.setCenter(this.proposalList); + p.setRight(this.documentationView); this.stage.getScene().setRoot(p); this.stage.focusedProperty().addListener((o) -> { if( this.stage != null && ! this.stage.isFocused() ) { @@ -183,4 +285,13 @@ public class ContentProposalPopup { }); } } + + @Override + public boolean verifyKey(VerifyEvent event) { + + System.err.println("CONTENT PROPOSAL POPUP: VerifyEvent " + event); + + return true; + } + } diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/ContextInformationPopup.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/ContextInformationPopup.java new file mode 100644 index 000000000..7c9c56143 --- /dev/null +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/ContextInformationPopup.java @@ -0,0 +1,175 @@ +/******************************************************************************* + * Copyright (c) 2000, 2015 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.eclipse.fx.text.ui.contentassist; + +import java.util.Iterator; +import java.util.Stack; + +import javax.swing.text.html.HTMLDocument.HTMLReader.HiddenAction; + +import org.eclipse.fx.text.ui.ITextViewer; +import org.eclipse.fx.ui.controls.styledtext.VerifyEvent; + +import javafx.geometry.Point2D; +import javafx.scene.control.Label; +import javafx.scene.layout.BorderPane; +import javafx.stage.Popup; +import javafx.stage.PopupWindow; + + +/** + * This class is used to present context information to the user. + * If multiple contexts are valid at the current cursor location, + * a list is presented from which the user may choose one context. + * Once the user makes their choice, or if there was only a single + * possible context, the context information is shown in a tool tip like popup. <p> + * If the tool tip is visible and the user wants to see context information of + * a context embedded into the one for which context information is displayed, + * context information for the embedded context is shown. As soon as the + * cursor leaves the embedded context area, the context information for + * the embedding context is shown again. + * + * @see IContextInformation + * @see IContextInformationValidator + */ +class ContextInformationPopup implements IContentAssistListener { + + + /** + * Represents the state necessary for embedding contexts. + * + * @since 2.0 + */ + static class ContextFrame { + + final int fBeginOffset; + final int fOffset; + final int fVisibleOffset; + final IContextInformation fInformation; + final IContextInformationValidator fValidator; + final IContextInformationPresenter fPresenter; + + /* + * @since 3.1 + */ + public ContextFrame(IContextInformation information, int beginOffset, int offset, int visibleOffset, IContextInformationValidator validator, IContextInformationPresenter presenter) { + fInformation = information; + fBeginOffset = beginOffset; + fOffset = offset; + fVisibleOffset = visibleOffset; + fValidator = validator; + fPresenter = presenter; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ContextFrame) { + ContextFrame frame= (ContextFrame) obj; + return fInformation.equals(frame.fInformation) && fBeginOffset == frame.fBeginOffset; + } + return super.equals(obj); + } + + @Override + public int hashCode() { + return (fInformation.hashCode() << 16) | fBeginOffset; + } + } + + private ITextViewer fViewer; + private ContentAssistant fContentAssistant; + + private PopupWindow fContextSelectorStage; + private IContextInformation[] fContextSelectorInput; + private String fLineDelimiter= null; + + private PopupWindow fContextInfoPopup; + private BorderPane fRoot; + private Label fContent; + + + private Stack<ContextFrame> fContextFrameStack= new Stack<>(); + + /** + * The last removed context frame is remembered in order to not re-query the + * user about which context should be used. + * + * @since 3.0 + */ + private ContextFrame fLastContext= null; + + /** + * Creates a new context information popup. + * + * @param contentAssistant the content assist for computing the context information + * @param viewer the viewer on top of which the context information is shown + */ + public ContextInformationPopup(ContentAssistant contentAssistant, ITextViewer viewer) { + fContentAssistant= contentAssistant; + fViewer= viewer; + + this.fContextInfoPopup = new PopupWindow() { + }; + this.fContextInfoPopup.setAutoFix(false); + this.fContextInfoPopup.setAutoHide(false); + viewer.getTextWidget().sceneProperty().addListener( e -> { + if( viewer.getTextWidget().getScene() != null ) { + fContextInfoPopup.getScene().getStylesheets().setAll(viewer.getTextWidget().getScene().getStylesheets()); + } + + }); + fRoot = new BorderPane(); + fRoot.getStyleClass().add("styled-text-hover"); + fContent = new Label(); + fRoot.setCenter(fContent); + fContent.getStyleClass().add("styled-text-hover-text"); + fContextInfoPopup.getScene().setRoot(fRoot); + } + + /** + * Shows all possible contexts for the given cursor position of the viewer. + * + * @param autoActivated <code>true</code> if auto activated + * @return a potential error message or <code>null</code> in case of no error + */ + public String showContextProposals(final boolean autoActivated) { + System.err.println("showContextProposals(" + autoActivated + ") (TODO)"); + return "TODO"; + } + + /** + * Displays the given context information for the given offset. + * + * @param info the context information + * @param offset the offset + * @since 2.0 + */ + public void showContextInformation(final IContextInformation info, final int offset) { + if (info != null && info.getInformationDisplayString() != null && !info.getInformationDisplayString().isEmpty()) { + fContent.setText(info.getInformationDisplayString()); + Point2D locationAtOffset = fViewer.getTextWidget().getLocationAtOffset(offset); + locationAtOffset = fViewer.getTextWidget().localToScreen(locationAtOffset); + if (locationAtOffset != null) { + fContextInfoPopup.show(fViewer.getTextWidget().getScene().getWindow(), locationAtOffset.getX(), locationAtOffset.getY()); + } + } + else { + fContextInfoPopup.hide(); + } + } + + @Override + public boolean verifyKey(VerifyEvent event) { + + return false; + } + +} diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/ICompletionProposal.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/ICompletionProposal.java index 63aa1370e..ad9aa1c1d 100644 --- a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/ICompletionProposal.java +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/ICompletionProposal.java @@ -17,9 +17,40 @@ import org.eclipse.jface.text.IDocument; public interface ICompletionProposal { public CharSequence getLabel(); + + public String getHoverInfo(); + public Node getGraphic(); // public List<String> getStyles(); + /** + * Inserts the proposed completion into the given document. + * + * @param document the document into which to insert the proposed completion + */ public void apply(IDocument document); + + /** + * Returns the new selection after the proposal has been applied to + * the given document in absolute document coordinates. If it returns + * <code>null</code>, no new selection is set. + * + * A document change can trigger other document changes, which have + * to be taken into account when calculating the new selection. Typically, + * this would be done by installing a document listener or by using a + * document position during {@link #apply(IDocument)}. + * + * @param document the document into which the proposed completion has been inserted + * @return the new selection + */ public TextSelection getSelection(IDocument document); + + /** + * Returns optional context information associated with this proposal. + * The context information will automatically be shown if the proposal + * has been applied. + * + * @return the context information for this proposal or <code>null</code> + */ + IContextInformation getContextInformation(); } diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/IContentAssistListener.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/IContentAssistListener.java new file mode 100644 index 000000000..055eac941 --- /dev/null +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/IContentAssistListener.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2000, 2005 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.eclipse.fx.text.ui.contentassist; + + +import org.eclipse.fx.ui.controls.styledtext.VerifyEvent; + + +/** + * An interface whereby listeners can not only receive key events, + * but can also consume them to prevent subsequent listeners from + * processing the event. + */ +interface IContentAssistListener { + + /** + * Verifies the key event. + * + * @param event the verify event + * @return <code>true</code> if processing should be continued by additional listeners + * @see org.eclipse.swt.custom.VerifyKeyListener#verifyKey(VerifyEvent) + */ + public boolean verifyKey(VerifyEvent event); +} diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/IContentAssistProcessor.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/IContentAssistProcessor.java new file mode 100644 index 000000000..bcf38a9a8 --- /dev/null +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/IContentAssistProcessor.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (c) 2000, 2005 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.eclipse.fx.text.ui.contentassist; + +import org.eclipse.fx.text.ui.ITextViewer; + + +/** + * A content assist processor proposes completions and + * computes context information for a particular content type. + * A content assist processor is a {@link org.eclipse.jface.text.contentassist.IContentAssistant} + * plug-in. + * <p> + * This interface must be implemented by clients. Implementers should be + * registered with a content assistant in order to get involved in the + * assisting process. + * </p> +*/ +public interface IContentAssistProcessor { + + /** + * Returns a list of completion proposals based on the + * specified location within the document that corresponds + * to the current cursor position within the text viewer. + * + * @param viewer the viewer whose document is used to compute the proposals + * @param offset an offset within the document for which completions should be computed + * @return an array of completion proposals or <code>null</code> if no proposals are possible + */ + ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset); + + /** + * Returns information about possible contexts based on the + * specified location within the document that corresponds + * to the current cursor position within the text viewer. + * + * @param viewer the viewer whose document is used to compute the possible contexts + * @param offset an offset within the document for which context information should be computed + * @return an array of context information objects or <code>null</code> if no context could be found + */ + IContextInformation[] computeContextInformation(ITextViewer viewer, int offset); + + /** + * Returns the characters which when entered by the user should + * automatically trigger the presentation of possible completions. + * + * @return the auto activation characters for completion proposal or <code>null</code> + * if no auto activation is desired + */ + char[] getCompletionProposalAutoActivationCharacters(); + + /** + * Returns the characters which when entered by the user should + * automatically trigger the presentation of context information. + * + * @return the auto activation characters for presenting context information + * or <code>null</code> if no auto activation is desired + */ + char[] getContextInformationAutoActivationCharacters(); + + /** + * Returns the reason why this content assist processor + * was unable to produce any completion proposals or context information. + * + * @return an error message or <code>null</code> if no error occurred + */ + String getErrorMessage(); + + /** + * Returns a validator used to determine when displayed context information + * should be dismissed. May only return <code>null</code> if the processor is + * incapable of computing context information. <p> + * + * @return a context information validator, or <code>null</code> if the processor + * is incapable of computing context information + */ + IContextInformationValidator getContextInformationValidator(); +} diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/IContextInformation.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/IContextInformation.java new file mode 100644 index 000000000..ac27014e6 --- /dev/null +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/IContextInformation.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * Copyright (c) 2000, 2008 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.eclipse.fx.text.ui.contentassist; + +import javafx.scene.Node; + + +/** + * The interface of context information presented to the user and + * generated by content assist processors. + * <p> + * In order to provide backward compatibility for clients of + * <code>IContextInformation</code>, extension interfaces are used to + * provide a means of evolution. The following extension interfaces + * exist: + * <ul> + * <li>{@link org.eclipse.jface.text.contentassist.IContextInformationExtension} + * since version 2.0 introducing the ability to freely position the + * context information.</li> + * </ul> + * </p> + * <p> + * The interface can be implemented by clients. By default, clients use + * {@link org.eclipse.jface.text.contentassist.ContextInformation} as + * the standard implementer of this interface. + * </p> + * + * @see IContentAssistProcessor + */ +public interface IContextInformation { + + /** + * Returns the string to be displayed in the list of contexts. + * This method is used to supply a unique presentation for + * situations where the context is ambiguous. These strings are + * used to allow the user to select the specific context. + * + * @return the string to be displayed for the context + */ + String getContextDisplayString(); + + /** + * Returns the image for this context information. + * The image will be shown to the left of the display string. + * + * @return the image to be shown or <code>null</code> if no image is desired + */ + Node getGraphic(); + + /** + * Returns the string to be displayed in the tool tip like information popup. + * + * @return the string to be displayed + */ + String getInformationDisplayString(); + + /** + * Compares the given object with this receiver. Two context informations are equal if there + * information display strings and their context display strings are equal. + * <p> + * <strong>Note:</strong> As specified in {@link Object#equals(Object)} clients will most likely + * also have to implement {@link Object#hashCode()}. + * </p> + * + * @see Object#equals(Object) + */ + @Override + boolean equals(Object object); + + /** + * Returns the start offset of the range for which this context + * information is valid or <code>-1</code> if unknown. + * + * @return the start offset of the range for which this context + * information is valid + */ + int getContextInformationPosition(); +} + + diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/IContextInformationPresenter.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/IContextInformationPresenter.java new file mode 100644 index 000000000..e8ed8da7d --- /dev/null +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/IContextInformationPresenter.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) 2000, 2005 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.eclipse.fx.text.ui.contentassist; + +import org.eclipse.fx.text.ui.ITextViewer; +import org.eclipse.fx.text.ui.TextPresentation; + + +/** + * A context information presenter determines the presentation + * of context information depending on a given document position. + * <p> + * The interface can be implemented by clients. + * </p> + * + * @since 2.0 + */ +public interface IContextInformationPresenter { + + /** + * Installs this presenter for the given context information. + * + * @param info the context information which this presenter should style + * @param viewer the text viewer on which the information is presented + * @param offset the document offset for which the information has been computed + */ + void install(IContextInformation info, ITextViewer viewer, int offset); + + /** + * Updates the given presentation of the given context information + * at the given document position. Returns whether update changed the + * presentation. + * + * @param offset the current offset within the document + * @param presentation the presentation to be updated + * @return <code>true</code> if the given presentation has been changed + */ + boolean updatePresentation(int offset, TextPresentation presentation); +} diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/IContextInformationValidator.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/IContextInformationValidator.java new file mode 100644 index 000000000..d34f415b1 --- /dev/null +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/contentassist/IContextInformationValidator.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) 2000, 2005 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.eclipse.fx.text.ui.contentassist; + +import org.eclipse.fx.text.ui.ITextViewer; + + +/** + * A context information validator is used to determine if + * a displayed context information is still valid or should + * be dismissed. + * <p> + * The interface can be implemented by clients. + * </p> + * + * @see IContextInformationPresenter + */ +public interface IContextInformationValidator { + + /** + * Installs this validator for the given context information. + * + * @param info the context information which this validator should check + * @param viewer the text viewer on which the information is presented + * @param offset the document offset for which the information has been computed + */ + void install(IContextInformation info, ITextViewer viewer, int offset); + + /** + * Returns whether the information this validator is installed on is still valid + * at the given document position. + * + * @param offset the current offset within the document + * @return <code>true</code> if the information also valid at the given document position + */ + boolean isContextInformationValid(int offset); +} diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/AnnotationModelSupport.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/AnnotationModelSupport.java new file mode 100644 index 000000000..70767a407 --- /dev/null +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/AnnotationModelSupport.java @@ -0,0 +1,198 @@ +package org.eclipse.fx.text.ui.internal; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +import org.eclipse.fx.core.Subscription; +import org.eclipse.fx.ui.controls.styledtext.StyledTextArea; +import org.eclipse.fx.ui.controls.styledtext.model.AnnotationProvider; +import org.eclipse.fx.ui.controls.styledtext.model.TextAnnotation; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.AnnotationModelEvent; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.jface.text.source.IAnnotationModelListener; +import org.eclipse.jface.text.source.IAnnotationModelListenerExtension; + +import com.google.common.collect.Range; +import com.google.common.collect.RangeSet; +import com.google.common.collect.TreeRangeSet; + +import javafx.application.Platform; + +public class AnnotationModelSupport { + + private IAnnotationModel annotationModel; + private StyledTextArea control; + + private Map<Integer, Set<MarkerAnnotation>> annotationCache = new HashMap<>(); + + public AnnotationModelSupport(IAnnotationModel model, StyledTextArea control) { + this.annotationModel = model; + this.control = control; + } + + static class MarkerAnnotation implements TextAnnotation, WrappedAnnotation { + public final Range<Integer> range; + public final Annotation annotation; + + private final String nfo; + + public MarkerAnnotation(Annotation annotation, Range<Integer> range) { + this.annotation = annotation; + this.range = range; + this.nfo = annotation.getType() + annotation.getText(); + } + + @Override + public Annotation getAnnotation() { + return annotation; + } + + @Override + public Range<Integer> getRange() { + return range; + } + + @Override + public String toString() { + return "MarkerAnnotation("+range+", "+nfo+")"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((nfo == null) ? 0 : nfo.hashCode()); + result = prime * result + ((range == null) ? 0 : range.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + MarkerAnnotation other = (MarkerAnnotation) obj; + if (nfo == null) { + if (other.nfo != null) + return false; + } else if (!nfo.equals(other.nfo)) + return false; + if (range == null) { + if (other.range != null) + return false; + } else if (!range.equals(other.range)) + return false; + return true; + } + + + + } + + class AnnotationModelAnnotationProvider implements AnnotationProvider { + @Override + public Set<? extends org.eclipse.fx.ui.controls.styledtext.model.Annotation> computeAnnotations(int index) { + return getAnnotations(index); + } + + + @Override + public Subscription registerChangeListener(Consumer<RangeSet<Integer>> onChange) { + AnnotationModelSupport.this.listener.add(onChange); + return new Subscription() { + @Override + public void dispose() { + AnnotationModelSupport.this.listener.remove(onChange); + } + }; + } + + + } + + private List<Consumer<RangeSet<Integer>>> listener = new ArrayList<>(); + + public void triggerChange(RangeSet<Integer> range) { + Platform.runLater(()-> { + this.listener.forEach(l->l.accept(range)); + }); + } + + private Set<MarkerAnnotation> findAnnotations(int lineIndex) { + long now = System.currentTimeMillis(); + Set<MarkerAnnotation> result = new HashSet<>(); + Iterator<Annotation> annotationIterator = annotationModel.getAnnotationIterator(); + while( annotationIterator.hasNext() ) { + Annotation a = annotationIterator.next(); + Position position = annotationModel.getPosition(a); + int curLineIndex = control.getLineAtOffset(position.offset); + if (curLineIndex == lineIndex) { + int lineBegin = control.getOffsetAtLine(curLineIndex); + int lineLength = control.getContent().getLine(curLineIndex).length(); + // TODO multiline = ???? + int lower = position.offset - lineBegin; + int upper = lower + Math.min(position.length, lineLength); + Range<Integer> lineLocalRange = Range.closed(lower, upper); + MarkerAnnotation annotation = new MarkerAnnotation(a, lineLocalRange); + result.add(annotation); + } + } +// System.err.println("AnnotationModelSupport: findAnnotations("+lineIndex+") needed " + (System.currentTimeMillis() - now) + "ms"); + return result; + } + + + + private Set<MarkerAnnotation> getAnnotations(int lineIndex) { + Set<MarkerAnnotation> set = annotationCache.get(lineIndex); + if (set == null) { + set = findAnnotations(lineIndex); + annotationCache.put(lineIndex, set); + } + return set; + } + + private void onAnnotationModelChange() { +// System.err.println("AnnotationModelSupport: clearing annotation cache"); + annotationCache.clear(); + RangeSet<Integer> range = TreeRangeSet.create(); + range.add(com.google.common.collect.Range.closed(0, Integer.MAX_VALUE)); + triggerChange(range); + + } + + class Listener implements IAnnotationModelListener, IAnnotationModelListenerExtension { + @Override + public void modelChanged(AnnotationModelEvent event) { + onAnnotationModelChange(); +// System.err.println("modelChanged"); +// System.err.println(" added: " + Arrays.toString(event.getAddedAnnotations())); +// System.err.println(" removed: " + Arrays.toString(event.getRemovedAnnotations())); +// System.err.println(" changed: " + Arrays.toString(event.getChangedAnnotations())); + } + + @Override + public void modelChanged(IAnnotationModel model) { + // full + } + + } + + + public void install() { + annotationModel.addAnnotationModelListener(new Listener()); + control.getAnnotationProvider().add(new AnnotationModelAnnotationProvider()); + } + +} diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/InvisibleCharSupport.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/InvisibleCharSupport.java new file mode 100644 index 000000000..49cc7285b --- /dev/null +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/InvisibleCharSupport.java @@ -0,0 +1,174 @@ +package org.eclipse.fx.text.ui.internal; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.fx.core.Subscription; +import org.eclipse.fx.ui.controls.styledtext.StyledTextArea; +import org.eclipse.fx.ui.controls.styledtext.StyledTextContent; +import org.eclipse.fx.ui.controls.styledtext.model.Annotation; +import org.eclipse.fx.ui.controls.styledtext.model.AnnotationProvider; +import org.eclipse.fx.ui.controls.styledtext.model.TextAnnotation; +import org.eclipse.fx.ui.controls.styledtext.model.TextAnnotationPresenter; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jface.text.source.IAnnotationModel; + +import com.google.common.collect.Range; +import com.google.common.collect.RangeSet; + +import javafx.scene.Node; +import javafx.scene.text.Text; + +public class InvisibleCharSupport { + + private StyledTextArea control; + + public InvisibleCharSupport(StyledTextArea control) { + this.control = control; + } + + + public class InvisibleCharAnnotation implements TextAnnotation { + + private final Range range; + private final String symbol; + + @Override + public Range getRange() { + return range; + } + + public String getSymbol() { + return symbol; + } + + public InvisibleCharAnnotation(String symbol, Range range) { + this.symbol = symbol; + this.range = range; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + getOuterType().hashCode(); + result = prime * result + ((range == null) ? 0 : range.hashCode()); + result = prime * result + ((symbol == null) ? 0 : symbol.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + InvisibleCharAnnotation other = (InvisibleCharAnnotation) obj; + if (!getOuterType().equals(other.getOuterType())) + return false; + if (range == null) { + if (other.range != null) + return false; + } else if (!range.equals(other.range)) + return false; + if (symbol == null) { + if (other.symbol != null) + return false; + } else if (!symbol.equals(other.symbol)) + return false; + return true; + } + + private InvisibleCharSupport getOuterType() { + return InvisibleCharSupport.this; + } + + + } + + public class InvisibleCharAnnotationPresenter implements TextAnnotationPresenter { + + @Override + public boolean isApplicable(Annotation annotation) { + return annotation instanceof InvisibleCharAnnotation; + } + + @Override + public Node createNode() { + Text n = new Text(); + n.getStyleClass().add("invisible-char"); + return n; + } + + @Override + public boolean isVisible(Annotation annotation) { + return true; + } + + @Override + public void updateNode(Node node, Annotation annotation) { + Text t = (Text) node; + InvisibleCharAnnotation a = (InvisibleCharAnnotation) annotation; + t.setText(a.getSymbol()); + } + + } + + public class InvisibleCharAnnotationProvider implements AnnotationProvider { + + @Override + public Set<? extends Annotation> computeAnnotations(int index) { + @NonNull + StyledTextContent content = control.getContent(); + + Set<InvisibleCharAnnotation> annotations = new HashSet<>(); + + int lineBegin = content.getOffsetAtLine(index); + int lineLength = content.getLine(index).length(); + + String line = content.getTextRange(lineBegin, lineLength); + + + int numOfLines = content.getLineCount(); + + // ADD TABS + Pattern tab = Pattern.compile("\\t"); + Matcher matcher = tab.matcher(line); + while (matcher.find()) { + annotations.add(new InvisibleCharAnnotation("\u21E5", Range.closed(matcher.start(), matcher.start() + 1))); + } + + // ADD NEWLINE + if (index < numOfLines-1) { + annotations.add(new InvisibleCharAnnotation("\u21B5", Range.closed(lineLength, lineLength +1))); + } + + return annotations; + } + + @Override + public Subscription registerChangeListener(Consumer<RangeSet<Integer>> onChange) { + return new Subscription() { + @Override + public void dispose() { + + } + }; + } + + } + + + + public void install() { + this.control.getAnnotationProvider().add(new InvisibleCharAnnotationProvider()); + this.control.getAnnotationPresenter().add(new InvisibleCharAnnotationPresenter()); + } +} diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/LineNumberSupport.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/LineNumberSupport.java new file mode 100644 index 000000000..5f61a1c85 --- /dev/null +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/LineNumberSupport.java @@ -0,0 +1,147 @@ +package org.eclipse.fx.text.ui.internal; + +import java.util.Collections; +import java.util.Set; +import java.util.function.Consumer; + +import org.eclipse.fx.core.Subscription; +import org.eclipse.fx.ui.controls.styledtext.StyledTextArea; +import org.eclipse.fx.ui.controls.styledtext.model.Annotation; +import org.eclipse.fx.ui.controls.styledtext.model.AnnotationProvider; +import org.eclipse.fx.ui.controls.styledtext.model.LineRulerAnnotationPresenter; + +import com.google.common.collect.RangeSet; + +import javafx.beans.binding.Bindings; +import javafx.beans.binding.DoubleBinding; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.scene.Node; +import javafx.scene.text.Text; + +public class LineNumberSupport { + + private StyledTextArea control; + + public LineNumberSupport(StyledTextArea control) { + this.control = control; + } + + + public class LineNrAnnotation implements Annotation { + + private final int nr; + + public LineNrAnnotation(int nr) { + this.nr = nr; + } + public int getNr() { + return nr; + } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + getOuterType().hashCode(); + result = prime * result + nr; + return result; + } + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + LineNrAnnotation other = (LineNrAnnotation) obj; + if (!getOuterType().equals(other.getOuterType())) + return false; + if (nr != other.nr) + return false; + return true; + } + private LineNumberSupport getOuterType() { + return LineNumberSupport.this; + } + + + } + + public class LineNrAnnotationPresenter implements LineRulerAnnotationPresenter { + + @Override + public LayoutHint getLayoutHint() { + return LayoutHint.ALIGN_RIGHT; + } + + @Override + public boolean isApplicable(Annotation annotation) { + return annotation instanceof LineNrAnnotation; + } + + @Override + public Node createNode() { + Text node = new Text(); + node.getStyleClass().add("line-ruler-text"); + return node; + } + + private DoubleProperty w = new SimpleDoubleProperty(16); + + @Override + public DoubleProperty getWidth() { + return w; + } + + @Override + public int getOrder() { + return 10000; + } + + @Override + public boolean isVisible(Set<Annotation> annotation) { + return true; + } + + @Override + public void updateNode(Node node, Set<Annotation> annotation) { + Text n = (Text) node; + annotation.stream().findFirst().ifPresent(m->{ + int nr = ((LineNrAnnotation)m).getNr(); + n.setText("" + nr); + }); + } + + + + } + + public class LineNrAnnotationProvider implements AnnotationProvider { + + @Override + public Set<? extends Annotation> computeAnnotations(int index) { + return Collections.singleton(new LineNrAnnotation(index + 1)); + } + + @Override + public Subscription registerChangeListener(Consumer<RangeSet<Integer>> onChange) { + return new Subscription() { + @Override + public void dispose() { + + } + }; + } + + } + + public void install() { + LineNrAnnotationPresenter presenter = new LineNrAnnotationPresenter(); + this.control.getAnnotationProvider().add(new LineNrAnnotationProvider()); + this.control.getAnnotationPresenter().add(presenter); + + DoubleBinding width = Bindings.createDoubleBinding(()->Integer.toString(this.control.lineCountProperty().get()).length() * 8d, this.control.lineCountProperty()); + presenter.w.bind(width); + } +} diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/TextAnnotationPresenterWrapper.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/TextAnnotationPresenterWrapper.java new file mode 100644 index 000000000..460bd42ed --- /dev/null +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/TextAnnotationPresenterWrapper.java @@ -0,0 +1,34 @@ +package org.eclipse.fx.text.ui.internal; + +import org.eclipse.fx.ui.controls.styledtext.model.Annotation; +import org.eclipse.fx.ui.controls.styledtext.model.TextAnnotationPresenter; + +import javafx.scene.Node; + +public class TextAnnotationPresenterWrapper implements TextAnnotationPresenter { + + @Override + public boolean isApplicable(Annotation annotation) { + // TODO Auto-generated method stub + return false; + } + + @Override + public Node createNode() { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean isVisible(Annotation annotation) { + // TODO Auto-generated method stub + return false; + } + + @Override + public void updateNode(Node node, Annotation annotation) { + // TODO Auto-generated method stub + + } + +} diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/WrappedAnnotation.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/WrappedAnnotation.java new file mode 100644 index 000000000..1de9aaf5f --- /dev/null +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/WrappedAnnotation.java @@ -0,0 +1,9 @@ +package org.eclipse.fx.text.ui.internal; + +import org.eclipse.jface.text.source.Annotation; + +public interface WrappedAnnotation { + + Annotation getAnnotation(); + +} diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/WrappedLineRulerAnnotationPresenter.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/WrappedLineRulerAnnotationPresenter.java new file mode 100644 index 000000000..9d57b36a7 --- /dev/null +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/WrappedLineRulerAnnotationPresenter.java @@ -0,0 +1,70 @@ +package org.eclipse.fx.text.ui.internal; + +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.fx.text.ui.source.ILineRulerAnnotationPresenter; +import org.eclipse.fx.ui.controls.styledtext.model.Annotation; +import org.eclipse.fx.ui.controls.styledtext.model.LineRulerAnnotationPresenter; + +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.scene.Node; + +public class WrappedLineRulerAnnotationPresenter implements LineRulerAnnotationPresenter { + + private ILineRulerAnnotationPresenter wrapped; + + public WrappedLineRulerAnnotationPresenter(ILineRulerAnnotationPresenter wrapped) { + this.wrapped = wrapped; + } + + @Override + public LayoutHint getLayoutHint() { + return LayoutHint.valueOf(wrapped.getLayoutHint().toString()); + } + + @Override + public int getOrder() { + return wrapped.getOrder(); + } + + private org.eclipse.jface.text.source.Annotation unwrap(Annotation annotation) { + return ((WrappedAnnotation) annotation).getAnnotation(); + } + + private Set<org.eclipse.jface.text.source.Annotation> unwrap(Set<Annotation> annotations) { + return annotations.stream().map(this::unwrap).collect(Collectors.toSet()); + } + + @Override + public boolean isApplicable(Annotation annotation) { + if (annotation instanceof WrappedAnnotation) { + return wrapped.isApplicable(unwrap(annotation)); + } + return false; + } + + @Override + public Node createNode() { + return wrapped.createNode(); + } + + DoubleProperty TODO = new SimpleDoubleProperty(16); + @Override + public DoubleProperty getWidth() { + // TODO + return TODO; + } + + @Override + public boolean isVisible(Set<Annotation> annotation) { + return true; + } + + @Override + public void updateNode(Node node, Set<Annotation> annotation) { + wrapped.updateNode(node, unwrap(annotation)); + } + +} diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/WrappedTextAnnotationPresenter.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/WrappedTextAnnotationPresenter.java new file mode 100644 index 000000000..986f89e6e --- /dev/null +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/internal/WrappedTextAnnotationPresenter.java @@ -0,0 +1,51 @@ +package org.eclipse.fx.text.ui.internal; + +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.fx.text.ui.source.ITextAnnotationPresenter; +import org.eclipse.fx.ui.controls.styledtext.model.Annotation; +import org.eclipse.fx.ui.controls.styledtext.model.TextAnnotationPresenter; + +import javafx.scene.Node; + +public class WrappedTextAnnotationPresenter implements TextAnnotationPresenter { + + private ITextAnnotationPresenter wrapped; + + public WrappedTextAnnotationPresenter(ITextAnnotationPresenter wrapped) { + this.wrapped = wrapped; + } + + private org.eclipse.jface.text.source.Annotation unwrap(Annotation annotation) { + return ((WrappedAnnotation) annotation).getAnnotation(); + } + + private Set<org.eclipse.jface.text.source.Annotation> unwrap(Set<Annotation> annotations) { + return annotations.stream().map(this::unwrap).collect(Collectors.toSet()); + } + + @Override + public boolean isApplicable(Annotation annotation) { + if (annotation instanceof WrappedAnnotation) { + return wrapped.isApplicable(unwrap(annotation)); + } + return false; + } + + @Override + public Node createNode() { + return wrapped.createNode(); + } + + @Override + public boolean isVisible(Annotation annotation) { + return true; + } + + @Override + public void updateNode(Node node, Annotation annotation) { + wrapped.updateNode(node, unwrap(annotation)); + } + +} diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/AnnotationPresenter.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/AnnotationPresenter.java index 4391469b6..bf9fe8c63 100644 --- a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/AnnotationPresenter.java +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/AnnotationPresenter.java @@ -10,13 +10,11 @@ *******************************************************************************/ package org.eclipse.fx.text.ui.source; -import java.util.List; +import java.util.Set; -import org.eclipse.jface.text.source.Annotation; +public interface AnnotationPresenter { -import javafx.scene.Node; + Set<ITextAnnotationPresenter> getTextAnnotationPresenter(); + Set<ILineRulerAnnotationPresenter> getLineRulerAnnotationPresenter(); -public interface AnnotationPresenter { - public List<String> getTypes(); - public Node getPresentation(Annotation annotation); } diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/IAnnotationPresenter.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/IAnnotationPresenter.java new file mode 100644 index 000000000..a75f1d036 --- /dev/null +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/IAnnotationPresenter.java @@ -0,0 +1,11 @@ +package org.eclipse.fx.text.ui.source; + +import org.eclipse.jface.text.source.Annotation; + +import javafx.scene.Node; + +public interface IAnnotationPresenter { + + boolean isApplicable(Annotation annotation); + Node createNode(); +} diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/ILineRulerAnnotationPresenter.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/ILineRulerAnnotationPresenter.java new file mode 100644 index 000000000..f992240b6 --- /dev/null +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/ILineRulerAnnotationPresenter.java @@ -0,0 +1,22 @@ +package org.eclipse.fx.text.ui.source; + +import java.util.Set; + +import org.eclipse.jface.text.source.Annotation; + +import javafx.scene.Node; + +public interface ILineRulerAnnotationPresenter extends IAnnotationPresenter { + + enum LayoutHint { + ALIGN_LEFT, + ALIGN_RIGHT, + ALIGN_CENTER; + } + + int getOrder(); + + void updateNode(Node node, Set<Annotation> annotation); + + LayoutHint getLayoutHint(); +} diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/ITextAnnotationPresenter.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/ITextAnnotationPresenter.java new file mode 100644 index 000000000..22ebb021f --- /dev/null +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/ITextAnnotationPresenter.java @@ -0,0 +1,18 @@ +package org.eclipse.fx.text.ui.source; + +import org.eclipse.jface.text.source.Annotation; + +import javafx.scene.Node; + +public interface ITextAnnotationPresenter extends IAnnotationPresenter { + + /** + * <p>Performance Hint: + * try to prevent adding nodes here! + * </p> + * @param node + * @param annotation + */ + void updateNode(Node node, Annotation annotation); + +} diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/SourceViewer.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/SourceViewer.java index 454fec1df..7abb16027 100644 --- a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/SourceViewer.java +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/SourceViewer.java @@ -12,16 +12,14 @@ *******************************************************************************/ package org.eclipse.fx.text.ui.source; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - import org.eclipse.fx.text.ui.ITextViewerExtension2; import org.eclipse.fx.text.ui.TextViewer; import org.eclipse.fx.text.ui.contentassist.IContentAssistant; +import org.eclipse.fx.text.ui.internal.AnnotationModelSupport; +import org.eclipse.fx.text.ui.internal.WrappedLineRulerAnnotationPresenter; +import org.eclipse.fx.text.ui.internal.WrappedTextAnnotationPresenter; import org.eclipse.fx.text.ui.presentation.IPresentationReconciler; import org.eclipse.fx.text.ui.reconciler.IReconciler; -import org.eclipse.fx.ui.controls.styledtext.StyledTextArea.StyledTextLine; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ISynchronizable; @@ -31,10 +29,6 @@ import org.eclipse.jface.text.source.Annotation; import org.eclipse.jface.text.source.AnnotationModel; import org.eclipse.jface.text.source.IAnnotationModel; import org.eclipse.jface.text.source.IAnnotationModelExtension; -import org.eclipse.jface.text.source.IAnnotationModelListener; - -import javafx.application.Platform; -import javafx.scene.Node; public class SourceViewer extends TextViewer implements ISourceViewer, ISourceViewerExtension, ISourceViewerExtension2, ISourceViewerExtension3, ISourceViewerExtension4 { @@ -42,7 +36,7 @@ public class SourceViewer extends TextViewer implements ISourceViewer, ISourceVi private IReconciler fReconciler; private IAnnotationModel fVisualAnnotationModel; protected final static Object MODEL_ANNOTATION_MODEL= new Object(); - private Map<String, AnnotationPresenter> presenterMap = new HashMap<>(); + private Annotation fRangeIndicator; @Override @@ -50,6 +44,10 @@ public class SourceViewer extends TextViewer implements ISourceViewer, ISourceVi if (getTextWidget() == null) return; +// installPlugin(new InvisibleCharacterPlugin()); + + + setUndoManager(configuration.getUndoManager(this)); getTextWidget().getStyleClass().add(configuration.getStyleclassName()); @@ -86,27 +84,37 @@ public class SourceViewer extends TextViewer implements ISourceViewer, ISourceVi //TODO This is complete different to JFace-Text IAnnotationModel annotationModel = configuration.getAnnotationModel(); if( annotationModel != null ) { - getTextWidget().setLineRulerGraphicNodeFactory(this::annotationFactory); - annotationModel.addAnnotationModelListener(new IAnnotationModelListener() { - - private boolean scheduled = false; - - @Override - public void modelChanged(IAnnotationModel model) { - if( ! this.scheduled ) { - this.scheduled = true; - Platform.runLater(() -> { - this.scheduled = false; - getTextWidget().refreshLineRuler(); - }); - } - } +// getTextWidget().setLineRulerGraphicNodeFactory(this::annotationFactory); - }); + // NFO: the line ruler is updated by changes to the annotation model + +// annotationModel.addAnnotationModelListener(new IAnnotationModelListener() { +// +// private boolean scheduled = false; +// +// @Override +// public void modelChanged(IAnnotationModel model) { +// if( ! this.scheduled ) { +// this.scheduled = true; +// Platform.runLater(() -> { +// this.scheduled = false; +// getTextWidget().refreshLineRuler(); +// }); +// } +// } +// +// }); } - if( configuration.getAnnotationPresenters() != null ) { - configuration.getAnnotationPresenters().stream().forEach(p -> p.getTypes().forEach( s -> this.presenterMap.put(s,p))); + if( configuration.getAnnotationPresenter() != null ) { + // install presenters + configuration.getAnnotationPresenter().getLineRulerAnnotationPresenter().forEach(p -> { + System.err.println("adding line ruler presenter " + p); + getTextWidget().getAnnotationPresenter().add(new WrappedLineRulerAnnotationPresenter(p)); + }); + configuration.getAnnotationPresenter().getTextAnnotationPresenter().forEach(p -> { + getTextWidget().getAnnotationPresenter().add(new WrappedTextAnnotationPresenter(p)); + }); } // presenterMap.putAll(configuration.getAnnotationPresenters().stream().collect(Collectors.toMap(p -> p.getType(), p -> p))); @@ -123,9 +131,21 @@ public class SourceViewer extends TextViewer implements ISourceViewer, ISourceVi setTextHover(configuration.getTextHover(this, t), t, ITextViewerExtension2.DEFAULT_HOVER_STATE_MASK); } - // register annotation model with text widget if (annotationModel != null) { + + AnnotationModelSupport support = new AnnotationModelSupport(annotationModel, getTextWidget()); + support.install(); + +// MarkerAnnotationPlugin markerAnnotationPlugin = new MarkerAnnotationPlugin(annotationModel, getTextWidget(), presenterMap); +// markerAnnotationPlugin.install(); + +// new AnnotationIconLineAnnotationPlugin(annotationModel, getTextWidget(), +// +// +// (a)->presenterMap.get(a.getType()).getPresentation(a)); + +// installPlugin(new AnnotationPlugin(annotationModel)); // annotationModel.addAnnotationModelListener(new IAnnotationModelListener() { // @Override // public void modelChanged(IAnnotationModel model) { @@ -192,24 +212,24 @@ public class SourceViewer extends TextViewer implements ISourceViewer, ISourceVi } - private Node annotationFactory(StyledTextLine l) { - //TODO Should use IAnnotationExtension2 - @SuppressWarnings("unchecked") - Iterator<Annotation> annotationIterator = this.fVisualAnnotationModel.getAnnotationIterator(); - while( annotationIterator.hasNext() ) { - Annotation a = annotationIterator.next(); - Position position = this.fVisualAnnotationModel.getPosition(a); - - if( l.getLineIndex() == getTextWidget().getContent().getLineAtOffset(position.offset) ) { - AnnotationPresenter annotationPresenter = this.presenterMap.get(a.getType()); - if( annotationPresenter != null ) { - return annotationPresenter.getPresentation(a); - } - return null; - } - } - return null; - } +// private Node annotationFactory(StyledTextLine l) { +// //TODO Should use IAnnotationExtension2 +// @SuppressWarnings("unchecked") +// Iterator<Annotation> annotationIterator = this.fVisualAnnotationModel.getAnnotationIterator(); +// while( annotationIterator.hasNext() ) { +// Annotation a = annotationIterator.next(); +// Position position = this.fVisualAnnotationModel.getPosition(a); +// +// if( l.getLineIndex() == getTextWidget().getContent().getLineAtOffset(position.offset) ) { +// AnnotationPresenter annotationPresenter = this.presenterMap.get(a.getType()); +// if( annotationPresenter != null ) { +// return annotationPresenter.getPresentation(a); +// } +// return null; +// } +// } +// return null; +// } /** * Disposes the visual annotation model. diff --git a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/SourceViewerConfiguration.java b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/SourceViewerConfiguration.java index 997e9b5be..a41830772 100644 --- a/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/SourceViewerConfiguration.java +++ b/bundles/code/org.eclipse.fx.text.ui/src/org/eclipse/fx/text/ui/source/SourceViewerConfiguration.java @@ -49,8 +49,8 @@ public abstract class SourceViewerConfiguration { return null; } - public List<AnnotationPresenter> getAnnotationPresenters() { - return Collections.emptyList(); + public AnnotationPresenter getAnnotationPresenter() { + return null; } public AnnotationPainter getAnnotationPainter(ISourceViewer sourceViewer) { diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/.settings/org.eclipse.pde.core.prefs b/bundles/runtime/org.eclipse.fx.ui.controls/.settings/org.eclipse.pde.core.prefs new file mode 100644 index 000000000..e313d3c3b --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/.settings/org.eclipse.pde.core.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1
+resolve.requirebundle=false
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/META-INF/MANIFEST.MF b/bundles/runtime/org.eclipse.fx.ui.controls/META-INF/MANIFEST.MF index 1456484f3..495004ff3 100644 --- a/bundles/runtime/org.eclipse.fx.ui.controls/META-INF/MANIFEST.MF +++ b/bundles/runtime/org.eclipse.fx.ui.controls/META-INF/MANIFEST.MF @@ -19,6 +19,7 @@ Export-Package: org.eclipse.fx.ui.controls;version="2.2.0", org.eclipse.fx.ui.controls.styledtext;version="2.2.0", org.eclipse.fx.ui.controls.styledtext.behavior;version="2.2.0";x-internal:=true, org.eclipse.fx.ui.controls.styledtext.events, + org.eclipse.fx.ui.controls.styledtext.model, org.eclipse.fx.ui.controls.styledtext.skin;version="2.2.0";x-internal:=true, org.eclipse.fx.ui.controls.table, org.eclipse.fx.ui.controls.tabpane;version="2.2.0", @@ -28,7 +29,8 @@ Bundle-ActivationPolicy: lazy Service-Component: OSGI-INF/services/org.eclipse.fx.ui.controls.styledtext.UnderlineStrategyFactory.xml, OSGI-INF/services/org.eclipse.fx.ui.controls.image.fontawesome.FontAwesomeIconFontProvider.xml Bundle-Vendor: Eclipse.org -Import-Package: org.eclipse.fx.core;version="2.2.0", +Import-Package: com.google.common.collect;version="15.0.0", + org.eclipse.fx.core;version="2.2.0", org.eclipse.fx.core.text;version="2.2.0", org.eclipse.fx.ui.panes;version="2.2.0", org.osgi.service.component.annotations;resolution:=optional diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/build.properties b/bundles/runtime/org.eclipse.fx.ui.controls/build.properties index 1e68d6ddd..c39a484ce 100644 --- a/bundles/runtime/org.eclipse.fx.ui.controls/build.properties +++ b/bundles/runtime/org.eclipse.fx.ui.controls/build.properties @@ -4,3 +4,4 @@ bin.includes = META-INF/,\ about.html,\
OSGI-INF/
source.. = src/
+additional.bundles = com.google.guava
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/DefaultContent.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/DefaultContent.java index b6a9d3c58..379ceced4 100644 --- a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/DefaultContent.java +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/DefaultContent.java @@ -880,7 +880,7 @@ class DefaultContent implements StyledTextContent { // then insert the new text
insert(start, newText);
// inform listeners
- TextChangedEvent textChanged = TextChangedEvent.textChanged(this);
+ TextChangedEvent textChanged = TextChangedEvent.textChanged(this, start, replaceLength, newText);
for (TextChangeListener l : this.textListeners) {
l.textChanged(textChanged);
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/FXBindUtil.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/FXBindUtil.java new file mode 100644 index 000000000..84ee9ea3f --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/FXBindUtil.java @@ -0,0 +1,102 @@ +package org.eclipse.fx.ui.controls.styledtext;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.eclipse.fx.core.Subscription;
+
+import javafx.beans.InvalidationListener;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+
+public class FXBindUtil {
+
+ public static <A, B> Subscription uniMapBindList(ObservableList<A> a, ObservableList<B> b, Function<A, B> map) {
+ final ListChangeListener<A> aChange = (ListChangeListener.Change<? extends A> change) -> {
+ while (change.next()) {
+// System.err.println("NEXT");
+// if (change.wasAdded() && change.wasRemoved()) System.err.println("WTF?!?");
+ if (change.wasPermutated()) {
+// System.err.println("permutate");
+ List<B> beforePermutate = b.subList(change.getFrom(), change.getTo());
+ for (int i = 0; i < beforePermutate.size(); i++) {
+ b.set(change.getPermutation(change.getFrom() + i), beforePermutate.get(i));
+ }
+ }
+
+ if (change.wasRemoved()) {
+// System.err.println("removed");
+ b.remove(change.getFrom(), change.getFrom() + change.getRemovedSize());
+ }
+ if (change.wasAdded()) {
+// System.err.println("added");
+ List<? extends A> added = change.getAddedSubList();
+ List<B> addedMapped = added.stream().map(map).collect(Collectors.toList());
+
+ b.addAll(change.getFrom(), addedMapped);
+
+ }
+ else if (change.wasUpdated() || change.wasReplaced()) {
+// System.err.println("updated/replaced");
+ List<? extends A> updated = a.subList(change.getFrom(), change.getTo());
+ List<B> updatedMapped = updated.stream().map(map).collect(Collectors.toList());
+ for (int i = 0; i < updatedMapped.size(); i++) {
+ b.set(change.getFrom(), updatedMapped.get(i));
+ }
+ }
+ }
+ };
+
+ a.addListener(aChange);
+
+ return new Subscription() {
+ @Override
+ public void dispose() {
+ a.removeListener(aChange);
+ }
+ };
+ }
+
+ public static void main(String[] args) {
+ ObservableList<String> a = FXCollections.observableArrayList();
+ ObservableList<Integer> b = FXCollections.observableArrayList();
+
+ uniMapBindList(a, b, Integer::valueOf);
+
+ b.addListener((InvalidationListener)(x)->System.err.println(b));
+
+
+ a.add("1");
+ assert(b.get(0) == 1);
+
+ a.add("2");
+ assert b.get(1) == 2;
+
+ a.add("3");
+ assert b.get(2) == 3;
+
+ a.remove("2");
+ assert b.get(1) == 3;
+
+ a.addAll(1, Arrays.asList("5", "6"));
+ assert b.get(1) == 5;
+ assert b.get(2) == 6;
+ assert b.get(3) == 3;
+
+ ArrayList<String> copy = new ArrayList<>(a);
+ String string = copy.get(2);
+ copy.remove(2);
+ copy.add(1, string);
+
+ a.setAll(copy);
+ assert b.get(1) == 6;
+ assert b.get(2) == 5;
+ }
+
+}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/NodeCachePane.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/NodeCachePane.java new file mode 100644 index 000000000..d9698bc55 --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/NodeCachePane.java @@ -0,0 +1,43 @@ +package org.eclipse.fx.ui.controls.styledtext;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+import java.util.function.Supplier;
+
+import javafx.scene.Node;
+import javafx.scene.layout.Pane;
+
+public class NodeCachePane extends Pane {
+
+ protected final ReuseCache<Node> cache;
+
+ public NodeCachePane(Supplier<Node> nodeFactory) {
+ cache = new ReuseCache<>(nodeFactory);
+ cache.addOnActivate(node->{
+ if (!getChildren().contains(node)) {
+ getChildren().add(node);
+ }
+ node.setVisible(true);
+ });
+ cache.addOnRelease( node -> getChildren().remove(node)); // FIXME modified by Tom leads to more and more text instances in AnnotationOverlay
+ cache.addOnClear( node -> getChildren().remove(node));
+ }
+
+ protected Node getNode() {
+ return cache.getElement();
+ }
+
+ protected void releaseNode(Node node) {
+ cache.releaseElement(node);
+ }
+
+ protected void cleanup() {
+
+ cache.clearFreeElements();
+ }
+
+
+}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/ReuseCache.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/ReuseCache.java new file mode 100644 index 000000000..e32e5ba6f --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/ReuseCache.java @@ -0,0 +1,66 @@ +package org.eclipse.fx.ui.controls.styledtext;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.Stack;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class ReuseCache<T> {
+
+ private Supplier<T> factory;
+
+ public ReuseCache(Supplier<T> factory) {
+ this.factory = factory;
+ }
+
+ private List<Consumer<T>> onActivate = new ArrayList<>();
+ private List<Consumer<T>> onRelease = new ArrayList<>();
+ private List<Consumer<T>> onClear = new ArrayList<>();
+
+ private Stack<T> free = new Stack<>();
+ private Set<T> active = new HashSet<>();
+
+ public void addOnActivate(Consumer<T> onActivate) {
+ this.onActivate.add(onActivate);
+ }
+
+ public void addOnRelease(Consumer<T> onRelease) {
+ this.onRelease.add(onRelease);
+ }
+
+ public void addOnClear(Consumer<T> onClear) {
+ this.onClear.add(onClear);
+ }
+
+ public T getElement() {
+ T element;
+ if (free.isEmpty()) {
+ element = factory.get();
+ }
+ else {
+ element = free.pop();
+ }
+ active.add(element);
+ onActivate.forEach(a->a.accept(element));
+ return element;
+ }
+
+ public void releaseElement(T element) {
+ active.remove(element);
+ onRelease.forEach(a->a.accept(element));
+// free.push(element); //FIXME Tom no caching as of NOW
+ }
+
+ public void clearFreeElements() {
+ free.clear();
+ }
+
+ public boolean isActive(T element) {
+ onClear.forEach(onClear->onClear.accept(element));
+ return active.contains(element);
+ }
+
+}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/StyledTextArea.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/StyledTextArea.java index 28b6d4d4e..d787dbbb8 100644 --- a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/StyledTextArea.java +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/StyledTextArea.java @@ -16,6 +16,8 @@ import java.lang.ref.WeakReference; import java.util.Collections; import org.eclipse.fx.ui.controls.styledtext.StyledTextContent.TextChangeListener; +import org.eclipse.fx.ui.controls.styledtext.model.AnnotationPresenter; +import org.eclipse.fx.ui.controls.styledtext.model.AnnotationProvider; import org.eclipse.fx.ui.controls.styledtext.skin.StyledTextSkin; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; @@ -23,17 +25,19 @@ import org.eclipse.jdt.annotation.Nullable; import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyIntegerProperty; +import javafx.beans.property.SetProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleSetProperty; +import javafx.collections.FXCollections; import javafx.geometry.Point2D; -import javafx.scene.Node; import javafx.scene.control.Control; import javafx.scene.control.Skin; import javafx.scene.input.Clipboard; import javafx.scene.input.DataFormat; import javafx.scene.paint.Color; -import javafx.util.Callback; /** * Control which allows to implemented a code-editor @@ -72,11 +76,6 @@ public class StyledTextArea extends Control { @NonNull final ObjectProperty<@NonNull StyledTextContent> contentProperty; -// @NonNull -// final SetProperty<@NonNull StyledTextAnnotation> annotationsProperty = new SimpleSetProperty<>(this, "annotations", FXCollections.observableSet()); //$NON-NLS-1$ -// public SetProperty<@NonNull StyledTextAnnotation> getAnnotations() { -// return this.annotationsProperty; -// } TextChangeListener textChangeListener = new TextChangeListener() { @Override @@ -107,9 +106,6 @@ public class StyledTextArea extends Control { @NonNull private final ObjectProperty<TextSelection> currentSelection = new SimpleObjectProperty<>(this, "currentSelection"); //$NON-NLS-1$ - @NonNull - private final ObjectProperty<@NonNull Callback<@NonNull StyledTextLine, @Nullable Node>> lineRulerGraphicNodeFactory = new SimpleObjectProperty<>(this, "lineRulerGraphicNodeFactory", e -> null); //$NON-NLS-1$ - /** * Separator for lines */ @@ -140,20 +136,16 @@ public class StyledTextArea extends Control { @NonNull private final ObjectProperty<@NonNull LineSeparator> lineSeparator = new SimpleObjectProperty<>(this, "lineSeparator", "\n".equals(System.getProperty("line.separator")) ? LineSeparator.NEW_LINE : LineSeparator.CARRIAGE_RETURN_NEW_LINE); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - /** - * Represents a line shown in the control - */ - public interface StyledTextLine { - /** - * @return the plain text value - */ - public String getText(); - /** - * @return the index of the line - */ - public int getLineIndex(); + private SetProperty<AnnotationPresenter> annotationPresenter = new SimpleSetProperty<>(this, "annotationPresenter", FXCollections.observableSet()); + public SetProperty<AnnotationPresenter> getAnnotationPresenter() { + return annotationPresenter; + } + + private SetProperty<AnnotationProvider> annotationProvider = new SimpleSetProperty<>(this, "annotationProvider", FXCollections.observableSet()); + public SetProperty<AnnotationProvider> getAnnotationProvider() { + return annotationProvider; } private int anchor; @@ -192,7 +184,15 @@ public class StyledTextArea extends Control { return USER_AGENT_STYLESHEET; } + private TextChangingEvent changingEvent; + + void handleTextChanging(TextChangingEvent event) { + System.err.println("handleTextChanging"); + changingEvent = event; + + this.renderer.textChanging(event); + if (event.replaceCharCount < 0) { event.offset += event.replaceCharCount; event.replaceCharCount *= -1; @@ -204,7 +204,7 @@ public class StyledTextArea extends Control { // this.lastTextChangeReplaceLineCount = event.replaceLineCount; this.lastTextChangeReplaceCharCount = event.replaceCharCount; - this.renderer.textChanging(event); + // Update the caret offset if it is greater than the length of the // content. @@ -216,35 +216,40 @@ public class StyledTextArea extends Control { setCaretOffset(newEndOfText/* , SWT.DEFAULT */); } - void handleTextSet(TextChangedEvent event) { - int newCharCount = getCharCount(); - if( this.caretOffsetProperty.get() > newCharCount ) { - this.caretOffsetProperty.set(newCharCount); - } + void handleTextChanged(TextChangedEvent xxx) { + System.err.println("handleTextChanged"); - // in SWT this is done in reset() - clearSelection(); + if (changingEvent == null) { + // full text change + int newCharCount = getCharCount(); + if( this.caretOffsetProperty.get() > newCharCount ) { + this.caretOffsetProperty.set(newCharCount); + } + + // in SWT this is done in reset() + clearSelection(); - if( getSkin() instanceof StyledTextSkin ) { - ((StyledTextSkin)getSkin()).recalculateItems(); +// if( getSkin() instanceof StyledTextSkin ) { +// ((StyledTextSkin)getSkin()).computeModel(); +// } } - } + else { + // partial text change + TextChangingEvent event = changingEvent; + changingEvent = null; - void handleTextChanged(TextChangedEvent event) { - // int firstLine = getContent().getLineAtOffset(lastTextChangeStart); - if (getSkin() instanceof StyledTextSkin) { - ((StyledTextSkin) getSkin()).recalculateItems(); +// if (getSkin() instanceof StyledTextSkin) { +// ((StyledTextSkin) getSkin()).computeModelDelta(event); +// } } + } - // TODO We need to re enable this in the future. - // For each change coming from outside (for example refactoring) - // we need to update the caret + void handleTextSet(TextChangedEvent event) { + System.err.println("handleTextSet"); + changingEvent = null; + } -// updateSelection(this.lastTextChangeStart, this.lastTextChangeReplaceCharCount, this.lastTextChangeNewCharCount); - // lastCharCount += lastTextChangeNewCharCount; - // lastCharCount -= lastTextChangeReplaceCharCount; - } void updateSelection(int startOffset, int replacedLength, int newLength) { if (getSelection().offset + getSelection().length > startOffset && getSelection().offset < startOffset + replacedLength) { @@ -534,9 +539,10 @@ public class StyledTextArea extends Control { this.renderer.setStyleRanges(ranges, styles); } - if (getSkin() instanceof StyledTextSkin) { - ((StyledTextSkin) getSkin()).recalculateItems(); - } +// if (getSkin() instanceof StyledTextSkin) { +// ((StyledTextSkin) getSkin()).requestRedraw(); +// } + } /** @@ -1465,6 +1471,12 @@ public class StyledTextArea extends Control { } + private IntegerProperty lineCount = new SimpleIntegerProperty(this, "lineCount", 0); + public ReadOnlyIntegerProperty lineCountProperty() { + return lineCount; + } + + /** * Paste the clipboard content */ @@ -1501,38 +1513,8 @@ public class StyledTextArea extends Control { } } - /** - * @return property holding the factory to create graphics in the ruler - */ - @NonNull - public final ObjectProperty<@NonNull Callback<@NonNull StyledTextLine, @Nullable Node>> lineRulerGraphicNodeFactoryProperty() { - return this.lineRulerGraphicNodeFactory; - } - - /** - * @return the current factory to create graphics in the ruler - */ - public final @NonNull Callback<@NonNull StyledTextLine, @Nullable Node> getLineRulerGraphicNodeFactory() { - return this.lineRulerGraphicNodeFactoryProperty().get(); - } - - /** - * Set a new factory to create graphics in the ruler - * - * @param lineRulerGraphicNodeFactory - * the factory - */ - public final void setLineRulerGraphicNodeFactory(final @NonNull Callback<@NonNull StyledTextLine, @Nullable Node> lineRulerGraphicNodeFactory) { - this.lineRulerGraphicNodeFactoryProperty().set(lineRulerGraphicNodeFactory); - } - - /** - * Refresh the line ruler - */ - public void refreshLineRuler() { - if (getSkin() != null) { - // TODO We need to send an event! - ((StyledTextSkin) getSkin()).refreshLineRuler(); - } + public int getOffsetAtPosition(double x, double y) { + int result = ((StyledTextSkin) getSkin()).getOffsetAtPosition(x, y); + return result; } } diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/StyledTextLayoutContainer.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/StyledTextLayoutContainer.java deleted file mode 100644 index 9d41dc27d..000000000 --- a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/StyledTextLayoutContainer.java +++ /dev/null @@ -1,487 +0,0 @@ -/*******************************************************************************
- * Copyright (c) 2015 BestSolution.at and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
- * which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v10.html
- *
- * Contributors:
- * Tom Schindl<tom.schindl@bestsolution.at> - initial API and implementation
- * Christoph Caks <ccaks@bestsolution.at> - improved editor behavior
- *******************************************************************************/
-package org.eclipse.fx.ui.controls.styledtext;
-
-import org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.Nullable;
-
-import javafx.animation.Animation;
-import javafx.animation.FadeTransition;
-import javafx.beans.Observable;
-import javafx.beans.binding.Bindings;
-import javafx.beans.property.IntegerProperty;
-import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.ReadOnlyBooleanProperty;
-import javafx.beans.property.SimpleBooleanProperty;
-import javafx.beans.property.SimpleIntegerProperty;
-import javafx.beans.property.SimpleObjectProperty;
-import javafx.collections.FXCollections;
-import javafx.collections.ObservableList;
-import javafx.geometry.Point2D;
-import javafx.scene.Node;
-import javafx.scene.layout.Region;
-import javafx.scene.shape.Line;
-import javafx.scene.text.TextFlow;
-import javafx.util.Duration;
-
-/**
- * A text layout container who is able to show selection
- *
- * @since 2.0
- */
-public class StyledTextLayoutContainer extends Region {
- @SuppressWarnings("null")
- @NonNull
- private final ObservableList<@NonNull StyledTextNode> textNodes = FXCollections.observableArrayList();
-
- @NonNull
- private final IntegerProperty caretIndex = new SimpleIntegerProperty(this, "caretIndex"); //$NON-NLS-1$
-
- @NonNull
- private final IntegerProperty startOffset = new SimpleIntegerProperty(this, "startOffset"); //$NON-NLS-1$
-
-
-// @NonNull
-// final SetProperty<@NonNull StyledTextAnnotation> annotationsProperty = new SimpleSetProperty<>(this, "annotations", FXCollections.observableSet()); //$NON-NLS-1$
-// public SetProperty<@NonNull StyledTextAnnotation> getAnnotations() {
-// return this.annotationsProperty;
-// }
-//
-// private Map<StyledTextAnnotation, Rectangle> annotationMarkers = new HashMap<>();
-
- /**
- * The start offset if used in a bigger context like {@link StyledTextArea}
- *
- * @return the offset property to observe
- */
- public final IntegerProperty startOffsetProperty() {
- return this.startOffset;
- }
-
- /**
- * the the caret index property.
- *
- * <p>the index or <code>-1</code> if caret is to be hidden</p>
- *
- * @return the caret index property
- */
- public final IntegerProperty caretIndexProperty() {
- return this.caretIndex;
- }
-
- /**
- * Set the caret index
- *
- * @param index
- * the index or <code>-1</code> if caret is to be hidden
- */
- public void setCaretIndex(int index) {
- this.caretIndex.set(index);
- }
-
- /**
- * Returns the caret index
- *
- * @return the index or <code>-1</code> if caret is to be hidden
- */
- public int getCaretIndex() {
- return this.caretIndex.get();
- }
-
-
- /**
- * The start offset if used in a bigger context like {@link StyledTextArea}
- *
- * @return the offset
- */
- public final int getStartOffset() {
- return this.startOffsetProperty().get();
- }
-
- /**
- * The start offset if used in a bigger context like {@link StyledTextArea}
- *
- * @param startOffset
- * the start offset
- */
- public final void setStartOffset(final int startOffset) {
- this.startOffsetProperty().set(startOffset);
- }
-
- @NonNull
- private final ObjectProperty<@NonNull TextSelection> selection = new SimpleObjectProperty<>(this, "selection", TextSelection.EMPTY); //$NON-NLS-1$
-
- /**
- * The selection currently shown
- *
- * @return the selection property to observe
- */
- public final ObjectProperty<@NonNull TextSelection> selectionProperty() {
- return this.selection;
- }
-
- /**
- * @return the text selection
- */
- public final @NonNull TextSelection getSelection() {
- return this.selectionProperty().get();
- }
-
- /**
- * The selection to be shown
- *
- * @param selection
- * the new selection
- */
- public final void setSelection(final @NonNull TextSelection selection) {
- this.selectionProperty().set(selection);
- }
-
- private final Region selectionMarker = new Region();
- final Line caret = new Line();
- private final TextFlow textLayoutNode = new TextFlow();
-
- private StyledTextNode selectionStartNode;
- private StyledTextNode selectionEndNode;
-
- private Animation caretAnimation;
-
- private final ReadOnlyBooleanProperty ownerFocusedProperty;
-
- /**
- * Create a container to layout text and allows to show e.g. a selection
- * range
- */
- public StyledTextLayoutContainer() {
- this(new SimpleBooleanProperty(true));
- }
-
- private static Animation createCaretAnimation(Node caret) {
- FadeTransition t = new FadeTransition(Duration.millis(400), caret);
- t.setAutoReverse(true);
- t.setFromValue(1);
- t.setToValue(0);
- t.setCycleCount(Animation.INDEFINITE);
- return t;
- }
-
- /**
- * Create a container to layout text and allows to show e.g. a selection
- * range
- *
- * @param ownerFocusedProperty
- * property identifing if the owner of the text container has the
- * input focus
- */
- @SuppressWarnings("null")
- public StyledTextLayoutContainer(ReadOnlyBooleanProperty ownerFocusedProperty) {
- this.ownerFocusedProperty = ownerFocusedProperty;
- getStyleClass().add("styled-text-layout-container"); //$NON-NLS-1$
- this.textNodes.addListener(this::recalculateOffset);
- this.selectionMarker.setVisible(false);
- this.selectionMarker.setManaged(false);
- this.selectionMarker.getStyleClass().add("selection-marker"); //$NON-NLS-1$
- this.caret.setVisible(false);
- this.caret.setStrokeWidth(2);
- this.caret.setManaged(false);
- this.caret.getStyleClass().add("text-caret"); //$NON-NLS-1$
-
- this.caretAnimation = createCaretAnimation(this.caret);
-
- Bindings.bindContent(this.textLayoutNode.getChildren(), this.textNodes);
- getChildren().setAll(this.selectionMarker, this.textLayoutNode, this.caret);
- selectionProperty().addListener(this::handleSelectionChange);
- this.textNodes.addListener(this::handleSelectionChange);
-
- this.ownerFocusedProperty.addListener(this::updateCaretVisibility);
- this.caretIndex.addListener(this::updateCaretVisibility);
-
- updateCaretVisibility(null);
-
-// this.annotationsProperty.addListener(new SetChangeListener<StyledTextAnnotation>() {
-// @Override
-// public void onChanged(javafx.collections.SetChangeListener.Change<? extends StyledTextAnnotation> change) {
-// System.err.println("ON ANNOTATION MARKER CHANGE");
-// if (change.getElementAdded() != null) {
-// StyledTextAnnotation a = change.getElementAdded();
-//
-// Rectangle child = new Rectangle();
-//
-// if (a.getType().contains("ERROR")) {
-// child.setFill(Color.RED);
-// }
-// else if (a.getType().contains("WARN")) {
-// child.setFill(Color.YELLOW);
-// }
-// child.setOpacity(0.3);
-//
-// Tooltip tt = new Tooltip();
-// tt.setText(a.getText());
-// getChildren().add(child);
-// Tooltip.install(child, tt);
-//
-// annotationMarkers.put(a, child);
-// }
-// if (change.getElementRemoved() != null) {
-// StyledTextAnnotation a = change.getElementRemoved();
-//
-// Rectangle child = annotationMarkers.remove(a);
-// if (child != null) {
-// getChildren().remove(child);
-// }
-// }
-//
-// requestLayout();
-// }
-// });
- }
-
- private void updateCaretVisibility(Observable o) {
- if (this.ownerFocusedProperty.get() && this.caretIndex.get() != -1) {
- showCaret();
- }
- else {
- hideCaret();
- }
- }
-
- private void showCaret() {
- this.caret.setVisible(true);
- this.caretAnimation.play();
- requestLayout();
- }
-
- private void hideCaret() {
- this.caret.setVisible(false);
- this.caretAnimation.stop();
- requestLayout();
- }
-
- private int getEndOffset() {
- return getStartOffset() + getText().length();
- }
-
- /**
- * Check if the offset is between the start and end
- *
- * @param start
- * the start
- * @param end
- * the end
- * @return <code>true</code> if intersects the offset
- */
- public boolean intersectOffset(int start, int end) {
- if (getStartOffset() > end) {
- return false;
- } else if (getEndOffset() < start) {
- return false;
- }
- return true;
- }
-
- private void recalculateOffset(Observable o) {
- int offset = 0;
- for (StyledTextNode t : this.textNodes) {
- t.setStartOffset(offset);
- offset = t.getEndOffset();
- }
- }
-
- private void handleSelectionChange(Observable o) {
- TextSelection selection = getSelection();
- if (selection.length == 0) {
- this.selectionMarker.setVisible(false);
- }
- else {
- this.selectionMarker.setVisible(true);
- int startOffset = selection.offset;
- int endOffset = selection.offset + selection.length;
-
- this.selectionStartNode = null;
- this.selectionEndNode = null;
- for (StyledTextNode t : this.textNodes) {
- if (t.intersectOffset(startOffset, endOffset)) {
- if (this.selectionStartNode == null) {
- this.selectionStartNode = t;
- }
- this.selectionEndNode = t;
- }
- }
- requestLayout();
- }
- }
-
- @Override
- protected double computePrefHeight(double width) {
- double d = super.computePrefHeight(width);
- return d;
- }
-
-// private double findX(int localOffset) {
-//
-// double len = 0;
-// for (StyledTextNode t : this.textNodes) {
-// if (t.getStartOffset() <= localOffset && t.getEndOffset() > localOffset || this.textNodes.get(this.textNodes.size() - 1) == t) {
-// return len + t.getCharLocation(localOffset - t.getStartOffset());
-// }
-// len += t.getWidth();
-// }
-// return -1;
-// }
-
- private void layoutAnnotations() {
-// for (Entry<StyledTextAnnotation, Rectangle> e : annotationMarkers.entrySet()) {
-// System.err.println("LAYOUTING MARKER: " + e.getKey().getText());
-// final int globalBeginIndex = e.getKey().getStartOffset();
-// final int globalEndIndex = e.getKey().getStartOffset() + e.getKey().getLength();
-//
-// System.err.println("global: " + globalBeginIndex + " - " + globalEndIndex);
-//
-// final int localBeginIndex = Math.max(0, globalBeginIndex - getStartOffset());
-// final int localEndIndex = Math.min(getText().length(), globalEndIndex - getStartOffset());
-//
-// System.err.println("local: " + localBeginIndex + " - " + localEndIndex);
-//
-// double xBegin = findX(localBeginIndex);
-// double xEnd = findX(localEndIndex);
-//
-//// System.err.println(xBegin + ", " + getInsets().getTop() + ", " + (xEnd - xBegin)+ ", " + textLayoutNode.prefHeight(-1));
-//// e.getValue().resizeRelocate(xBegin, getInsets().getTop(), xEnd - xBegin, textLayoutNode.prefHeight(-1));
-// e.getValue().setX(xBegin);
-// e.getValue().setY(getInsets().getTop());
-// e.getValue().setWidth(xEnd - xBegin);
-// e.getValue().setHeight(textLayoutNode.prefHeight(-1));
-// e.getValue().toFront();
-// System.err.println(" -> " + e.getValue());
-// }
- }
-
- private void layoutSelection() {
- if (this.selectionStartNode != null && this.selectionEndNode != null) {
- final TextSelection selection = getSelection();
- final double selectionStart = this.selectionStartNode.getLayoutX() + this.selectionStartNode.getCharLocation(selection.offset - this.selectionStartNode.getStartOffset());
- final double selectionEnd = this.selectionEndNode.getLayoutX() + this.selectionEndNode.getCharLocation(selection.offset + selection.length - this.selectionEndNode.getStartOffset());
- this.selectionMarker.resizeRelocate(selectionStart, 0, selectionEnd - selectionStart, getHeight());
- }
- }
-
- private void layoutCaret() {
- if (this.getCaretIndex() >= 0) {
- for (StyledTextNode t : this.textNodes) {
- // XXX do we really need to apply the css on the StyledTextNode here??
-// t.applyCss();
- if (t.getStartOffset() <= this.getCaretIndex() && (t.getEndOffset() > this.getCaretIndex() || this.textNodes.get(this.textNodes.size() - 1) == t)) {
- double caretX = t.getCharLocation(this.getCaretIndex() - t.getStartOffset());
- double x = this.textLayoutNode.localToParent(t.getBoundsInParent().getMinX(), 0).getX() + caretX;
-
- double h = getHeight();
-
- this.caret.setStartX(x);
- this.caret.setEndX(x);
- this.caret.setStartY(getInsets().getTop() + 1);
- this.caret.setEndY(h + getInsets().getTop() + 1);
- this.caret.toFront();
- return;
- }
- }
-
- this.caret.setStartX(0);
- this.caret.setEndX(0);
- this.caret.setStartY(0);
- this.caret.setEndY(15);
-
- } else {
- this.caret.setStartX(0);
- this.caret.setEndX(0);
- this.caret.setStartY(0);
- this.caret.setEndY(getHeight());
- }
- }
-
- @Override
- protected void layoutChildren() {
- super.layoutChildren();
-
- this.textLayoutNode.relocate(getInsets().getLeft(), getInsets().getTop());
-
- // we need to ensure that the text is layouted correctly before processing any kind of markers
- // since we need the correct letter positions to place them
- this.textLayoutNode.layout();
- this.textLayoutNode.applyCss();
-
- layoutSelection();
- layoutCaret();
-
- layoutAnnotations();
-
- }
-
- /**
- * @return list of text nodes rendered
- */
- public @NonNull ObservableList<@NonNull StyledTextNode> getTextNodes() {
- return this.textNodes;
- }
-
- /**
- * Find the caret index at the give point
- *
- * @param point
- * the point relative to coordinate system of this node
- * @return the index or <code>-1</code> if not found
- */
- public int getCaretIndexAtPoint(Point2D point) {
- Point2D scenePoint = localToScene(point);
- for (StyledTextNode t : this.textNodes) {
- if (t.localToScene(t.getBoundsInLocal()).contains(scenePoint)) {
- int idx = t.getCaretIndexAtPoint(t.sceneToLocal(scenePoint));
- if( idx != -1 ) {
- return idx + t.getStartOffset();
- }
- }
- }
-
- return -1;
- }
-
- private String getText() {
- StringBuilder b = new StringBuilder();
- for (StyledTextNode t : this.textNodes) {
- b.append(t.getText());
- }
- return b.toString();
- }
-
- /**
- * Find the position of a the caret at a given index
- *
- * @param index
- * the index
- * @return the location relative to this node or <code>null</code>
- */
- public @Nullable Point2D getCareLocation(int index) {
- for (StyledTextNode t : this.textNodes) {
- if (t.getStartOffset() <= index && t.getEndOffset() >= index) {
- double x = t.getCharLocation(index);
- return sceneToLocal(t.localToScene(x, 0));
- }
- }
- return null;
- }
-
- /**
- * Releases resources immediately
- */
- public void dispose() {
- this.caretAnimation.stop();
- }
-}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/TextChangedEvent.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/TextChangedEvent.java index aa4372eab..b40e6c0ab 100644 --- a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/TextChangedEvent.java +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/TextChangedEvent.java @@ -71,6 +71,11 @@ public class TextChangedEvent { return new TextChangedEvent(source, 0, 0, 0, null, 0, 0); } + public static TextChangedEvent textChanged(StyledTextContent source, int offset, int replaceLength, String newText) { + return new TextChangedEvent(source, offset, replaceLength, -1, newText, newText.length(), -1); + } + + /** * Create the text set event * @@ -82,4 +87,11 @@ public class TextChangedEvent { return new TextChangedEvent(source, 0, 0, 0, null, 0, 0); } + + @Override + public String toString() { + return "TextChangedEvent(offset="+this.offset+", replaceCharCount="+this.replaceCharCount+", replaceText=" + (this.newText == null ? "null" : "\""+this.newText+"\"") + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ + } + + } diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/TextChangingEvent.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/TextChangingEvent.java index 8c837812c..aa31ef73c 100644 --- a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/TextChangingEvent.java +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/TextChangingEvent.java @@ -82,4 +82,9 @@ public class TextChangingEvent { public static TextChangingEvent textChanging(StyledTextContent source, int offset, int replaceCharCount, int replaceLineCount, String newText, int newCharCount, int newLineCount) { return new TextChangingEvent(source, offset, replaceCharCount, replaceLineCount, newText, newCharCount, newLineCount); } + + @Override + public String toString() { + return "TextChangingEvent(offset="+this.offset+", replaceCharCount="+this.replaceCharCount+", replaceText=" + (this.newText == null ? "null" : "\""+this.newText+"\"") + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ + } } diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/VerticalLineFlow.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/VerticalLineFlow.java new file mode 100644 index 000000000..6dba57505 --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/VerticalLineFlow.java @@ -0,0 +1,158 @@ +package org.eclipse.fx.ui.controls.styledtext;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.Stack;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.ListProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.property.SimpleListProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.scene.Node;
+import javafx.scene.layout.Pane;
+
+import com.google.common.collect.ContiguousSet;
+import com.google.common.collect.DiscreteDomain;
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import com.google.common.collect.TreeRangeSet;
+
+public class VerticalLineFlow<M, N> extends NodeCachePane {
+
+ private Predicate<Set<N>> needsPresentation;
+
+ private BiConsumer<Node, Set<N>> nodePopulator;
+ private Function<Integer, Set<N>> converter;
+ protected Map<Integer, Double> yOffsetData = new HashMap<>();
+
+ private DoubleProperty lineHeigth = new SimpleDoubleProperty(this, "lineHeight", 16.0);
+ public DoubleProperty lineHeightProperty() {
+ return this.lineHeigth;
+ }
+ public double getLineHeight() {
+ return this.lineHeigth.get();
+ }
+ public void setLineHeight(double lineHeight) {
+ this.lineHeigth.set(lineHeight);
+ }
+
+// private ListProperty<M> model = new SimpleListProperty<>(this, "model", FXCollections.observableArrayList());
+// public ListProperty<M> getModel() {
+// return this.model;
+// }
+
+ private IntegerProperty numberOfLines = new SimpleIntegerProperty(this, "numberOfLines", 0);
+ public IntegerProperty numberOfLinesProperty() {
+ return this.numberOfLines;
+ }
+
+ private ObjectProperty<Range<Integer>> visibleLines = new SimpleObjectProperty<>(this, "visibleLines", Range.all()); //$NON-NLS-1$
+ public ObjectProperty<Range<Integer>> visibleLinesProperty() {
+ return this.visibleLines;
+ }
+ public RangeSet<Integer> getVisibleLines() {
+ RangeSet<Integer> visibleLines = TreeRangeSet.create();
+ visibleLines.add(this.visibleLines.get());
+ return visibleLines.subRangeSet(Range.closedOpen(0, numberOfLines.get()));
+ }
+
+ public void setVisibleLines(Range<Integer> visibleLines) {
+ this.visibleLines.set(visibleLines);
+ }
+
+ public VerticalLineFlow(Function<Integer, Set<N>> converter, Predicate<Set<N>> needsPresentation, Supplier<Node> nodeFactory, BiConsumer<Node, Set<N>> nodePopulator) {
+ super(nodeFactory);
+ this.needsPresentation = needsPresentation;
+ this.nodePopulator = nodePopulator;
+ this.converter = converter;
+
+ this.visibleLines.addListener((InvalidationListener)(x)->prepareNodes(getVisibleLines()));
+ this.numberOfLines.addListener((InvalidationListener)(x)->prepareNodes(getVisibleLines()));
+ }
+
+ protected Map<Integer, Node> activeNodes = new HashMap<>();
+
+ protected void releaseNode(int lineIndex) {
+ Node node = activeNodes.remove(lineIndex);
+ if (node != null) {
+ releaseNode(node);
+ }
+ }
+
+ protected Node getNode(int lineIndex) {
+ Node node = activeNodes.get(lineIndex);
+
+ if (node == null) {
+ node = getNode();
+ }
+
+ activeNodes.put(lineIndex, node);
+ return node;
+ }
+
+ private void prepareNodes(RangeSet<Integer> range) {
+ if (range == null) return;
+
+// System.err.println("VerticalLineFlow: prepareNodes " + range);
+
+ // release invisible nodes
+ Iterator<Entry<Integer, Node>> iterator = this.activeNodes.entrySet().iterator();
+ while (iterator.hasNext()) {
+ Entry<Integer, Node> entry = iterator.next();
+ int index = entry.getKey();
+ if (!range.contains(index)) {
+ releaseNode(entry.getValue());
+ iterator.remove();
+ }
+ }
+
+ // prepare range nodes
+ range.asRanges().forEach(r->ContiguousSet.create(r, DiscreteDomain.integers()).forEach(index -> prepareNode(index)));
+
+ }
+
+ private void prepareNode(int lineIndex) {
+ Node node = getNode(lineIndex);
+ nodePopulator.accept(node, converter.apply(lineIndex));
+ }
+
+ public void setLineOffset(int lineIndex, double yOffset) {
+ yOffsetData.put(lineIndex, yOffset);
+ requestLayout();
+ }
+
+ @Override
+ protected void layoutChildren() {
+ this.activeNodes.entrySet().forEach(e -> {
+ if (!yOffsetData.containsKey(e.getKey())) {
+ System.err.println("NO DATA FOR " + e);
+ return;
+ }
+ double x = 0;
+ double y = yOffsetData.get(e.getKey());
+ double width = getWidth();
+ double height = getLineHeight();
+ e.getValue().resizeRelocate(x, y, width, height);
+ });
+ }
+
+
+
+ public void update(RangeSet<Integer> r) {
+ prepareNodes(r.subRangeSet(visibleLines.get()).subRangeSet(Range.closedOpen(0, numberOfLines.get())));
+ }
+
+}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/behavior/HoverSupport.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/behavior/HoverSupport.java index 166d8cf0a..69bb1184b 100644 --- a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/behavior/HoverSupport.java +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/behavior/HoverSupport.java @@ -12,15 +12,13 @@ package org.eclipse.fx.ui.controls.styledtext.behavior;
import org.eclipse.fx.ui.controls.Util;
-import org.eclipse.fx.ui.controls.styledtext.StyledTextLayoutContainer;
-import org.eclipse.fx.ui.controls.styledtext.StyledTextNode;
import org.eclipse.fx.ui.controls.styledtext.events.TextHoverEvent;
import javafx.event.Event;
+import javafx.event.EventTarget;
import javafx.scene.Node;
-import javafx.scene.Parent;
-import javafx.scene.control.Control;
import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Region;
import javafx.util.Duration;
/**
@@ -30,10 +28,10 @@ import javafx.util.Duration; */
public class HoverSupport {
- private Control control;
+ private Region control;
private TextHoverEvent lastHover;
- public HoverSupport(Control control) {
+ public HoverSupport(Region control) {
this.control = control;
}
@@ -45,7 +43,7 @@ public class HoverSupport { }
- public static HoverSupport install(Control control) {
+ public static HoverSupport install(Region control) {
HoverSupport support = new HoverSupport(control);
support.install();
return support;
@@ -59,6 +57,7 @@ public class HoverSupport { }
private void onMouseMoved(MouseEvent event) {
+ System.err.println("moved " + event.getTarget());
if( this.lastHover != null ) {
TextHoverEvent hoverEvent = createHoverEvent(event);
if( this.lastHover.getOffsetTokenStart() != hoverEvent.getOffsetTokenStart() ) {
@@ -77,26 +76,34 @@ public class HoverSupport { protected static TextHoverEvent createHoverEvent(MouseEvent e) {
- Parent parent = ((Node)e.getTarget()).getParent();
- if( parent instanceof StyledTextNode ) {
- StyledTextNode n = (StyledTextNode) parent;
- if( n.getParent() == null || n.getParent().getParent() == null ) {
- return new TextHoverEvent(e, -1, -1, -1, ""); //$NON-NLS-1$
- }
- StyledTextLayoutContainer lc = (StyledTextLayoutContainer) n.getParent().getParent();
- int start = lc.getStartOffset() + n.getStartOffset();
- int end = lc.getStartOffset() + n.getEndOffset();
- String text = n.getText();
-
- int offset = n.getCaretIndexAtPoint(n.sceneToLocal(e.getSceneX(), e.getSceneY()));
- return new TextHoverEvent(e, start, end, start + offset, text);
- } else {
- return new TextHoverEvent(e, -1, -1, -1, ""); //$NON-NLS-1$
- }
+
+ System.err.println("createHover for " + e.getTarget());
+
+ // TODO fix hover event
+// if (e.getTarget() instanceof Annotation)
+
+ return new TextHoverEvent(e, -1, -1, -1, "");
+// Parent parent = ((Node)e.getTarget()).getParent();
+// if( parent instanceof StyledTextNode ) {
+// StyledTextNode n = (StyledTextNode) parent;
+// if( n.getParent() == null || n.getParent().getParent() == null ) {
+// return new TextHoverEvent(e, -1, -1, -1, ""); //$NON-NLS-1$
+// }
+// StyledTextLayoutContainer lc = (StyledTextLayoutContainer) n.getParent().getParent();
+// int start = lc.getStartOffset() + n.getStartOffset();
+// int end = lc.getStartOffset() + n.getEndOffset();
+// String text = n.getText();
+//
+// int offset = n.getCaretIndexAtPoint(n.sceneToLocal(e.getSceneX(), e.getSceneY()));
+// return new TextHoverEvent(e, start, end, start + offset, text);
+// } else {
+// return new TextHoverEvent(e, -1, -1, -1, ""); //$NON-NLS-1$
+// }
}
protected void handleHover(MouseEvent e) {
TextHoverEvent event = createHoverEvent(e);
+ System.err.println("hoverEvent " + event);
if( this.lastHover == null || this.lastHover.getOffsetTokenStart() != event.getOffsetTokenStart() ) {
Event.fireEvent(control, event);
if( event.getOffset() == -1 ) {
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/behavior/StyledTextBehavior.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/behavior/StyledTextBehavior.java index 4f3b53d19..cee4c505d 100644 --- a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/behavior/StyledTextBehavior.java +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/behavior/StyledTextBehavior.java @@ -17,7 +17,6 @@ import static javafx.scene.input.KeyCode.C; import static javafx.scene.input.KeyCode.D; import static javafx.scene.input.KeyCode.DELETE; import static javafx.scene.input.KeyCode.DOWN; -import static javafx.scene.input.KeyCode.E; import static javafx.scene.input.KeyCode.END; import static javafx.scene.input.KeyCode.ENTER; import static javafx.scene.input.KeyCode.HOME; @@ -50,18 +49,15 @@ import org.eclipse.fx.ui.controls.styledtext.ActionEvent; import org.eclipse.fx.ui.controls.styledtext.ActionEvent.ActionType; import org.eclipse.fx.ui.controls.styledtext.StyledTextArea; import org.eclipse.fx.ui.controls.styledtext.StyledTextContent; -import org.eclipse.fx.ui.controls.styledtext.StyledTextLayoutContainer; import org.eclipse.fx.ui.controls.styledtext.TextSelection; import org.eclipse.fx.ui.controls.styledtext.VerifyEvent; import org.eclipse.fx.ui.controls.styledtext.behavior.StyledTextBehavior.KeyMapping.InputAction; import org.eclipse.fx.ui.controls.styledtext.behavior.StyledTextBehavior.KeyMapping.KeyCombo; import org.eclipse.fx.ui.controls.styledtext.events.TextPositionEvent; import org.eclipse.fx.ui.controls.styledtext.skin.StyledTextSkin; -import org.eclipse.fx.ui.controls.styledtext.skin.StyledTextSkin.LineCell; import org.eclipse.jdt.annotation.NonNull; import javafx.event.Event; -import javafx.geometry.Bounds; import javafx.scene.control.Control; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; @@ -103,7 +99,7 @@ public class StyledTextBehavior { } // called from skin - public void installContentListeners(final Control contentNode) { + public void installContentListeners(final javafx.scene.layout.Region contentNode) { this.textPositionSupport = TextPositionSupport.install(contentNode, getControl()); this.hoverSupport = HoverSupport.install(contentNode); } @@ -1324,49 +1320,50 @@ public class StyledTextBehavior { // action for insert tab support keyMapping.mapKey(new KeyCombo(TAB), ()->getControl().insert("\t")); //$NON-NLS-1$ - } - - /** - * computes the text offset under the mouse cursor. - * @param event - * @return the offset - */ - private int computeCursorOffset(MouseEvent event) { - List<LineCell> visibleCells = ((StyledTextSkin) getControl().getSkin()).getCurrentVisibleCells(); - - LineCell lastCell = null; - - int result = getControl().getContent().getCharCount(); - - for (LineCell tmp : visibleCells) { - Bounds boundsInParent = tmp.getBoundsInParent(); - if (boundsInParent.getMinY() > event.getY()) { - if (lastCell == null) { - lastCell = tmp; - } - if (lastCell.getDomainElement() != null) { - StyledTextLayoutContainer n = (StyledTextLayoutContainer) lastCell.getGraphic(); - if (n.localToScene(n.getBoundsInLocal()).contains(event.getSceneX(), event.getSceneY())) { - int index = n.getCaretIndexAtPoint(n.sceneToLocal(event.getSceneX(), event.getSceneY())); - if (index >= 0) { - return n.getStartOffset() + index; - } - } - - final double minX = n.localToScene(n.getBoundsInLocal()).getMinX(); - final double mouseX = event.getSceneX(); - final boolean left = minX >= mouseX; - - result = lastCell.getDomainElement().getLineOffset() + (left ? 0 : lastCell.getDomainElement().getLineLength()); - } - break; - } - lastCell = tmp; - } - return result; } +// /** +// * computes the text offset under the mouse cursor. +// * @param event +// * @return the offset +// */ +// private int x_computeCursorOffset(MouseEvent event) { +// List<LineCell1> visibleCells = ((StyledTextSkin) getControl().getSkin()).getCurrentVisibleCells(); +// +// LineCell lastCell = null; +// +// int result = getControl().getContent().getCharCount(); +// +// for (LineCell tmp : visibleCells) { +// Bounds boundsInParent = tmp.getBoundsInParent(); +// if (boundsInParent.getMinY() > event.getY()) { +// if (lastCell == null) { +// lastCell = tmp; +// } +// +// if (lastCell.getDomainElement() != null) { +// StyledTextLayoutContainer n = (StyledTextLayoutContainer) lastCell.getGraphic(); +// if (n.localToScene(n.getBoundsInLocal()).contains(event.getSceneX(), event.getSceneY())) { +// int index = n.getCaretIndexAtPoint(n.sceneToLocal(event.getSceneX(), event.getSceneY())); +// if (index >= 0) { +// return n.getStartOffset() + index; +// } +// } +// +// final double minX = n.localToScene(n.getBoundsInLocal()).getMinX(); +// final double mouseX = event.getSceneX(); +// final boolean left = minX >= mouseX; +// +// result = lastCell.getDomainElement().getLineOffset() + (left ? 0 : lastCell.getDomainElement().getLineLength()); +// } +// break; +// } +// lastCell = tmp; +// } +// return result; +// } + diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/behavior/TextPositionSupport.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/behavior/TextPositionSupport.java index 1e3b0210e..952712e19 100644 --- a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/behavior/TextPositionSupport.java +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/behavior/TextPositionSupport.java @@ -11,19 +11,12 @@ *******************************************************************************/
package org.eclipse.fx.ui.controls.styledtext.behavior;
-import java.util.List;
-
import org.eclipse.fx.ui.controls.styledtext.StyledTextArea;
-import org.eclipse.fx.ui.controls.styledtext.StyledTextLayoutContainer;
-import org.eclipse.fx.ui.controls.styledtext.events.TextHoverEvent;
import org.eclipse.fx.ui.controls.styledtext.events.TextPositionEvent;
-import org.eclipse.fx.ui.controls.styledtext.skin.StyledTextSkin;
-import org.eclipse.fx.ui.controls.styledtext.skin.StyledTextSkin.LineCell;
import javafx.event.Event;
-import javafx.geometry.Bounds;
-import javafx.scene.control.Control;
import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Region;
/**
* manages text position support
@@ -33,9 +26,9 @@ import javafx.scene.input.MouseEvent; public class TextPositionSupport {
private StyledTextArea textArea;
- private Control control;
+ private Region control;
- public TextPositionSupport(Control control, StyledTextArea textArea) {
+ public TextPositionSupport(Region control, StyledTextArea textArea) {
this.control = control;
this.textArea = textArea;
}
@@ -49,7 +42,7 @@ public class TextPositionSupport { this.control.addEventFilter(MouseEvent.MOUSE_MOVED, this::onMouseEvent);
}
- public static TextPositionSupport install(Control control, StyledTextArea textArea) {
+ public static TextPositionSupport install(Region control, StyledTextArea textArea) {
TextPositionSupport support = new TextPositionSupport(control, textArea);
support.install();
return support;
@@ -65,39 +58,43 @@ public class TextPositionSupport { * @return the offset
*/
protected int computeCursorOffset(MouseEvent event) {
- List<LineCell> visibleCells = ((StyledTextSkin) this.textArea.getSkin()).getCurrentVisibleCells();
-
- LineCell lastCell = null;
-
- int result = this.textArea.getContent().getCharCount();
-
- for (LineCell tmp : visibleCells) {
- Bounds boundsInParent = tmp.getBoundsInParent();
- if (boundsInParent.getMinY() > event.getY()) {
- if (lastCell == null) {
- lastCell = tmp;
- }
-
- if (lastCell.getDomainElement() != null) {
- StyledTextLayoutContainer n = (StyledTextLayoutContainer) lastCell.getGraphic();
- if (n.localToScene(n.getBoundsInLocal()).contains(event.getSceneX(), event.getSceneY())) {
- int index = n.getCaretIndexAtPoint(n.sceneToLocal(event.getSceneX(), event.getSceneY()));
- if (index >= 0) {
- return n.getStartOffset() + index;
- }
- }
-
- final double minX = n.localToScene(n.getBoundsInLocal()).getMinX();
- final double mouseX = event.getSceneX();
- final boolean left = minX >= mouseX;
-
- result = lastCell.getDomainElement().getLineOffset() + (left ? 0 : lastCell.getDomainElement().getLineLength());
- }
- break;
- }
- lastCell = tmp;
- }
- return result;
+ return textArea.getOffsetAtPosition(event.getX(), event.getY());
+
+
+// List<LineCell> visibleCells = ((StyledTextSkin) this.textArea.getSkin()).getCurrentVisibleCells();
+//
+// LineCell lastCell = null;
+//
+// int result = this.textArea.getContent().getCharCount();
+//
+// for (LineCell tmp : visibleCells) {
+// Bounds boundsInParent = tmp.getBoundsInParent();
+// if (boundsInParent.getMinY() > event.getY()) {
+// if (lastCell == null) {
+// lastCell = tmp;
+// }
+//
+// if (lastCell.getDomainElement() != null) {
+// LineNode n = (LineNode) lastCell.getGraphic();
+//// StyledTextLayoutContainer n = (StyledTextLayoutContainer) lastCell.getGraphic();
+// if (n.localToScene(n.getBoundsInLocal()).contains(event.getSceneX(), event.getSceneY())) {
+// int index = n.getCaretIndexAtPoint(n.sceneToLocal(event.getSceneX(), event.getSceneY()));
+// if (index >= 0) {
+// return n.getStartOffset() + index;
+// }
+// }
+//
+// final double minX = n.localToScene(n.getBoundsInLocal()).getMinX();
+// final double mouseX = event.getSceneX();
+// final boolean left = minX >= mouseX;
+//
+// result = lastCell.getDomainElement().getLineOffset() + (left ? 0 : lastCell.getDomainElement().getLineLength());
+// }
+// break;
+// }
+// lastCell = tmp;
+// }
+// return result;
}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/ContentView.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/ContentView.java new file mode 100644 index 000000000..de0ede402 --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/ContentView.java @@ -0,0 +1,816 @@ +package org.eclipse.fx.ui.controls.styledtext.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.eclipse.fx.ui.controls.styledtext.StyledTextContent;
+import org.eclipse.fx.ui.controls.styledtext.StyledTextContent.TextChangeListener;
+import org.eclipse.fx.ui.controls.styledtext.TextChangedEvent;
+import org.eclipse.fx.ui.controls.styledtext.TextChangingEvent;
+import org.eclipse.fx.ui.controls.styledtext.TextSelection;
+import org.eclipse.fx.ui.controls.styledtext.model.TextAnnotationPresenter;
+
+import com.google.common.collect.ContiguousSet;
+import com.google.common.collect.DiscreteDomain;
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import com.google.common.collect.TreeRangeSet;
+
+import javafx.beans.Observable;
+import javafx.beans.binding.DoubleBinding;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ReadOnlyIntegerProperty;
+import javafx.beans.property.SetProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleSetProperty;
+import javafx.collections.FXCollections;
+import javafx.geometry.Point2D;
+import javafx.scene.control.ScrollBar;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.StackPane;
+
+public class ContentView extends Pane {
+
+ private static boolean debugOut = Boolean.getBoolean("styledtext.debugout");
+
+ private SetProperty<TextAnnotationPresenter> textAnnotationPresenter = new SimpleSetProperty<>(FXCollections.observableSet());
+ public SetProperty<TextAnnotationPresenter> textAnnotationPresenterProperty() {
+ return textAnnotationPresenter;
+ }
+
+ private class LineLayer extends VFlow<LineNode> {
+
+ public LineLayer(Supplier<LineNode> nodeFactory, BiConsumer<LineNode, Integer> nodePopulator) {
+ super(nodeFactory, nodePopulator);
+
+ setOnRelease(n->n.release());
+ setOnActivate((idx, n)->n.setIndex(idx));
+ }
+
+
+
+// protected void releaseNode(int lineIndex) {
+//
+// get(model.get(lineIndex)).ifPresent(n->{
+// n.setVisible(false);
+// n.release();
+// });
+// }
+
+// protected void releaseNode(StyledTextLine line) {
+// if (debugOut) System.err.println("RELEASE " + line);
+// release(line);
+//// get(line).ifPresent(n->{
+//// n.setVisible(false);
+//// n.release();
+//// });
+// }
+
+// private void updateNode(StyledTextLine line) {
+// if (debugOut) System.err.println("UPDATE " + line);
+// LineNode node = get(line);
+// node.update(line, textAnnotationPresenter);
+//// LineNode node = getCreate(m);
+//// node.setVisible(true);
+////// node.setModel(m);
+//// node.update(m, textAnnotationPresenter);
+// }
+
+// private void updateNode(int lineIndex, StyledTextLine m) {
+// LineNode node = getCreate(m);
+// node.setVisible(true);
+//// node.setModel(m);
+// node.update(m, textAnnotationPresenter);
+// }
+
+// @Override
+// public void requestLayout() {
+// super.requestLayout();
+// }
+
+// @Override
+// protected void layoutChildren() {
+// if (debugOut) System.err.println("layout LineLayer");
+// ContiguousSet.create(visibleLines.get(), DiscreteDomain.integers()).forEach(e -> {
+// if (!yOffsetData.containsKey(e)) {
+// if (debugOut) System.err.println("NO yOffset FOR " + e);
+// return;
+// }
+// double x = 0;
+// double y = yOffsetData.get(e);
+// double width = getWidth();
+// double height = getLineHeight();
+//
+// if (model.size() > e) {
+// StyledTextLine m = model.get(e);
+// LineNode lineNode = get(m);
+// lineNode.resizeRelocate(x, y, width, height);
+//
+// if (debugOut) System.err.println("layout " + e + ": y=" + y + " line= " + lineNode);
+// if (debugOut) System.err.println(" visible = " + lineNode.isVisible());
+// if (debugOut) System.err.println(" bounds = " + lineNode.getBoundsInParent());
+//// get(m).ifPresent(n->n.resizeRelocate(x, y, width, height));
+//
+// lineNode.layout();
+// }
+// });
+// }
+
+ @Override
+ protected void releaseNode(int lineIndex) {
+ super.releaseNode(lineIndex);
+
+ }
+
+ private Stream<LineNode> createVisibleLineNodesStream() {
+ ContiguousSet<Integer> visibleIndexes = ContiguousSet.create(visibleLines.get(), DiscreteDomain.integers());
+// return visibleIndexes.stream().filter(i->i<model.size()).map(idx->get(model.get(idx))).filter(n->n.isPresent()).map(n->n.get()).filter(n->n.isVisible());
+ return visibleIndexes.stream().filter(i->i<getNumberOfLines()).map(idx->getVisibleNode(idx)).filter(n->n.isPresent()).map(n->n.get());
+ }
+
+ private Optional<Integer> getLineIndex(javafx.geometry.Point2D point) {
+ final Optional<LineNode> hitLine = createVisibleLineNodesStream().filter(n->n.getBoundsInParent().contains(point)).findFirst();
+ final Optional<Integer> index = hitLine.map(n->{
+ int i = n.getCaretIndexAtPoint(new javafx.geometry.Point2D(point.getX(), n.getHeight()/2));
+ if (i >= 0 ) {
+ return n.getStartOffset() + i;
+ }
+ else {
+ return n.getEndOffset();
+ }
+ });
+ return index;
+ }
+
+
+// protected void permutate(int a, int b) {
+// LineNode nodeA = existingNodes.get(a);
+// LineNode nodeB = existingNodes.get(b);
+//
+// existingNodes.put(a, nodeB);
+// existingNodes.put(b, nodeA);
+//
+// System.err.println(" . permutate " + a + " -> " + b);
+// }
+ }
+
+ private StackPane contentBody = new StackPane();
+
+ private LineLayer lineLayer = new LineLayer(()->new LineNode(), (n, m)->{
+ n.setLineHelper(getLineHelper());
+ n.update(textAnnotationPresenter.get());
+ });
+
+ private Predicate<Set<LineNode>> needsPresentation;
+
+ private Map<Integer, Double> yOffsetData = new HashMap<>();
+
+ private boolean forceLayout = true;
+
+ private DoubleProperty lineHeigth = new SimpleDoubleProperty(this, "lineHeight", 16.0);
+ public DoubleProperty lineHeightProperty() {
+ return this.lineHeigth;
+ }
+ public double getLineHeight() {
+ return this.lineHeigth.get();
+ }
+ public void setLineHeight(double lineHeight) {
+ this.lineHeigth.set(lineHeight);
+ }
+
+ private IntegerProperty numberOfLines = new SimpleIntegerProperty(this, "numberOfLines", 0);
+ public ReadOnlyIntegerProperty numberOfLinesProperty() {
+ return this.numberOfLines;
+ }
+ public int getNumberOfLines() {
+ return this.numberOfLines.get();
+ }
+
+ private IntegerProperty caretOffset = new SimpleIntegerProperty(this, "caretOffset", 0);
+ public IntegerProperty caretOffsetProperty() {
+ return this.caretOffset;
+ }
+
+ private ObjectProperty<TextSelection> textSelection = new SimpleObjectProperty<>(this, "textSelection", new TextSelection(0, 0));
+ public ObjectProperty<TextSelection> textSelectionProperty() {
+ return this.textSelection;
+ }
+
+ private ObjectProperty<StyledTextContent> content = new SimpleObjectProperty<>(this, "content");
+ public ObjectProperty<StyledTextContent> contentProperty() {
+ return this.content;
+ }
+ public StyledTextContent getContent() {
+ return this.content.get();
+ }
+
+ private LineHelper lineHelper;
+
+ protected LineLayer getLineLayer() {
+ return this.lineLayer;
+ }
+
+ protected LineHelper getLineHelper() {
+ return this.lineHelper;
+ }
+
+// private ObservableList<StyledTextLine> model;
+// private IntegerBinding modelSize;
+
+// public void setModel(ObservableList<StyledTextLine> model) {
+// this.model = model;
+//
+//
+//// model.addListener((InvalidationListener)(x)->prepareNodes(getVisibleLines()));
+//
+//// model.addListener((ListChangeListener<StyledTextLine>)(c)->{
+//// RangeSet<Integer> updateNodes = TreeRangeSet.create();
+////
+//// while (c.next()) {
+//// if (debugOut) System.err.println("CHANGE [");
+//// if (c.wasPermutated()) {
+//// if (debugOut) System.err.println("-> permutation " + c);
+////// for (int i = c.getFrom(); i < c.getTo(); i++) {
+////// lineLayer.permutate(i, c.getPermutation(i));
+////// }
+////// lineLayer.requestLayout();
+//// }
+//// if (c.wasUpdated() || c.wasReplaced()) {
+//// if (debugOut) System.err.println("-> updated or replaced: " + c.getFrom() + " - " + c.getTo());
+//// updateNodes.add(Range.closedOpen(c.getFrom(), c.getTo()));
+////
+////
+//// }
+//// if (c.wasAdded()) {
+//// if (debugOut) System.err.println("-> added: " + c.getFrom() + " - " + c.getTo());
+//// updateNodes.add(Range.closedOpen(c.getFrom(), model.size()));
+//// }
+//// if (c.wasRemoved()) {
+//// if (debugOut) System.err.println("-> removed: " + c.getFrom() + " - " + c.getTo());
+////
+//// c.getRemoved().forEach(line->lineLayer.releaseNode(line));
+////
+//// updateNodes.add(Range.closedOpen(c.getFrom(), model.size()));
+//// }
+//// if (debugOut) System.err.println("]");
+//// }
+////
+//// updateNodesNow(updateNodes);
+//// });
+////
+//// modelSize = Bindings.size(model);
+////
+//// modelSize.addListener((x, o, n)-> {
+//// int newSize = n.intValue();
+//// int oldSize = o.intValue();
+//// if (newSize < oldSize) {
+////// for (int lineIdx = newSize; lineIdx < oldSize; lineIdx++) {
+////// lineLayer.releaseNode(lineIdx);
+////// }
+//// }
+//// });
+// }
+
+// private ListProperty<StyledTextLine> model = new SimpleListProperty<>(this, "model", FXCollections.observableArrayList());
+// public ListProperty<StyledTextLine> getModel() {
+// return this.model;
+// }
+
+ private ObjectProperty<Range<Integer>> visibleLines = new SimpleObjectProperty<>(this, "visibleLines", Range.closed(0, 0));
+ public ObjectProperty<Range<Integer>> visibleLinesProperty() {
+ return this.visibleLines;
+ }
+ public com.google.common.collect.Range<Integer> getVisibleLines() {
+ return this.visibleLines.get();
+ }
+ public void setVisibleLines(Range<Integer> visibleLines) {
+ this.visibleLines.set(visibleLines);
+ }
+
+ private Range<Integer> curVisibleLines;
+
+ public ContentView(LineHelper lineHelper) {
+ this.lineHelper = lineHelper;
+// setStyle("-fx-border-color: green; -fx-border-width:2px; -fx-border-style: dashed;");
+
+ this.contentBody.getChildren().setAll(this.lineLayer);
+
+// this.lineLayer.setStyle("-fx-border-color: orange; -fx-border-width:2px; -fx-border-style: solid;");
+
+
+// this.contentBody.setStyle("-fx-border-color: blue; -fx-border-width:2px; -fx-border-style: dotted;");
+
+
+ this.getChildren().setAll(this.contentBody);
+
+
+
+// AnimationTimer t = new AnimationTimer() {
+// @Override
+// public void handle(long now) {
+// updatePulse(now);
+// }
+// };
+//
+// visibleProperty().addListener((x, o, n)->{
+// if (n) {
+// t.start();
+// }
+// else {
+// t.stop();
+// }
+// });
+//
+// t.start();
+// sceneProperty().addListener((x, o, n) -> {
+// if (n == null) {
+// t.stop();
+// }
+// else {
+// t.start();
+// }
+// });
+
+
+ setMinWidth(200);
+ setMinHeight(200);
+
+ visibleLines.addListener(this::onLineChange);
+ offsetX.addListener(this::onLineChange);
+
+
+
+ this.lineLayer.lineHeightProperty().bind(this.lineHeigth);
+ this.lineLayer.yOffsetProperty().bind(this.offsetY);
+ this.lineLayer.visibleLinesProperty().bind(this.visibleLines);
+ this.lineLayer.numberOfLinesProperty().bind(this.numberOfLines);
+
+ bindContentListener();
+ bindCaretListener();
+ bindSelectionListener();
+ }
+
+ private void bindCaretListener() {
+ caretOffset.addListener((x, o, n)-> {
+ int oldCaretLine = getContent().getLineAtOffset(o.intValue());
+ int newCaretLine = getContent().getLineAtOffset(n.intValue());
+
+ RangeSet<Integer> toUpdate = TreeRangeSet.create();
+ toUpdate.add(Range.closed(oldCaretLine, oldCaretLine));
+ toUpdate.add(Range.closed(newCaretLine, newCaretLine));
+
+ updateNodesNow(toUpdate);
+ });
+ }
+
+ private Range<Integer> getAffectedLines(TextSelection selection) {
+ int firstLine = getContent().getLineAtOffset(selection.offset);
+ int lastLine = getContent().getLineAtOffset(selection.offset + selection.length);
+ System.err.println("getAffectedLines " + selection + " -> " + firstLine + " - " + lastLine);
+ if (lastLine == -1) {
+ lastLine = numberOfLines.get();
+ }
+ return Range.closed(firstLine, Math.min(lastLine, numberOfLines.get()));
+ }
+
+ private Range<Integer> toRange(TextSelection s) {
+ return Range.closedOpen(s.offset, s.offset + s.length);
+ }
+
+ private Range<Integer> toLineRange(Range<Integer> globalOffsetRange) {
+ int lower = getContent().getLineAtOffset(globalOffsetRange.lowerEndpoint());
+ int upper = getContent().getLineAtOffset(globalOffsetRange.upperEndpoint());
+ return Range.closed(lower, upper);
+ }
+
+ private void bindSelectionListener() {
+ textSelection.addListener((x, o, n) -> {
+ RangeSet<Integer> toUpdate = TreeRangeSet.create();
+ if (o != null) toUpdate.add(getAffectedLines(o));
+ if (n != null) toUpdate.add(getAffectedLines(n));
+ System.err.println("ContentView: onSelectionChange " + o + " -> " + n + " triggered " + toUpdate);
+ updateNodesNow(toUpdate);
+ });
+ }
+
+ private void bindContentListener() {
+ this.content.addListener((x, o, n)->{
+ if (o != null) {
+ o.removeTextChangeListener(this.textChangeListener);
+ }
+ if (n != null) {
+ n.addTextChangeListener(this.textChangeListener);
+ System.err.println("init #ofLines with " + n.getLineCount());
+ this.numberOfLines.set(n.getLineCount());
+ }
+ });
+ StyledTextContent current = this.content.get();
+ if (current != null) {
+ current.addTextChangeListener(this.textChangeListener);
+
+ // set inital values
+ System.err.println("init #ofLines with " + current.getLineCount());
+ numberOfLines.set(current.getLineCount());
+
+ }
+ }
+
+ private TextChangeListener textChangeListener = new TextChangeListener() {
+
+ private Function<Integer, Integer> mapping;
+ private RangeSet<Integer> toUpdate = TreeRangeSet.create();
+ private RangeSet<Integer> toRelease = TreeRangeSet.create();
+
+ @Override
+ public void textSet(TextChangedEvent event) {
+ System.err.println("ContentView textSet");
+
+ // update number of lines
+ ContentView.this.numberOfLines.set(getContent().getLineCount());
+
+ getLineLayer().requestLayout();
+ }
+
+ private int computeFirstUnchangedLine(TextChangingEvent event) {
+
+ int endOffset = event.offset + event.replaceCharCount;
+ int endLineIndex = getContent().getLineAtOffset(endOffset);
+ int endLineBegin = getContent().getOffsetAtLine(endLineIndex);
+ int endLineLength = lineHelper.getLength(endLineIndex);
+
+ int firstSafeLine;
+
+ if (endLineBegin == event.offset) {
+ // offset at beginning of line
+ firstSafeLine = endLineIndex;
+ }
+ else {
+ // offset in middle or at end of line
+ firstSafeLine = endLineIndex + 1;
+ }
+
+ return firstSafeLine;
+ }
+
+ @Override
+ public void textChanging(TextChangingEvent event) {
+ System.err.println("ContentView textChanging");
+ System.err.println(" current doc len = " + getContent().getLineCount());
+ System.err.println("Event: "+ event);
+
+ final int changeBeginLine = getContent().getLineAtOffset(event.offset);
+
+ // determine first unchanged line
+ int firstUnchangedLine = computeFirstUnchangedLine(event);
+
+ System.err.println("FIRST UNCHANGEDLINE: " + firstUnchangedLine);
+
+ int deltaLines = event.newLineCount - event.replaceLineCount;
+
+ if (deltaLines < 0) {
+ this.toRelease.add(Range.closedOpen(firstUnchangedLine + deltaLines, firstUnchangedLine));
+ }
+
+ // prepare permutation
+ this.mapping = (idx) -> {
+ if (idx >= firstUnchangedLine) {
+ return idx + deltaLines;
+ }
+ return idx;
+ };
+
+ this.toUpdate.add(Range.closedOpen(changeBeginLine, firstUnchangedLine + deltaLines));
+
+
+//
+// // simple insert
+// if (event.replaceCharCount == 0) {
+// System.err.println("# Simple Insert");
+// if (event.newLineCount > 0) {
+// System.err.println("# We have new lines");
+//
+// int lineIndex = getContent().getLineAtOffset(event.offset);
+// int lineBegin = getContent().getOffsetAtLine(lineIndex);
+// int lineLength = lineHelper.getLength(lineIndex);
+//
+// int firstSafeLine;
+// Range<Integer> updateRange;
+//
+// if (lineBegin == event.offset) {
+// // at beginning of line
+// firstSafeLine = lineIndex;
+// updateRange = Range.closedOpen(lineIndex, lineIndex + event.newLineCount);
+// }
+// else if (lineBegin == event.offset + lineLength) {
+// // insert was at end of line
+// firstSafeLine= lineIndex + 1;
+// updateRange = Range.closedOpen(lineIndex, lineIndex + event.newLineCount);
+// }
+// else {
+// // insert was in middle of line
+// firstSafeLine = lineIndex + 2;
+// updateRange = Range.closedOpen(lineIndex, lineIndex + 1 + event.newLineCount);
+// }
+// System.err.println("# firstSafeLine = " + firstSafeLine + " / updateRange " + updateRange);
+//
+// // prepare update
+// toUpdate.add(updateRange);
+//
+// // prepare permutation
+// this.mapping = (idx) -> {
+// if (idx >= firstSafeLine) {
+// return idx + event.newLineCount;
+// }
+// return idx;
+// };
+//
+// }
+// }
+
+
+
+// int firstUnchangedLine = changeBeginLine + replaceLines;
+//
+// int newFirstUnchnagedLine = changeBeginLine + newLines;
+//
+// System.err.println(" changeBeginLine = " + changeBeginLine);
+// System.err.println(" firstUnchangedLine = " + firstUnchangedLine);
+// System.err.println(" newFirstUnchangedLine = " + newFirstUnchnagedLine);
+//
+// // prepare updates
+// toUpdate.add(Range.closedOpen(changeBeginLine, changeBeginLine + newLines));
+// // prepare permutation
+// this.mapping = (idx) -> {
+// if (idx >= firstUnchangedLine) {
+// return idx + firstUnchangedLine - newFirstUnchnagedLine;
+// }
+// return idx;
+// };
+
+ }
+
+ @Override
+ public void textChanged(TextChangedEvent event) {
+ System.err.println("ContentView textChanged");
+
+
+ if (!toRelease.isEmpty()) {
+ releaseNodesNow(toRelease);
+ toRelease.clear();
+ }
+
+ // execute permutation
+ if (this.mapping != null) {
+ getLineLayer().permutateNodes(mapping);
+ this.mapping = null;
+ }
+
+
+ // execute updates
+ if (!toUpdate.isEmpty()) {
+
+ updateNodesNow(toUpdate);
+ toUpdate.clear();
+ }
+
+ // update number of lines
+ ContentView.this.numberOfLines.set(getContent().getLineCount());
+
+
+ getLineLayer().requestLayout();
+ }
+ };
+
+// Timer t = new Timer();
+// volatile boolean scheduled = false;
+// private void scheduleUpdate() {
+// if (true) return;
+//
+// try {
+// if (!scheduled) {
+// scheduled = true;
+// t.schedule(new TimerTask() {
+// @Override
+// public void run() {
+// Platform.runLater(()->doUpdate());
+// scheduled = false;
+// }
+// }, 16);
+// }
+// }
+// catch (Exception e) {
+// e.printStackTrace();
+// }
+//
+// }
+
+ private void onLineChange(Observable o) {
+ RangeSet<Integer> toUpdate = TreeRangeSet.create();
+ RangeSet<Integer> toRelease = TreeRangeSet.create();
+
+ double offsetY = offsetYProperty().get();
+ com.google.common.collect.Range<Integer> visibleLines = visibleLinesProperty().get();
+ ContiguousSet<Integer> set = ContiguousSet.create(visibleLines, DiscreteDomain.integers());
+ double lineHeight = lineHeightProperty().get();
+
+// System.err.println("onLineChange " + offsetY + " " + visibleLines);
+
+
+ // schedule visible line updates
+ if (curVisibleLines == null) {
+ toUpdate.add(visibleLines);
+ }
+ else {
+ RangeSet<Integer> hiddenLines = TreeRangeSet.create();
+ hiddenLines.add(curVisibleLines);
+ hiddenLines.remove(visibleLines);
+
+ RangeSet<Integer> shownLines = TreeRangeSet.create();
+ shownLines.add(visibleLines);
+ shownLines.remove(curVisibleLines);
+
+ toUpdate.addAll(shownLines);
+ toRelease.addAll(hiddenLines);
+ }
+ this.curVisibleLines = visibleLines;
+
+ // store precomputed y data
+ for (int index : set) {
+ double y = index * lineHeight - offsetY;
+ yOffsetData.put(index, y);
+ forceLayout = true;
+ }
+
+ releaseNodesNow(toRelease);
+ updateNodesNow(toUpdate);
+
+ lineLayer.requestLayout();
+// scheduleUpdate();
+ }
+
+ private double computeLongestLine() {
+ System.err.println("compute longest line til " + getNumberOfLines());
+ System.err.println(" while content has " + content.get().getLineCount());
+ Optional<Integer> longestLine = IntStream.range(0, getNumberOfLines()).mapToObj(index->lineHelper.getLengthCountTabsAsChars(index)).collect(Collectors.maxBy(Integer::compare));
+// Optional<Integer> longestLine = model.stream().map(m->m.getLineLength() + countTabs(m.getText()) * 3).collect(Collectors.maxBy(Integer::compare));
+ longestLine = longestLine.map(s->s+2); // extra space
+ Optional<Double> longestLineWidth = longestLine.map(i->i*8d);
+ longestLineWidth = longestLineWidth.map(l->Math.max(l, getWidth()));
+ return longestLineWidth.orElse(getWidth());
+ }
+
+ @Override
+ protected void layoutChildren() {
+ double scrollX = -offsetX.get();
+ this.contentBody.resizeRelocate(scrollX, 0, computeLongestLine(), getHeight());
+ }
+
+
+ private DoubleProperty offsetY = new SimpleDoubleProperty();
+ private DoubleProperty offsetX = new SimpleDoubleProperty();
+
+ public void bindHorizontalScrollbar(ScrollBar bar) {
+ bar.setMin(0);
+ DoubleBinding max = this.contentBody.widthProperty().subtract(widthProperty());
+ DoubleBinding factor = this.contentBody.widthProperty().divide(max);
+ bar.maxProperty().bind(contentBody.widthProperty().divide(factor));
+ bar.visibleAmountProperty().bind(widthProperty().divide(factor));
+ this.offsetX.bind(bar.valueProperty());
+ }
+
+ public DoubleProperty offsetYProperty() {
+ return this.offsetY;
+ }
+
+
+// private RangeSet<Integer> toRelease = TreeRangeSet.create();
+// private RangeSet<Integer> toUpdate = TreeRangeSet.create();
+
+
+ private void updateNodesNow(com.google.common.collect.RangeSet<Integer> rs) {
+ System.err.println("updateNodesNow nr of lines = " + getNumberOfLines());
+ RangeSet<Integer> subRangeSet = rs.subRangeSet(getVisibleLines()).subRangeSet(Range.closedOpen(0, getNumberOfLines()));
+ System.err.println("updateNodesNow: " + subRangeSet);
+ subRangeSet.asRanges().forEach(r-> {
+ ContiguousSet.create(r, DiscreteDomain.integers()).forEach(index-> {
+ getLineLayer().updateNode(index);
+// StyledTextLine m = this.model.get(index);
+// lineLayer.updateNode(m);
+ });
+ });
+ }
+
+ private void releaseNodesNow(com.google.common.collect.RangeSet<Integer> rs) {
+ RangeSet<Integer> subRangeSet = rs.subRangeSet(Range.closedOpen(0, getNumberOfLines()));
+// System.err.println("releaseNodesNow " + subRangeSet);
+ if (debugOut) System.err.println("releasing " + rs);
+ subRangeSet.asRanges().forEach(r-> {
+ ContiguousSet.create(r, DiscreteDomain.integers()).forEach(index-> {
+ getLineLayer().releaseNode(index);
+// StyledTextLine m = this.model.get(index);
+// System.err.println("RELEASE " + m);
+// lineLayer.releaseNode(m);
+ });
+ });
+ }
+
+// private void updateNodes(com.google.common.collect.Range<Integer> range) {
+// if (debugOut) System.err.println("updateNodes(" + range + ")");
+// toUpdate.add(range);
+// scheduleUpdate();
+////
+//// if (range.intersects(getVisibleLines())) {
+//// Range intersection = range.intersect(getVisibleLines());
+//// for (int index = intersection.getOffset(); index <intersection.getEndOffset(); index++) {
+//// toUpdate.add(index);
+////// StyledTextLine m = this.model.get(index);
+////// prepareNode(index, m);
+//// }
+//// }
+// }
+
+// private boolean doUpdate() {
+// try {
+// long now = -System.nanoTime();
+// if (!toRelease.isEmpty()) {
+// toRelease.asRanges().forEach(r-> {
+// ContiguousSet.create(r, DiscreteDomain.integers()).forEach(this.lineLayer::releaseNode);
+// });
+// toRelease.clear();
+// }
+//
+// if (!toUpdate.isEmpty()) {
+// toUpdate.subRangeSet(getVisibleLines()).subRangeSet(Range.closedOpen(0, this.model.size())).asRanges().forEach(r-> {
+// ContiguousSet.create(r, DiscreteDomain.integers()).forEach(index-> {
+// StyledTextLine m = this.model.get(index);
+// lineLayer.updateNode(index, m);
+// });
+// });
+// toUpdate.clear();
+// }
+//
+//
+// now += System.nanoTime();
+//
+// if (now > 1000_000 * 5) {
+// System.err.println("update needed " + (now/1000000) + "ms");
+// }
+//
+// if (!toRelease.isEmpty() || !toUpdate.isEmpty() || forceLayout) {
+//// System.err.println("releasing " + toRelease + " and updating " + toUpdate + " lines");
+//
+// lineLayer.requestLayout();
+//
+// forceLayout = false;
+//
+// return true;
+// }
+// }
+// catch (Exception e) {
+// e.printStackTrace();
+// }
+// return false;
+//
+// }
+//
+// private void updatePulse(long now) {
+// doUpdate();
+// }
+
+
+ public Optional<Point2D> getLocationInScene(int globalOffset) {
+ int lineIndex = getContent().getLineAtOffset(globalOffset);
+ Optional<LineNode> node = lineLayer.getVisibleNode(lineIndex);
+
+ return node.map(n->{
+ double x = n.getCharLocation(globalOffset - n.getStartOffset());
+ Point2D p = new Point2D(x, 0);
+ return n.localToScene(p);
+ });
+ }
+
+ public Optional<Integer> getLineIndex(Point2D point) {
+ // transform point to respect horizontal scrolling
+ point = this.lineLayer.sceneToLocal(this.localToScene(point));
+ Optional<Integer> result = this.lineLayer.getLineIndex(point);
+ return result;
+
+ }
+ public void updateAnnotations(RangeSet<Integer> r) {
+ updateNodesNow(r);
+ }
+
+}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/DebugMarker.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/DebugMarker.java new file mode 100644 index 000000000..a75134620 --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/DebugMarker.java @@ -0,0 +1,40 @@ +package org.eclipse.fx.ui.controls.styledtext.internal;
+
+import javafx.animation.FillTransition;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+import javafx.util.Duration;
+
+public class DebugMarker extends Rectangle {
+
+ private FillTransition transition;
+
+
+ public DebugMarker(Color color, long millis) {
+ this.setFill(Color.TRANSPARENT);
+
+ this.setMouseTransparent(true);
+
+ this.transition = new FillTransition();
+ this.transition.setFromValue(color);
+ this.transition.setToValue(Color.TRANSPARENT);
+ this.transition.setShape(this);
+ this.transition.setDuration(Duration.millis(millis));
+
+ this.setOpacity(0.5);
+
+
+ }
+
+
+ public void play() {
+ this.transition.playFromStart();
+ }
+
+ @Override
+ public void resize(double width, double height) {
+ this.setWidth(width);
+ this.setHeight(height);
+ }
+
+}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/DynCachePane.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/DynCachePane.java new file mode 100644 index 000000000..51560e250 --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/DynCachePane.java @@ -0,0 +1,102 @@ +package org.eclipse.fx.ui.controls.styledtext.internal;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Queue;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import javafx.scene.Node;
+import javafx.scene.layout.Pane;
+
+public class DynCachePane<K, N extends Node> extends Pane {
+
+ int maxCache = 100;
+
+ protected Map<K, N> existingNodes = new HashMap<>();
+
+ protected Map<K, N> usedNodes = new HashMap<>();
+
+ protected Queue<N> cachedNodes = new LinkedList<>();
+
+ private Supplier<N> nodeFactory;
+
+ long count = 0;
+
+ public DynCachePane(Supplier<N> nodeFactory) {
+ this.nodeFactory = nodeFactory;
+ }
+
+
+ protected N get(K key) {
+ N node = usedNodes.get(key);
+ if (node == null) {
+ node = existingNodes.get(key);
+ }
+ if (node == null) {
+ if (!cachedNodes.isEmpty()) {
+ node = cachedNodes.poll();
+ }
+ }
+ if (node == null) {
+
+ node = this.nodeFactory.get();
+ existingNodes.put(key, node);
+ }
+ usedNodes.put(key, node);
+ node.setVisible(true);
+ return node;
+ }
+
+ protected void release(K key) {
+ N node = existingNodes.get(key);
+ if (node != null) {
+ node.setVisible(false);
+ usedNodes.remove(node);
+ cachedNodes.add(node);
+ }
+ }
+
+
+// protected N create(K key) {
+// N node = this.nodeFactory.get();
+// this.existingNodes.put(key, node);
+// this.getChildren().add(node);
+// count++;
+// if (count > 1000 ) {
+// System.err.println("ALLOCATED "+ count + " nodes!!!");
+// }
+// return node;
+// }
+//
+// protected void destroy(K key) {
+// N removed = this.existingNodes.remove(key);
+// getChildren().remove(removed);
+// }
+//
+// protected void release(K key) {
+// N removed = this.existingNodes.remove(key);
+// removed.setVisible(false);
+// cachedNodes.add(removed);
+// }
+//
+// protected Optional<N> get(K key) {
+// return Optional.ofNullable(this.existingNodes.get(key));
+// }
+//
+// protected N getCreate(K key) {
+// N n = this.existingNodes.get(key);
+// if (n == null) {
+// return create(key);
+// }
+// return n;
+// }
+
+
+
+
+
+}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/LineHelper.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/LineHelper.java new file mode 100644 index 000000000..585b78d5e --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/LineHelper.java @@ -0,0 +1,242 @@ +package org.eclipse.fx.ui.controls.styledtext.internal;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.eclipse.fx.ui.controls.styledtext.StyleRange;
+import org.eclipse.fx.ui.controls.styledtext.StyledTextArea;
+import org.eclipse.fx.ui.controls.styledtext.StyledTextContent;
+import org.eclipse.fx.ui.controls.styledtext.TextSelection;
+import org.eclipse.fx.ui.controls.styledtext.model.Annotation;
+import org.eclipse.fx.ui.controls.styledtext.model.AnnotationProvider;
+import org.eclipse.fx.ui.controls.styledtext.model.TextAnnotation;
+import org.eclipse.jdt.annotation.NonNull;
+
+import com.google.common.collect.Range;
+
+public class LineHelper {
+
+ private StyledTextArea control;
+
+ public LineHelper(StyledTextArea control) {
+ this.control = control;
+ }
+
+ private StyledTextContent getContent() {
+ return this.control.getContent();
+ }
+
+ private Range<Integer> getSelection() {
+ @NonNull
+ TextSelection selection = this.control.getSelection();
+ return Range.closedOpen(selection.offset, selection.offset + selection.length);
+ }
+
+ private int getCaretOffset() {
+ return this.control.getCaretOffset();
+ }
+
+ private StyleRange[] getStyleRanges(int start, int length, boolean includeRanges) {
+ return this.control.getStyleRanges(start, length, includeRanges);
+ }
+
+ private Set<AnnotationProvider> getAnnotationProvider() {
+ return this.control.getAnnotationProvider();
+ }
+
+
+
+ public int getOffset(int index) {
+ return getContent().getOffsetAtLine(index);
+ }
+
+ public int getLength(int index) {
+ return getText(index).length();
+ }
+
+ public int getLengthCountTabsAsChars(int index) {
+ String t = getText(index);
+ return t.length() + countTabs(t) * 3;
+ }
+
+ public String getText(int index) {
+ return getContent().getLine(index);
+ }
+
+ public Range<Integer> getRange(int index) {
+ int lower = getOffset(index);
+ int upper = lower + getLength(index);
+ return Range.closed(lower, upper);
+ }
+
+ private int mapToLocal(int index, int globalOffset) {
+ return globalOffset - getOffset(index);
+ }
+
+ private com.google.common.collect.Range<Integer> mapToLocal(int index, com.google.common.collect.Range<Integer> global) {
+ return com.google.common.collect.Range.range(global.lowerEndpoint() - getOffset(index), global.lowerBoundType(), global.upperEndpoint() - getOffset(index), global.upperBoundType());
+ }
+
+ public Range<Integer> getSelection(int index) {
+ Range<Integer> selection = getSelection();
+ if (selection.isEmpty()) {
+ return null;
+ }
+
+ Range<Integer> range = getRange(index);
+
+ Range<Integer> localSelection = null;
+
+ if (range.isConnected(selection)) {
+ Range<Integer> intersection = selection.intersection(range);
+ localSelection = mapToLocal(index, intersection);
+ }
+
+ if (localSelection != null) {
+ System.err.println(index + " linerange - " + range);
+ System.err.println(index + " globalSelection - " + selection);
+ System.err.println(index + " localSelection - " + localSelection);
+ }
+
+ return localSelection;
+ }
+
+ public int getCaret(int index) {
+ int globalCaret = getCaretOffset();
+ Range<Integer> range = getRange(index);
+ if (range.contains(globalCaret)) {
+ return mapToLocal(index, globalCaret);
+ }
+ else {
+ return -1;
+ }
+ }
+
+ private Segment createSegement(String text, StyleRange style) {
+ List<String> styleClasses = new ArrayList<>();
+ if (style.stylename != null) {
+ if (style.stylename.contains(".")) { //$NON-NLS-1$
+ List<String> styles = new ArrayList<String>(Arrays.asList(style.stylename.split("\\."))); //$NON-NLS-1$
+ styles.add(0, "source-segment"); //$NON-NLS-1$
+ styleClasses.addAll(styles);
+ } else {
+ styleClasses.add("source-segment"); //$NON-NLS-1$
+ styleClasses.add(style.stylename);
+ }
+
+ } else {
+ if (style.foreground != null) {
+ styleClasses.add("plain-source-segment"); //$NON-NLS-1$
+ } else {
+ styleClasses.add("source-segment"); //$NON-NLS-1$
+ }
+ }
+ return new Segment(text, styleClasses);
+ }
+
+ static String removeLineending(String s) {
+ return s.replace("\n", "").replace("\r", ""); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
+ }
+
+ public List<Segment> getSegments(int index) {
+ List<Segment> segments = new ArrayList<>();
+
+ String line = getContent().getLine(index);
+ if (line != null) {
+ int start = getContent().getOffsetAtLine(index);
+ int length = line.length();
+
+ StyleRange[] ranges = getStyleRanges(start, length, true);
+ if (ranges == null) {
+ return Collections.emptyList();
+ }
+
+ if (ranges.length == 0 && line.length() > 0) {
+ StyleRange styleRange = new StyleRange((String) null);
+ styleRange.start = start;
+ styleRange.length = line.length();
+
+ String text = removeLineending(line.substring(0, line.length()));
+
+ segments.add(createSegement(text, styleRange));
+ } else {
+ int lastIndex = -1;
+
+ if (ranges.length > 0) {
+ if (ranges[0].start - start > 0) {
+ StyleRange styleRange = new StyleRange((String) null);
+ styleRange.start = start;
+ styleRange.length = ranges[0].start - start;
+
+ String text = removeLineending(line.substring(0, ranges[0].start - start));
+
+ segments.add(createSegement(text, styleRange));
+ }
+ }
+
+ for (StyleRange r : ranges) {
+ int begin = r.start - start;
+ int end = r.start - start + r.length;
+
+ if (lastIndex != -1 && lastIndex != begin) {
+ StyleRange styleRange = new StyleRange((String) null);
+ styleRange.start = start + lastIndex;
+ styleRange.length = begin - lastIndex;
+
+ String text = removeLineending(line.substring(lastIndex, begin));
+
+ segments.add(createSegement(text, styleRange));
+ }
+
+ String text = removeLineending(line.substring(begin, end));
+
+ segments.add(createSegement(text, r));
+ lastIndex = end;
+ }
+
+ if (lastIndex > 0 && lastIndex < line.length()) {
+ StyleRange styleRange = new StyleRange((String) null);
+ styleRange.start = start + lastIndex;
+ styleRange.length = line.length() - lastIndex;
+
+ String text = removeLineending(line.substring(lastIndex, line.length()));
+
+ segments.add(createSegement(text, styleRange));
+ }
+ }
+ }
+
+ return segments;
+ }
+
+ public Set<TextAnnotation> getTextAnnotations(int index) {
+ return getAnnotations(index).stream().filter(m->m instanceof TextAnnotation).map(m->(TextAnnotation)m).collect(Collectors.toSet());
+ }
+
+ public Set<Annotation> getAnnotations(int index) {
+ // collect all the annotations for this line
+ Set<Annotation> result = getAnnotationProvider()
+ .stream().map(p->p.computeAnnotations(index))
+ .flatMap(Collection::stream)
+ .collect(Collectors.toSet());
+ return result;
+ }
+
+ private int countTabs(String s) {
+ Matcher matcher = Pattern.compile("\t").matcher(s);
+ int count = 0;
+ while (matcher.find()) {
+ count++;
+ }
+ return count;
+ }
+
+
+}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/LineNode.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/LineNode.java new file mode 100644 index 000000000..9d4032051 --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/LineNode.java @@ -0,0 +1,718 @@ +package org.eclipse.fx.ui.controls.styledtext.internal;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.eclipse.fx.ui.controls.styledtext.NodeCachePane;
+import org.eclipse.fx.ui.controls.styledtext.ReuseCache;
+import org.eclipse.fx.ui.controls.styledtext.model.TextAnnotation;
+import org.eclipse.fx.ui.controls.styledtext.model.TextAnnotationPresenter;
+
+import javafx.animation.Animation;
+import javafx.animation.Animation.Status;
+import javafx.animation.FadeTransition;
+import javafx.animation.Interpolator;
+import javafx.geometry.Insets;
+import javafx.geometry.Point2D;
+import javafx.scene.Node;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Line;
+import javafx.util.Duration;
+
+public class LineNode extends StackPane {
+
+ int leftPadding = 0;
+
+ private static boolean debugAnimation = Boolean.getBoolean("styledtext.debuganimation");
+ private static boolean debugOut = Boolean.getBoolean("styledtext.debugout");
+
+ private DebugMarker debugUpdateText;
+ private DebugMarker debugUpdateAnnotations;
+ private DebugMarker debugUpdateSelection;
+ private DebugMarker debugUpdateCaret;
+ private HBox debugBox;
+
+ private int index;
+ private LineHelper lineHelper;
+
+
+ private int getLineIndex() {
+ return index;
+ }
+
+ private static TextNode createNode() {
+ TextNode textNode = new TextNode("");
+ return textNode;
+ }
+
+ public class TextLayer extends HBox {
+
+ protected final ReuseCache<TextNode> cache;
+
+ public TextLayer() {
+ getStyleClass().add("source-segment-container");
+// setCache(true);
+// setCacheHint(CacheHint.QUALITY);
+ setMinWidth(HBox.USE_COMPUTED_SIZE);
+
+ cache = new ReuseCache<>(LineNode::createNode);
+ cache.addOnActivate(node->{
+ getChildren().add(node);
+// if (!getChildren().contains(node)) {
+// getChildren().add(node);
+// }
+// node.setVisible(true);
+// node.setManaged(true);
+ });
+ cache.addOnRelease(node->{
+ getChildren().remove(node);
+// node.setVisible(false);
+// node.setManaged(false);
+ });
+// cache.addOnClear(node->getChildren().remove(node));
+ }
+
+ private List<Segment> currentContent = new ArrayList<>();
+ private List<TextNode> currentTextNodes = new ArrayList<>();
+
+ public boolean updateContent(List<Segment> content) {
+ if (currentContent.equals(content)) {
+ return false;
+ }
+ if (debugOut) System.err.println("update " + getLineIndex() + ": TextLayer");
+
+ for (TextNode c : currentTextNodes) {
+ cache.releaseElement(c);
+ }
+ currentTextNodes.clear();
+
+ for (Segment segment : content) {
+ TextNode node = cache.getElement();
+ node.updateText(segment.text);
+ node.getStyleClass().setAll(segment.styleClasses);
+ currentTextNodes.add(node);
+ }
+
+ if (content.isEmpty()) {
+ TextNode node = cache.getElement();
+ node.updateText("");
+ currentTextNodes.add(node);
+ }
+
+ currentContent = content;
+
+// applyCss();
+// layout();
+ return true;
+ }
+
+ public int getCaretIndexAtPoint(Point2D point) {
+ Point2D scenePoint = localToScene(point);
+ int offset = 0;
+ for (TextNode t : this.currentTextNodes) {
+ if (t.localToScene(t.getBoundsInLocal()).contains(scenePoint)) {
+ int idx = t.getCaretIndexAtPoint(t.sceneToLocal(scenePoint));
+ if( idx != -1 ) {
+// System.err.println("returning " + idx + " + " + offset);
+ int result = idx + offset;
+// System.err.println("==> (LineNode)" + result);
+ return result;
+ }
+ }
+ offset += t.getText().length();
+ }
+
+ return -1;
+ }
+
+ public double getCharLocation(int charOffset) {
+ double result = 0;
+
+ int startOffset = 0;
+ for (TextNode t : this.currentTextNodes) {
+ int len = t.getText().length();
+
+ if (charOffset >= startOffset && charOffset <= startOffset + len) {
+ // hit
+ int textNodeOffset = charOffset - startOffset;
+ result = t.getCharLocation(textNodeOffset);
+ break;
+ }
+ else if (charOffset > startOffset + len) {
+ result = t.getLayoutX() + t.getWidth();
+ }
+ startOffset += len;
+ }
+ return result;
+
+
+// double location = 0;
+// int offset = 0;
+// for (TextNode t : this.currentTextNodes) {
+// int length = t.getText().length();
+// int endOffset = offset + length;
+//
+// if (offset <= charOffset && charOffset <= endOffset ) {
+// int localCharOffset = charOffset - offset;
+// location = t.getLayoutX() + t.getCharLocation(localCharOffset);
+// break;
+// }
+// else if (charOffset > endOffset) {
+// location = t.getLayoutX() + t.getWidth();
+// }
+// offset += length;
+// }
+// System.err.println("getCharLocation(" + charOffset + ") -> " + location);
+// return location;
+ }
+
+ @Override
+ protected void layoutChildren() {
+ super.layoutChildren();
+ if (debugOut) System.err.println("layout Line "+getLineIndex()+": TextLayer");
+ }
+
+ protected String getText() {
+ StringBuilder b = new StringBuilder();
+ for (TextNode t : this.currentTextNodes) {
+ b.append(t.getText());
+ }
+ return b.toString();
+ }
+
+ }
+
+ public class SelectionLayer extends Region {
+ private Region selectionMarker = new Region();
+
+ private com.google.common.collect.Range<Integer> selection;
+
+ public SelectionLayer() {
+ this.selectionMarker.getStyleClass().add("selection-marker");
+ this.selectionMarker.setManaged(false);
+
+ this.getChildren().setAll(this.selectionMarker);
+ }
+
+ private boolean isSelectionChange(com.google.common.collect.Range<Integer> localSelection) {
+// System.err.println("change? " + localSelection + " vs " + selection);
+ if (localSelection == null && this.selection == null) {
+ return false;
+ }
+ return this.selection == null || !this.selection.equals(localSelection);
+ }
+
+ public void updateSelection(com.google.common.collect.Range<Integer> localSelection) {
+ if (isSelectionChange(localSelection)) {
+ System.err.println("updating selection for " + index);
+ if (localSelection == null) {
+ this.selectionMarker.setVisible(false);
+ }
+ else {
+ this.selectionMarker.setVisible(true);
+ }
+
+ this.selection = localSelection;
+ requestLayout();
+ if (debugAnimation) {
+ debugUpdateSelection.play();
+ }
+ }
+ }
+
+ @Override
+ protected void layoutChildren() {
+ //if (model == null) return;
+
+ if (selection != null) {
+ textLayer.layout();
+
+ double begin = textLayer.getCharLocation(selection.lowerEndpoint());
+ double end = textLayer.getCharLocation(selection.upperEndpoint());
+ if (selection.upperEndpoint() == lineHelper.getLength(index)) {
+ end = getWidth();
+ }
+
+
+ if (debugOut) System.err.println("layout Line "+getLineIndex()+": SelectionLayer");
+ // System.err.println("SelectionLayer: layoutChildren()");
+ // System.err.println(" setting selection to " + begin + " - " + end);
+ this.selectionMarker.resizeRelocate(begin, 0, end - begin, getHeight());
+ }
+ }
+ }
+
+ private static Animation createCaretAnimation(Node caret) {
+// Timeline t = new Timeline(
+// new KeyFrame(Duration.millis(200), new KeyValue(caret.opacityProperty(), 1, Interpolator.DISCRETE)),
+// new KeyFrame(Duration.millis(200), new KeyValue(caret.opacityProperty(), 0))
+// );
+// t.setCycleCount(Animation.INDEFINITE);
+// return t;
+
+ FadeTransition t = new FadeTransition(Duration.millis(200), caret);
+ t.setInterpolator(new Interpolator() {
+ @Override
+ protected double curve(double t) {
+ if (t < 0.5) {
+ return 0;
+ }
+ else {
+ return 1;
+ }
+ }
+ });
+ t.setAutoReverse(true);
+ t.setFromValue(1);
+ t.setToValue(0);
+ t.setCycleCount(Animation.INDEFINITE);
+
+ return t;
+ }
+
+ public class CaretLayer extends Region {
+ private int caretIndex = -1;
+
+ private Line caret = new Line();
+
+ private Animation caretAnimation;
+
+ public CaretLayer() {
+ this.caret.setVisible(false);
+ this.caret.setStrokeWidth(2);
+ this.caret.getStyleClass().add("text-caret"); //$NON-NLS-1$
+ this.caret.setVisible(false);
+ this.getChildren().add(this.caret);
+
+ this.caretAnimation = createCaretAnimation(this.caret);
+
+ this.caret.visibleProperty().addListener((x, o, n)->{
+ if (n) {
+ if (this.caretAnimation.getStatus() != Status.RUNNING) {
+ this.caretAnimation.playFromStart();
+ }
+ }
+ else {
+ this.caretAnimation.stop();
+ }
+ });
+ }
+
+ private void hideCaret() {
+ this.caret.setVisible(false);
+ }
+
+ private void showCaret() {
+ this.caret.setVisible(true);
+ }
+
+ public void updateCaret(int index) {
+ if (index != this.caretIndex) {
+ if (index == -1) {
+ hideCaret();
+ }
+ else {
+ showCaret();
+ }
+
+ this.caretIndex = index;
+ requestLayout();
+ if (debugAnimation) {
+ debugUpdateCaret.play();
+ }
+ }
+ }
+
+ public void layoutChildren() {
+ double caretOffset = getCharLocation(caretIndex);
+ if (debugOut) System.err.println("layout Line "+getLineIndex()+": CaretLayer caretIndex=" + caretIndex + " = " + caretOffset);
+ this.caret.setStartX(caretOffset);
+ this.caret.setEndX(caretOffset);
+ this.caret.setStartY(0);
+ this.caret.setEndY(getHeight());
+ this.caret.toFront();
+ }
+ }
+
+ public class AnnotationLayer extends StackPane {
+
+ private Set<TextAnnotation> currentAnnotations;
+ private Set<TextAnnotationPresenter> currentPresenters;
+
+ public AnnotationLayer() {
+ //setStyle("-fx-border-color: red; -fx-border-style: dashed; -fx-border-width: 1;");
+
+
+// setCache(true);
+// setCacheHint(CacheHint.SPEED);
+
+ }
+
+ private class AnnotationOverlay extends NodeCachePane {
+ private TextAnnotationPresenter presenter;
+
+ public AnnotationOverlay(TextAnnotationPresenter presenter) {
+ super(presenter::createNode);
+ this.presenter = presenter;
+
+// setStyle("-fx-border-color: red; -fx-border-style: dashed; -fx-border-width: 1;");
+// setBackground(new Background(new BackgroundFill(Color.LIGHTGREEN, CornerRadii.EMPTY, Insets.EMPTY)));
+// setBorder(new Border(new BorderStroke(Color.CADETBLUE, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
+ setOpacity(0.5);
+
+// setPrefHeight(10);
+// cache.addOnActivate(node->node.setManaged(false));
+
+
+ }
+
+ private Map<TextAnnotation, Node> usedNodes = new HashMap<>();
+
+ private Node getNode(TextAnnotation a) {
+ Node node = this.usedNodes.get(a);
+ if (node == null) {
+ node = getNode();
+ this.usedNodes.put(a, node);
+ }
+ return node;
+ }
+
+ private Set<TextAnnotation> current = new HashSet<>();
+
+ public void prepareNodes(Set<TextAnnotation> annotations) {
+ boolean same = current.equals(annotations);
+ System.err.println("annotation check: " + current +"\n"
+ + " versus: " + annotations + "\n"
+ + " -> " + same);
+
+ if (same) {
+ return;
+ }
+
+ current = annotations;
+
+ // release invisible nodes
+ int released = 0;
+ Iterator<Entry<TextAnnotation, Node>> iterator = this.usedNodes.entrySet().iterator();
+ while (iterator.hasNext()) {
+ Entry<TextAnnotation, Node> entry = iterator.next();
+ if (!annotations.contains(entry.getKey())) {
+ releaseNode(entry.getValue());
+ iterator.remove();
+ released ++;
+ }
+ else if (!presenter.isVisible(entry.getKey())) {
+ releaseNode(entry.getValue());
+ iterator.remove();
+ released++;
+ }
+ }
+
+ // release non used nodes
+// HashSet<Node> all = new HashSet<>(getChildren());
+// all.removeAll(usedNodes.values());
+// System.err.println("FORCE RELEASE OF " + all);
+// all.forEach(this::releaseNode);
+
+ int presented = 0;
+ // prepare nodes
+ for (TextAnnotation a : annotations) {
+ Node n = getNode(a);
+ this.presenter.updateNode(n, a);
+ presented ++;
+ }
+// iterator = this.usedNodes.entrySet().iterator();
+// while (iterator.hasNext()) {
+// Entry<TextAnnotation, Node> entry = iterator.next();
+// presenter.updateNode(entry.getValue(), entry.getKey());
+// presented ++;
+// }
+
+ if (debugOut) System.err.println("update "+getLineIndex()+": AnnotationOverlay: released " + released + " and presented " + presented + " annotation nodes");
+ if (debugOut) System.err.println("update "+getLineIndex()+": AnnotationOverlay: active " + annotations);
+ }
+
+ @Override
+ protected void layoutChildren() {
+ if (debugOut) System.err.println("layout Line "+getLineIndex()+": AnnotationLayer -> AnnotationOverlay");
+ Iterator<Entry<TextAnnotation, Node>> iterator = this.usedNodes.entrySet().iterator();
+ while (iterator.hasNext()) {
+ Entry<TextAnnotation, Node> entry = iterator.next();
+ com.google.common.collect.Range<Integer> range = entry.getKey().getRange();
+ double x = getCharLocation(range.lowerEndpoint());
+ double width = getCharLocation(range.upperEndpoint()) -x;
+ if (debugOut) System.err.println("layout " + entry.getKey() + "-> " + x + " " + width);
+ entry.getValue().resizeRelocate(x, 0, width, getHeight());
+ }
+ }
+
+ }
+
+ Map<TextAnnotationPresenter, AnnotationOverlay> overlays = new HashMap<>();
+
+ public void updateAnnoations(Set<TextAnnotation> annotations, Set<TextAnnotationPresenter> presenters) {
+ if (currentAnnotations != null && currentAnnotations.equals(annotations) &&
+ currentPresenters != null && currentPresenters.equals(presenters)) {
+ // nothing to updated
+
+ return;
+ }
+
+ if (debugOut) System.err.println("ANNOTATIONS: " + annotations);
+ if (debugOut) System.err.println("update " + getLineIndex() + ": AnnotationLayer " + annotations.size() + " / " + presenters.size());
+ Iterator<Entry<TextAnnotationPresenter, AnnotationOverlay>> iterator = overlays.entrySet().iterator();
+ // cleanup presenters
+ while (iterator.hasNext()) {
+ Entry<TextAnnotationPresenter, AnnotationOverlay> entry = iterator.next();
+ if (!presenters.contains(entry.getKey())) {
+ getChildren().remove(entry.getValue());
+ iterator.remove();
+ }
+ }
+ // update presenters
+ for (TextAnnotationPresenter presenter : presenters) {
+ Set<TextAnnotation> applicableAnnotations = annotations.stream().filter(presenter::isApplicable).collect(Collectors.toSet());
+ AnnotationOverlay overlay = this.overlays.get(presenter);
+ if (overlay == null) {
+ overlay = new AnnotationOverlay(presenter);
+ getChildren().add(overlay);
+ this.overlays.put(presenter, overlay);
+ }
+ overlay.prepareNodes(applicableAnnotations);
+ overlay.requestLayout();
+ }
+
+ if (debugOut) System.err.println("update Line " + getLineIndex() + getChildren());
+ requestLayout();
+
+ if (debugAnimation) {
+ debugUpdateAnnotations.play();
+ }
+
+ currentAnnotations = annotations;
+ currentPresenters = presenters;
+ }
+
+
+ @Override
+ protected void layoutChildren() {
+ super.layoutChildren();
+ if (debugOut) System.err.println("layout Line " + getLineIndex() + ": AnnotationLayer ");
+ }
+
+ }
+
+ private TextLayer textLayer = new TextLayer();
+ private SelectionLayer selectionLayer = new SelectionLayer();
+ private CaretLayer caretLayer = new CaretLayer();
+ private AnnotationLayer annotationLayer = new AnnotationLayer();
+
+
+ public LineNode() {
+// this.model = model;
+ getStyleClass().add("styled-text-line");
+
+ setPadding(new Insets(0, 0, 0, leftPadding));
+
+
+// setStyle("-fx-border-width: 0.1px; -fx-border-color: red");
+
+ getChildren().setAll(this.selectionLayer, this.textLayer, this.caretLayer, this.annotationLayer);
+
+ if (debugAnimation) {
+ this.debugUpdateAnnotations = new DebugMarker(Color.RED, 400);
+ this.debugUpdateText = new DebugMarker(Color.AQUAMARINE, 300);
+ this.debugUpdateSelection = new DebugMarker(Color.BLUE, 150);
+ this.debugUpdateCaret = new DebugMarker(Color.GRAY, 150);
+
+ this.debugUpdateAnnotations.setWidth(10);
+ this.debugUpdateText.setWidth(10);
+ this.debugUpdateSelection.setWidth(10);
+ this.debugUpdateCaret.setWidth(10);
+
+ this.debugBox = new HBox();
+ this.debugBox.setManaged(false);
+ this.debugBox.getChildren().addAll(this.debugUpdateAnnotations, this.debugUpdateSelection, this.debugUpdateCaret, this.debugUpdateText);
+
+ this.getChildren().add(debugBox);
+ }
+
+ }
+
+ @Override
+ protected void layoutChildren() {
+ if (debugOut) System.err.println("layout Line " + index);
+ super.layoutChildren();
+ if (debugAnimation) {
+ debugBox.resizeRelocate(0, 0, 40, getHeight());
+ debugUpdateAnnotations.resize(getHeight(), getHeight());
+ debugUpdateText.resize(getHeight(), getHeight());
+ debugUpdateSelection.resize(getHeight(), getHeight());
+ debugUpdateCaret.resize(getHeight(), getHeight());
+// debugUpdateAnnotations.resizeRelocate(0, 0, getWidth(), getHeight());
+// debugUpdateText.resizeRelocate(0, 0, getWidth(), getHeight());
+// debugUpdateSelection.resizeRelocate(0, 0, getWidth(), getHeight());
+// debugUpdateCaret.resizeRelocate(0, 0, getWidth(), getHeight());
+ }
+
+ }
+
+ public void setLineHelper(LineHelper helper) {
+ this.lineHelper = helper;
+ }
+
+ public void update(Set<TextAnnotationPresenter> presenters) {
+ requestLayout();
+ updateContent(lineHelper.getSegments(index));
+ updateSelection(lineHelper.getSelection(index));
+ updateCaret(lineHelper.getCaret(index));
+ updateAnnotations(lineHelper.getTextAnnotations(index), presenters);
+ }
+
+
+// public void update(StyledTextLine model, Set<TextAnnotationPresenter> annotationPresenter) {
+//// System.err.println("update " + model.getLineIndex());
+//
+// this.model = model;
+//
+// updateContent(model.getSegments());
+// updateSelection(model.getSelectionRange());
+// updateCaret(model.getCaretIndex());
+//
+// Set<TextAnnotation> textAnnotations = model.getAnnotations().stream().filter(m->m instanceof TextAnnotation).map(m->(TextAnnotation)m).collect(Collectors.toSet());
+// this.annotationLayer.updateAnnoations(textAnnotations, annotationPresenter);
+// }
+
+ public void updateSelection(com.google.common.collect.Range<Integer> lineSelection) {
+// System.err.println("LineNode: updateSelection " + lineSelection);
+
+ if (lineSelection != null && lineSelection.isEmpty()) {
+ this.selectionLayer.updateSelection(null);
+ }
+ else {
+ this.selectionLayer.updateSelection(lineSelection);
+ }
+ }
+
+ boolean currentLine = false;
+
+ public void updateCaret(int caret) {
+ this.caretLayer.updateCaret(caret);
+
+ updateCurrentLine(caret != -1);
+ }
+
+ public void updateCurrentLine(boolean current) {
+ if (this.currentLine != current) {
+
+ if (current) {
+ getStyleClass().add("current-line");
+ }
+ else {
+ getStyleClass().remove("current-line");
+ }
+
+ this.currentLine = current;
+ requestLayout();
+ }
+ }
+
+ public void updateContent(List<Segment> content) {
+ boolean updated = this.textLayer.updateContent(content);
+ if (updated) {
+ if (debugAnimation) {
+ debugUpdateText.play();
+ }
+ }
+ }
+
+ public void updateAnnotations(Set<TextAnnotation> annotations, Set<TextAnnotationPresenter> presenters) {
+ this.annotationLayer.updateAnnoations(annotations, presenters);
+ }
+
+
+ public int getCaretIndexAtPoint(Point2D p) {
+ return textLayer.getCaretIndexAtPoint(p);
+ }
+
+ public int getStartOffset() {
+ return lineHelper.getOffset(index);
+ }
+
+ public int getEndOffset() {
+ return lineHelper.getOffset(index) + lineHelper.getLength(index);
+ }
+
+ public double getCharLocation(int charOffset) {
+ return this.textLayer.getCharLocation(charOffset);
+ }
+
+ /**
+ * Check if the offset is between the start and end
+ *
+ * @param start
+ * the start
+ * @param end
+ * the end
+ * @return <code>true</code> if intersects the offset
+ */
+// public boolean intersectOffset(int start, int end) {
+// if (getStartOffset() > end) {
+// return false;
+// } else if (getEndOffset() < start) {
+// return false;
+// }
+// return true;
+// }
+//
+// public boolean intersects(Range globalRange) {
+// return intersectOffset(globalRange.getOffset(), globalRange.getEndOffset());
+// }
+//
+// public Range computeLineLocalIntersection(Range globalRange) {
+// Range result;
+// if (intersects(globalRange)) {
+// int begin = Math.max(0, globalRange.getOffset() - getStartOffset());
+// int end = Math.min(getEndOffset(), globalRange.getEndOffset() - getStartOffset());
+// result = new Range(begin, end - begin);
+// }
+// else {
+// result = new Range(0, 0);
+// }
+// System.err.println("Line local intersection " + this + ": " + globalRange + " -> " + result);
+// return result;
+// }
+
+ @Override
+ public String toString() {
+ return "LineNode(idx: "+getLineIndex() + ")@" + hashCode();
+ }
+
+
+ public int getLineLength() {
+ return lineHelper.getLength(index);
+ }
+
+ public void release() {
+ this.index = -1;
+ this.caretLayer.hideCaret();
+ this.selectionLayer.selection = null;
+ this.selectionLayer.selectionMarker.resize(0, 0);
+ }
+
+ public void setIndex(int idx) {
+// System.err.println("LineNode#setIndex " + index + " -> " + idx);
+ this.index = idx;
+ }
+
+}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/LineRuler.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/LineRuler.java new file mode 100644 index 000000000..eaf9ae0c8 --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/LineRuler.java @@ -0,0 +1,57 @@ +package org.eclipse.fx.ui.controls.styledtext.internal;
+
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+import org.eclipse.fx.ui.controls.styledtext.VerticalLineFlow;
+import org.eclipse.fx.ui.controls.styledtext.model.Annotation;
+import org.eclipse.fx.ui.controls.styledtext.model.LineRulerAnnotationPresenter;
+import org.eclipse.fx.ui.controls.styledtext.model.LineRulerAnnotationPresenter.LayoutHint;
+
+import javafx.scene.Node;
+
+public class LineRuler extends VerticalLineFlow<Integer, Annotation>{
+
+ private LineRulerAnnotationPresenter.LayoutHint h;
+
+ public LineRuler(LineRulerAnnotationPresenter.LayoutHint h, Function<Integer, Set<Annotation>> converter, Predicate<Set<Annotation>> needsPresentation, Supplier<Node> nodeFactory, BiConsumer<Node, Set<Annotation>> nodePopulator) {
+ super(converter, needsPresentation, nodeFactory, nodePopulator);
+ this.h = h;
+ }
+
+ @Override
+ protected void layoutChildren() {
+ this.activeNodes.entrySet().forEach(e -> {
+ if (!yOffsetData.containsKey(e.getKey())) {
+ System.err.println("NO DATA FOR " + e);
+ return;
+ }
+ double x = 0;
+ double y = yOffsetData.get(e.getKey());
+ double width = getWidth();
+ double height = getLineHeight();
+
+ if (h == LayoutHint.ALIGN_RIGHT) {
+ e.getValue().autosize();
+ double w = e.getValue().getBoundsInLocal().getWidth();
+
+ x = width - w;
+ width = w;
+ }
+ else if (h == LayoutHint.ALIGN_CENTER) {
+ e.getValue().autosize();
+ double w = e.getValue().getBoundsInLocal().getWidth();
+
+ x = width / 2 - w / 2;
+ width = w;
+ }
+
+ e.getValue().resizeRelocate(x, y, width, height);
+ });
+ }
+
+
+}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/ScrollbarPane.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/ScrollbarPane.java new file mode 100644 index 000000000..bd19b6de7 --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/ScrollbarPane.java @@ -0,0 +1,61 @@ +package org.eclipse.fx.ui.controls.styledtext.internal;
+
+import javafx.geometry.Orientation;
+import javafx.scene.Node;
+import javafx.scene.control.ScrollBar;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Region;
+import javafx.scene.shape.Rectangle;
+
+public class ScrollbarPane<N extends Node> extends Region {
+
+ public final ScrollBar horizontal;
+ public final ScrollBar vertical;
+ private Rectangle clip;
+ private N center;
+
+ public ScrollbarPane() {
+ this.horizontal = new ScrollBar();
+ this.horizontal.setOrientation(Orientation.HORIZONTAL);
+
+ this.vertical = new ScrollBar();
+ this.vertical.setOrientation(Orientation.VERTICAL);
+
+ clip = new Rectangle();
+
+ getChildren().setAll(horizontal, vertical);
+ }
+
+ public void setCenter(N center) {
+ if (this.center != null) {
+ this.center.setClip(null);
+ getChildren().remove(this.center);
+ }
+
+ this.center = center;
+ if (this.center != null) {
+ this.center.setClip(clip);
+ getChildren().add(0, this.center);
+ }
+ }
+
+ @Override
+ protected void layoutChildren() {
+ int space = 0;
+ int w = 16;
+ this.horizontal.resizeRelocate(0, getHeight() - w, getWidth() - w , w);
+ this.vertical.resizeRelocate(getWidth() - w, 0, w, getHeight()- w);
+
+ if (this.center != null) {
+ this.clip.setX(0);
+ this.clip.setY(0);
+ this.clip.setWidth(getWidth() - w - space);
+ this.clip.setHeight(getHeight() - w - space);
+ this.center.resizeRelocate(0, 0, getWidth() - w - space, getHeight() - w - space);
+ }
+
+
+ }
+
+
+}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/Scroller.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/Scroller.java new file mode 100644 index 000000000..c6aae79ca --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/Scroller.java @@ -0,0 +1,163 @@ +package org.eclipse.fx.ui.controls.styledtext.internal;
+
+
+import com.google.common.collect.Range;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ReadOnlyDoubleProperty;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.scene.control.ScrollBar;
+
+public class Scroller {
+
+ private DoubleProperty lineHeight = new SimpleDoubleProperty(this, "lineHeight", 16); //$NON-NLS-1$
+ public DoubleProperty lineHeightProperty() {
+ return this.lineHeight;
+ }
+
+ private DoubleProperty lineCount = new SimpleDoubleProperty(this, "lineCount", 0); //$NON-NLS-1$
+ public DoubleProperty lineCountProperty() {
+ return lineCount;
+ }
+
+ private IntegerProperty visibleLineCount = new SimpleIntegerProperty(this, "visibleLineCount", 0); //$NON-NLS-1$
+
+ private DoubleProperty contentHeight = new SimpleDoubleProperty(this, "contentHeight", 0); //$NON-NLS-1$
+ public ReadOnlyDoubleProperty contentHeightProperty() {
+ return this.contentHeight;
+ }
+
+ private DoubleProperty contentAreaHeight = new SimpleDoubleProperty(this, "contentAreaHeight", 0); //$NON-NLS-1$
+ public DoubleProperty contentAreaHeightProperty() {
+ return this.contentAreaHeight;
+ }
+
+ private ObjectProperty<Range<Integer>> visibleLines = new SimpleObjectProperty<>(this, "visibleLines", Range.all()); //$NON-NLS-1$
+ public ReadOnlyObjectProperty<Range<Integer>> visibleLinesProperty() {
+ return this.visibleLines;
+ }
+
+ private DoubleProperty offset = new SimpleDoubleProperty(this, "offset", 0.01); //$NON-NLS-1$
+ public DoubleProperty offsetProperty() {
+ return this.offset;
+ }
+
+ private DoubleProperty min = new SimpleDoubleProperty(this, "min", 0); //$NON-NLS-1$
+ private DoubleProperty max = new SimpleDoubleProperty(this, "max", 0); //$NON-NLS-1$
+
+ public Scroller() {
+
+ install();
+ }
+
+ private void recomputeContentHeight(Observable x) {
+ double contentHeight = this.lineCount.get() * this.lineHeight.get();
+ this.contentHeight.set(contentHeight);
+// System.err.println("Scroller: contentHeight = " + contentHeight);
+ }
+
+ private void recomputeMax(Observable x) {
+// System.err.println("Scroller: max = " + contentHeight.get() + " - " + contentAreaHeight.get());
+ double max = Math.max(0, this.contentHeight.get() - this.contentAreaHeight.get());
+ this.max.set(max);
+// System.err.println("Scroller: max = " + max);
+ }
+
+ private void recomputeVisibleLineCount(Observable x) {
+ int visibleLineCount = (int) Math.ceil(this.contentAreaHeight.get() / this.lineHeight.get());
+ this.visibleLineCount.set(visibleLineCount);
+// System.err.println("Scroller: visibleLineCount = " + visibleLineCount);
+ }
+
+ private void recomputeVisibleLines(Observable x) {
+ int lower = (int) Math.floor(this.offset.get() / this.lineHeight.get());
+ int upper = lower + this.visibleLineCount.get();
+ Range<Integer> visibleLines = Range.closed(lower, upper);
+ this.visibleLines.set(visibleLines);
+// System.err.println("Scroller: visibleLines = " + visibleLines);
+ }
+
+ private void recomputeAll() {
+ recomputeContentHeight(null);
+ recomputeMax(null);
+ recomputeVisibleLineCount(null);
+ recomputeVisibleLines(null);
+ }
+
+ private void install() {
+ this.lineCount.addListener(this::recomputeContentHeight);
+ this.lineHeight.addListener(this::recomputeContentHeight);
+
+
+ this.contentHeight.addListener(this::recomputeMax);
+ this.contentAreaHeight.addListener(this::recomputeMax);
+
+ this.lineHeight.addListener(this::recomputeVisibleLineCount);
+ this.contentAreaHeight.addListener(this::recomputeVisibleLineCount);
+
+ this.offset.addListener(this::recomputeVisibleLines);
+ this.lineHeight.addListener(this::recomputeVisibleLines);
+ this.visibleLineCount.addListener(this::recomputeVisibleLines);
+ }
+
+ public void bind(ScrollBar bar) {
+ bar.minProperty().bind(this.min);
+
+ bar.maxProperty().bind(this.max);
+
+ bar.visibleAmountProperty().bind(this.contentAreaHeight.divide(contentHeight.divide(max)));
+
+ bar.valueProperty().bindBidirectional(this.offset);
+
+ recomputeAll();
+ }
+
+
+ public void scrollBy(int lines) {
+ scrollBy(lineHeightProperty().get() * lines);
+ }
+
+ public void scrollBy(double units) {
+
+ double newVal = this.offset.get() + units;
+ newVal = Math.min(max.get(), newVal);
+ newVal = Math.max(min.get(), newVal);
+
+ this.offset.set(newVal);
+ }
+
+ public void scrollIntoView(int line) {
+ System.err.println("scrollIntoView " + line);
+
+ double lineOffset = line * this.lineHeight.get();
+
+ if (lineOffset < this.offset.get()) {
+ // we need to scroll up
+ this.offset.set(lineOffset);
+ }
+
+ double lastLineDelta = this.contentAreaHeight.get() - this.lineHeight.get();
+ if (lineOffset > this.offset.get() + lastLineDelta) {
+ // we need to scroll down
+ this.offset.set(lineOffset - lastLineDelta);
+ }
+ }
+
+ public void scrollTo(int line) {
+ this.offset.set(line * this.lineHeight.get());
+ }
+
+ public void refresh() {
+ recomputeAll();
+ }
+
+
+
+}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/Segment.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/Segment.java new file mode 100644 index 000000000..231617b9f --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/Segment.java @@ -0,0 +1,60 @@ +package org.eclipse.fx.ui.controls.styledtext.internal;
+
+import java.util.List;
+
+public class Segment {
+ String text;
+ List<String> styleClasses;
+
+ public String getText() {
+ return text;
+ }
+
+ public List<String> getStyleClasses() {
+ return styleClasses;
+ }
+
+ public Segment(String text, List<String> styleClasses) {
+ this.text = text;
+ this.styleClasses = styleClasses;
+ }
+
+ @Override
+ public String toString() {
+ return "Segment(" + this.text + ", " + this.styleClasses + ")"; //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((styleClasses == null) ? 0 : styleClasses.hashCode());
+ result = prime * result + ((text == null) ? 0 : text.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Segment other = (Segment) obj;
+ if (styleClasses == null) {
+ if (other.styleClasses != null)
+ return false;
+ } else if (!styleClasses.equals(other.styleClasses))
+ return false;
+ if (text == null) {
+ if (other.text != null)
+ return false;
+ } else if (!text.equals(other.text))
+ return false;
+ return true;
+ }
+
+
+
+}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/TextNode.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/TextNode.java new file mode 100644 index 000000000..a5d9a356e --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/TextNode.java @@ -0,0 +1,543 @@ +package org.eclipse.fx.ui.controls.styledtext.internal;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import org.eclipse.fx.ui.controls.styledtext.ReuseCache;
+import org.eclipse.jdt.annotation.NonNull;
+
+import com.sun.javafx.css.converters.PaintConverter;
+
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.collections.ObservableList;
+import javafx.css.CssMetaData;
+import javafx.css.SimpleStyleableIntegerProperty;
+import javafx.css.SimpleStyleableObjectProperty;
+import javafx.css.StyleConverter;
+import javafx.css.Styleable;
+import javafx.css.StyleableProperty;
+import javafx.geometry.Bounds;
+import javafx.geometry.Point2D;
+import javafx.scene.Node;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+import javafx.scene.text.Text;
+import javafx.scene.text.TextBoundsType;
+
+public class TextNode extends HBox {
+
+ /**
+ * A strategy to decorate the text
+ */
+ public interface DecorationStrategy {
+ /**
+ * Attach the decoration on the text
+ *
+ * @param node
+ * the node the decoration is attached to
+ * @param textNode
+ * the text node decorated
+ */
+ public void attach(TextNode node, Node textNode);
+
+ /**
+ * Remove the decoration from the text
+ *
+ * @param node
+ * the node the decoration is attached to
+ * @param textNode
+ * the text node decorated
+ */
+ public void unattach(TextNode node, Node textNode);
+
+ /**
+ * Layout the decoration
+ *
+ * @param node
+ * the node the layout pass is done on
+ * @param textNode
+ * the text node decorated
+ */
+ public void layout(TextNode node, Node textNode);
+ }
+
+ @SuppressWarnings("null")
+ @NonNull
+ private static final CssMetaData<TextNode, @NonNull Paint> FILL = new CssMetaData<TextNode, @NonNull Paint>("-fx-fill", PaintConverter.getInstance(), Color.BLACK) { //$NON-NLS-1$
+
+ @Override
+ public boolean isSettable(TextNode styleable) {
+ return !styleable.fillProperty().isBound();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public StyleableProperty<@NonNull Paint> getStyleableProperty(TextNode styleable) {
+ return (StyleableProperty<@NonNull Paint>) styleable.fill;
+ }
+
+ };
+ @SuppressWarnings("null")
+ @NonNull
+ final ObjectProperty<@NonNull Paint> fill = new SimpleStyleableObjectProperty<>(FILL, this, "fill", Color.BLACK); //$NON-NLS-1$
+
+ private static String[] BASIC_STRING_CACHE = new String[256];
+
+ private static String toString(char c) {
+ String rv = null;
+ if( c < BASIC_STRING_CACHE.length ) {
+ rv = BASIC_STRING_CACHE[c];
+ if( rv == null ) {
+ BASIC_STRING_CACHE[c] = String.valueOf(c);
+ }
+ }
+ if( rv == null ) {
+ rv = String.valueOf(c);
+ }
+ return rv;
+ }
+
+ /**
+ * The paint used to fill the text
+ *
+ * @return the property to observe
+ */
+ public final @NonNull ObjectProperty<@NonNull Paint> fillProperty() {
+ return (ObjectProperty<@NonNull Paint>) this.fill;
+ }
+
+ /**
+ * @return the paint used to fill the text
+ */
+ public final @NonNull Paint getFill() {
+ return this.fillProperty().get();
+ }
+
+ /**
+ * Set a new paint
+ *
+ * @param color
+ * the paint used to fill the text
+ */
+ public final void setFill(final @NonNull Paint color) {
+ this.fillProperty().set(color);
+ }
+
+// static class DecorationStyleConverter extends StyleConverter<ParsedValue<?, DecorationStrategy>, DecorationStrategy> {
+// private static Map<String, DecorationStrategyFactory> FACTORIES;
+//
+// @SuppressWarnings("null")
+// @Override
+// public DecorationStrategy convert(ParsedValue<ParsedValue<?, DecorationStrategy>, DecorationStrategy> value, Font font) {
+// String definition = value.getValue() + ""; //$NON-NLS-1$
+//
+// if (FACTORIES == null) {
+// FACTORIES = Util.lookupServiceList(getClass(), DecorationStrategyFactory.class).stream().sorted((f1, f2) -> -1 * Integer.compare(f1.getRanking(), f2.getRanking())).collect(Collectors.toMap(f -> f.getDecorationStrategyName(), f -> f));
+// }
+//
+// String type;
+// if (definition.contains("(")) { //$NON-NLS-1$
+// type = definition.substring(0, definition.indexOf('('));
+// } else {
+// type = definition + ""; //$NON-NLS-1$
+// }
+//
+// DecorationStrategyFactory strategy = FACTORIES.get(type);
+// if (strategy != null) {
+// return strategy.create(definition.contains("(") ? definition.substring(definition.indexOf('(') + 1, definition.lastIndexOf(')')) : null); //$NON-NLS-1$
+// }
+//
+// return null;
+// }
+// }
+
+// @NonNull
+// private static final DecorationStyleConverter CONVERTER = new DecorationStyleConverter();
+
+// @SuppressWarnings("null")
+// @NonNull
+// private static final CssMetaData<TextNode, @Nullable DecorationStrategy> DECORATIONSTRATEGY = new CssMetaData<TextNode, @Nullable DecorationStrategy>("-efx-decoration", CONVERTER, null) { //$NON-NLS-1$
+//
+// @Override
+// public boolean isSettable(TextNode node) {
+// return !node.decorationStrategyProperty().isBound();
+// }
+//
+// @SuppressWarnings("unchecked")
+// @Override
+// public StyleableProperty<@Nullable DecorationStrategy> getStyleableProperty(TextNode node) {
+// return (StyleableProperty<@Nullable DecorationStrategy>) node.decorationStrategyProperty();
+// }
+//
+// };
+
+ @SuppressWarnings("null")
+ @NonNull
+ private static final CssMetaData<TextNode, @NonNull Number> TABCHARADANCE = new CssMetaData<TextNode, @NonNull Number>("-efx-tab-char-advance", StyleConverter.getSizeConverter(), Integer.valueOf(4)) { //$NON-NLS-1$
+
+ @Override
+ public boolean isSettable(TextNode styleable) {
+ return !styleable.tabCharAdvanceProperty().isBound();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public StyleableProperty<@NonNull Number> getStyleableProperty(TextNode styleable) {
+ return (StyleableProperty<@NonNull Number>) styleable.tabCharAdvance;
+ }
+
+ };
+
+ private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
+
+ static {
+ final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<CssMetaData<? extends Styleable, ?>>(Region.getClassCssMetaData());
+// styleables.add(DECORATIONSTRATEGY);
+ styleables.add(FILL);
+ STYLEABLES = Collections.unmodifiableList(styleables);
+ }
+
+ public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
+ return STYLEABLES;
+ }
+
+ @Override
+ public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
+ return getClassCssMetaData();
+ }
+
+// private @NonNull ObjectProperty<@Nullable DecorationStrategy> decorationStrategy = new SimpleStyleableObjectProperty<@Nullable DecorationStrategy>(DECORATIONSTRATEGY, this, "decorationStrategy"); //$NON-NLS-1$
+
+// /**
+// * The current strategy used for decoration
+// *
+// * @return the property to observe
+// */
+// public final @NonNull ObjectProperty<@Nullable DecorationStrategy> decorationStrategyProperty() {
+// return this.decorationStrategy;
+// }
+
+// /**
+// * @return strategy used for decoration
+// */
+// public final @Nullable DecorationStrategy getDecorationStrategy() {
+// return this.decorationStrategyProperty().get();
+// }
+
+// /**
+// * Set a new strategy used for decoration
+// *
+// * @param strategy
+// * the strategy
+// */
+// public final void setDecorationStrategy(final @Nullable DecorationStrategy strategy) {
+// this.decorationStrategyProperty().set(strategy);
+// }
+
+ @NonNull
+ IntegerProperty tabCharAdvance = new SimpleStyleableIntegerProperty(TABCHARADANCE, this, "tabCharAdvance", Integer.valueOf(4)); //$NON-NLS-1$
+
+ /**
+ * Number of chars to use for tab advance (default is 4)
+ *
+ * @return the property to observe
+ */
+ public final IntegerProperty tabCharAdvanceProperty() {
+ return this.tabCharAdvance;
+ }
+
+ /**
+ * @return the number of chars to use for tab advance (default is 4)
+ */
+ public final int getTabCharAdvance() {
+ return this.tabCharAdvanceProperty().get();
+ }
+
+ /**
+ * Set a new number for chars to advance for a tab
+ *
+ * @param tabCharAdvance
+ * the number of chars to use for tab advance (default is 4)
+ */
+ public final void setTabCharAdvance(final int tabCharAdvance) {
+ this.tabCharAdvanceProperty().set(tabCharAdvance);
+ }
+
+
+// private ReuseCache<Text> letterCache = new ReuseCache<Text>(this::createText);
+
+ private int startOffset;
+ private List<Integer> tabPositions = new ArrayList<>();
+ private String originalText;
+// private final HBox textNode;
+// private final Text textNode;
+
+ private List<Text> activeLetters = new ArrayList<>();
+ private ReuseCache<Text> cache;
+
+
+ /**
+ * Create a new styled text node
+ *
+ * @param text
+ * the text
+ */
+ public TextNode(String text) {
+ setMinWidth(HBox.USE_COMPUTED_SIZE);
+ cache = new ReuseCache<>(()->{
+ Text letter = new Text();
+ letter.setBoundsType(TextBoundsType.LOGICAL_VERTICAL_CENTER);
+ return letter;
+ });
+ cache.addOnActivate(node->{
+ getChildren().add(node);
+ });
+ cache.addOnRelease(node-> {
+ getChildren().remove(node);
+ });
+// cache.addOnClear(node->getChildren().remove(node));
+
+ getStyleClass().add("styled-text-node"); //$NON-NLS-1$
+ this.originalText = text;
+
+ rebuildText(text);
+
+ this.tabCharAdvance.addListener(o -> {
+ rebuildText(text);
+ });
+ cache.addOnActivate((n)->{
+ ((Text)n).fillProperty().bind(fillProperty());
+
+ });
+ cache.addOnRelease(n->{
+ ((Text)n).fillProperty().unbind();
+ });
+
+// setStyle("-fx-border-width: 0.1px; -fx-border-color: blue");
+ }
+
+ public void updateText(String text) {
+ rebuildText(text);
+ this.originalText = text;
+ }
+
+ private void rebuildText(String text) {
+ for (Text t : this.activeLetters) {
+ cache.releaseElement(t);
+ }
+ this.activeLetters.clear();
+ for( char c : processText(text).toCharArray() ) {
+ Text textNode = cache.getElement();
+ textNode.setText(toString(c));
+ this.activeLetters.add(textNode);
+ }
+ }
+
+ private String processText(String text) {
+ String tmp = text;
+ StringBuilder b = new StringBuilder();
+ for (int i = 0; i < this.tabCharAdvance.get(); i++) {
+ b.append(" "); //$NON-NLS-1$
+ }
+ int position = -1;
+ this.tabPositions.clear();
+ while ((position = tmp.indexOf('\t')) != -1) {
+ tmp = tmp.replaceFirst("\t", b.toString()); //$NON-NLS-1$
+ this.tabPositions.add(Integer.valueOf(position));
+ }
+ return tmp;
+ }
+
+// private void handleDecorationChange(ObservableValue<? extends DecorationStrategy> observable, DecorationStrategy oldValue, DecorationStrategy newValue) {
+// if (oldValue != null) {
+// oldValue.unattach(this, this.textNode);
+// }
+//
+// if (newValue != null) {
+// newValue.attach(this, this.textNode);
+// }
+// }
+
+ void setStartOffset(int startOffset) {
+ this.startOffset = startOffset;
+ }
+
+ /**
+ * @return the start offset in the file
+ */
+ @Deprecated
+ public int getStartOffset() {
+ return this.startOffset;
+ }
+
+ /**
+ * @return the end offset in the file
+ */
+ @Deprecated
+ public int getEndOffset() {
+ return this.startOffset + getText().length();
+ }
+
+ /**
+ * Check if the text node intersects with the start and end locations
+ *
+ * @param start
+ * the start
+ * @param end
+ * the end
+ * @return <code>true</code> if intersects
+ */
+ public boolean intersectOffset(int start, int end) {
+ if (getStartOffset() > end) {
+ return false;
+ } else if (getEndOffset() < start) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "'" + this.originalText + "'"; //$NON-NLS-1$//$NON-NLS-2$
+ }
+
+ @Override
+ public ObservableList<Node> getChildren() {
+ return super.getChildren();
+ }
+
+ /**
+ * @return the text
+ */
+ public String getText() {
+ return this.originalText;
+ }
+
+ protected double getFixedLetterWidth() {
+ return 8;
+ }
+
+ @Override
+ protected void layoutChildren() {
+ super.layoutChildren();
+
+// double off = 0;
+//
+// for (Text letter : this.activeLetters) {
+// letter.setLayoutX(off);
+// off += getFixedLetterWidth();
+// }
+
+// this.textNode.relocate(0, 0);
+// DecorationStrategy decorationStrategy2 = this.decorationStrategy.get();
+// if (decorationStrategy2 != null) {
+// decorationStrategy2.layout(this, this.textNode);
+// }
+ }
+
+ /**
+ * Get the caret index at the given point
+ *
+ * @param point
+ * the point relative to this node
+ * @return the index or <code>-1</code>
+ */
+ public int getCaretIndexAtPoint(Point2D point) {
+ Point2D local = this.sceneToLocal(localToScene(point));
+// System.err.println(local);
+
+ Optional<Node> charNode = this.getChildren().stream().filter( n -> n.getBoundsInParent().contains(local)).findFirst();
+ if( charNode.isPresent() ) {
+ Node node = charNode.get();
+ int idx = this.getChildren().indexOf(node);
+ Bounds bounds = node.getBoundsInParent();
+
+ // if it is the 2nd half of the character
+ if( bounds.getMinX() + bounds.getWidth() / 2 < local.getX() ) {
+ idx += 1;
+ }
+
+ int toRemove = 0;
+ for (Integer i : this.tabPositions) {
+ if (i.intValue() <= idx && idx < i.intValue() + this.tabCharAdvance.get()) {
+ toRemove += idx - i.intValue();
+ // If we are in the 2nd half of the tab we
+ // simply move one past the value
+ if ((idx - i.intValue()) % this.tabCharAdvance.get() >= this.tabCharAdvance.get() / 2) {
+ idx += 1;
+ }
+ break;
+ } else if (i.intValue() < idx) {
+ toRemove += this.tabCharAdvance.get() - 1;
+ }
+ }
+ idx -= toRemove;
+// System.err.println("==> (TextNode)" + idx);
+ return idx;
+ } else {
+// System.err.println("COULD NOT FIND NODE AT: " + local);
+// this.textNode.getChildren().stream().map( n -> n.getBoundsInParent()).forEach(System.err::println);
+ }
+
+// @SuppressWarnings("deprecation")
+// HitInfo info = this.textNode.impl_hitTestChar(this.textNode.sceneToLocal(localToScene(point)));
+// if (info != null) {
+// int idx = info.getInsertionIndex();
+// int toRemove = 0;
+// for (Integer i : this.tabPositions) {
+// if (i.intValue() <= idx && idx < i.intValue() + this.tabCharAdvance.get()) {
+// toRemove += idx - i.intValue();
+// // If we are in the 2nd half of the tab we
+// // simply move one past the value
+// if ((idx - i.intValue()) % this.tabCharAdvance.get() >= this.tabCharAdvance.get() / 2) {
+// idx += 1;
+// }
+// break;
+// } else if (i.intValue() < idx) {
+// toRemove += this.tabCharAdvance.get() - 1;
+// }
+// }
+// idx -= toRemove;
+// return idx;
+// }
+ return -1;
+ }
+
+ /**
+ * Find the x coordinate where we the character for the given index starts
+ *
+ * @param index
+ * the index
+ * @return the location or <code>0</code> if not found
+ */
+ public double getCharLocation(int index) {
+ int realIndex = index;
+ for (Integer i : this.tabPositions) {
+ if (i.intValue() < realIndex) {
+ realIndex += this.tabCharAdvance.get() - 1;
+ }
+ }
+
+ if( realIndex >= 0 && realIndex < this.getChildren().size() ) {
+ return this.localToParent(this.getChildren().get(realIndex).getBoundsInParent()).getMinX();
+ } else if( ! this.getChildren().isEmpty() ) {
+ return this.localToParent(this.getChildren().get(this.getChildren().size()-1).getBoundsInParent()).getMaxX();
+ }
+
+ return 0.0;
+
+// this.textNode.setImpl_caretPosition(realIndex);
+// PathElement[] pathElements = this.textNode.getImpl_caretShape();
+// for (PathElement e : pathElements) {
+// if (e instanceof MoveTo) {
+// return this.textNode.localToParent(((MoveTo) e).getX(), 0).getX();
+// }
+// }
+
+ }
+}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/VFlow.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/VFlow.java new file mode 100644 index 000000000..c27eb0f23 --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/internal/VFlow.java @@ -0,0 +1,323 @@ +package org.eclipse.fx.ui.controls.styledtext.internal;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.Queue;
+import java.util.Set;
+import java.util.Stack;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.IntStream;
+
+import org.eclipse.fx.ui.controls.styledtext.NodeCachePane;
+
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.IntegerBinding;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.ListProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.property.SimpleListProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.scene.Node;
+import javafx.scene.layout.Pane;
+
+import com.google.common.collect.ContiguousSet;
+import com.google.common.collect.DiscreteDomain;
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import com.google.common.collect.TreeRangeSet;
+
+public class VFlow<N extends Node> extends Pane {
+
+ private Object sync = new Object();
+
+ private int overload = 100;
+
+ private Supplier<N> nodeFactory;
+ private BiConsumer<N, Integer> nodePopulator;
+
+ private Map<Integer, N> activeNodes = new HashMap<>();
+ private Queue<N> freeNodes = new LinkedList<>();
+
+ private DoubleProperty lineHeigth = new SimpleDoubleProperty(this, "lineHeight", 16.0);
+ public DoubleProperty lineHeightProperty() {
+ return this.lineHeigth;
+ }
+ public double getLineHeight() {
+ return this.lineHeigth.get();
+ }
+ public void setLineHeight(double lineHeight) {
+ this.lineHeigth.set(lineHeight);
+ }
+
+ private IntegerProperty numberOfLines = new SimpleIntegerProperty(this, "numberOfLines", 0);
+ public IntegerProperty numberOfLinesProperty() {
+ return this.numberOfLines;
+ }
+ public int getNumberOfLines() {
+ return this.numberOfLines.get();
+ }
+ public void setNumberOfLines(int lines) {
+ this.numberOfLines.set(lines);
+ }
+
+ private DoubleProperty yOffset = new SimpleDoubleProperty(this, "yOffset", 0);
+ public DoubleProperty yOffsetProperty() {
+ return this.yOffset;
+ }
+ public double getYOffset() {
+ return this.yOffset.get();
+ }
+ public void setYOffset(double yOffset) {
+ this.yOffset.set(yOffset);
+ }
+
+ private ObjectProperty<Range<Integer>> visibleLines = new SimpleObjectProperty<>(this, "visibleLines", Range.all()); //$NON-NLS-1$
+ public ObjectProperty<Range<Integer>> visibleLinesProperty() {
+ return this.visibleLines;
+ }
+ public Range<Integer> getVisibleLines() {
+ return this.visibleLines.get();
+ }
+ public void setVisibleLines(Range<Integer> visibleLines) {
+ this.visibleLines.set(visibleLines);
+ }
+
+ private Consumer<N> onRelease;
+ private BiConsumer<Integer, N> onActivate;
+
+ protected void setOnRelease(Consumer<N> onRelease) {
+ this.onRelease = onRelease;
+ }
+
+ protected void setOnActivate(BiConsumer<Integer, N> onActivate) {
+ this.onActivate = onActivate;
+ }
+
+ public VFlow(Supplier<N> nodeFactory, BiConsumer<N, Integer> nodePopulator) {
+ this.nodeFactory = nodeFactory;
+ this.nodePopulator = nodePopulator;
+ this.visibleLines.addListener(this::onVisibleLinesChange);
+ this.yOffset.addListener((x)->requestLayout());
+ this.numberOfLines.addListener(this::onNumberOfLinesChange);
+ }
+
+
+// private void onModelChange(ListChangeListener.Change<? extends M> change) {
+// System.err.println("ON MODEL CHANGE " + change);
+// RangeSet<Integer> toUpdate = TreeRangeSet.create();
+// RangeSet<Integer> toRelease = TreeRangeSet.create();
+////
+// while (change.next()) {
+// if (change.wasUpdated()) {
+// toUpdate.add(Range.closedOpen(change.getFrom(), change.getTo()));
+// }
+// if (change.wasAdded()) {
+// toUpdate.add(Range.closedOpen(change.getFrom(), model.size()));
+// }
+// if (change.wasRemoved()) {
+// toUpdate.add(Range.closedOpen(change.getFrom(), model.size()));
+// }
+// }
+////
+////
+////
+//// triggerUpdate(toUpdate);
+//// triggerRelease(toRelease);
+//// toUpdate.add(visibleLines.get());
+//
+//
+// Range<Integer> all = Range.all();
+// RangeSet<Integer> notAll = TreeRangeSet.create();
+// notAll.add(all);
+// // always release not existing nodes
+// toRelease.addAll(notAll.complement());
+//
+// triggerRelease(toRelease);
+// triggerUpdate(toUpdate);
+//
+// requestLayout();
+// }
+
+ public void permutateNodes(Function<Integer, Integer> mapping) {
+ String permutate = "permutateNodes(): ";
+ synchronized (this.sync) {
+ Map<Integer, N> newActiveNodes = new HashMap<>();
+ Set<Integer> changedIndexes = new HashSet<>();
+ for (Entry<Integer, N> entry : this.activeNodes.entrySet()) {
+ final int curIndex = entry.getKey();
+ final int newIndex = mapping.apply(entry.getKey());
+ permutate += "\n * " + curIndex + " -> " + newIndex;
+ if (curIndex != newIndex) changedIndexes.add(newIndex);
+ newActiveNodes.put(newIndex, entry.getValue());
+ }
+ this.activeNodes = newActiveNodes;
+
+ for (Integer index : changedIndexes) {
+ // apply indices
+ if (this.onActivate != null) this.onActivate.accept(index, this.activeNodes.get(index));
+ }
+
+ }
+// System.err.println(permutate);
+ }
+
+ private void onNumberOfLinesChange(Observable x, Number o, Number n) {
+ if (n.intValue() > o.intValue()) {
+ RangeSet<Integer> toUpdate = TreeRangeSet.create();
+ toUpdate.add(Range.closedOpen(o.intValue(), n.intValue()));
+ triggerUpdate(toUpdate);
+ }
+ else if (n.intValue() < o.intValue()) {
+ RangeSet<Integer> toRelease = TreeRangeSet.create();
+ toRelease.add(Range.closed(n.intValue(), o.intValue()));
+ triggerRelease(toRelease);
+ }
+ }
+
+ private void onVisibleLinesChange(Observable x, Range<Integer> o, Range<Integer> n) {
+ RangeSet<Integer> toUpdate = TreeRangeSet.create();
+ RangeSet<Integer> toRelease = TreeRangeSet.create();
+
+ if (o != null && n != null) {
+ RangeSet<Integer> hidden = TreeRangeSet.create();
+ hidden.add(o);
+ hidden.remove(n);
+
+ toRelease.addAll(hidden);
+ }
+
+ if (n != null) {
+ toUpdate.add(getVisible());
+ }
+
+ triggerUpdate(toUpdate);
+ triggerRelease(toRelease);
+ requestLayout();
+ }
+
+ private Range<Integer> getVisible() {
+ return visibleLines.get();
+// Range<Integer> r = visibleLines.get();
+// return Range.closedOpen(r.lowerEndpoint() - overload, r.upperEndpoint() + overload);
+ }
+
+ private void triggerUpdate(RangeSet<Integer> range) {
+ Range<Integer> all = Range.closedOpen(0, getNumberOfLines());
+// System.err.println("all = " + all);
+ RangeSet<Integer> onlyExisting = range.subRangeSet(all);
+ RangeSet<Integer> onlyVisible = onlyExisting.subRangeSet(getVisible());
+// System.err.println("triggerUpdate: " + range + " -> " + onlyExisting + " -> " + onlyVisible);
+
+ onlyVisible.asRanges().stream()
+ .flatMapToInt(this::toIntStream)
+ .forEach(index -> updateNode(index));
+ }
+
+ private void triggerRelease(RangeSet<Integer> range) {
+// System.err.println("triggerRelease: " + range );
+ if (range.contains(Integer.MAX_VALUE) || range.contains(Integer.MIN_VALUE)) {
+ System.err.println(" -> infinite -.-");
+ return;
+ }
+ range.asRanges().stream()
+ .flatMapToInt(this::toIntStream)
+ .forEach(index -> releaseNode(index));
+ }
+
+ @Override
+ protected void layoutChildren() {
+ synchronized (this.sync) {
+ this.activeNodes.entrySet().forEach(e -> {
+ double x = 0;
+ double y = e.getKey() * getLineHeight() - getYOffset();
+ double width = getWidth();
+ double height = getLineHeight();
+// System.err.println("VFlow#layout " + e.getValue());
+// System.err.println("VFlow layoutChildren " + e.getKey() + ": " + x +", " + y+ ", " + width + ", " + height);
+// System.err.println(" * isVisible: " + e.getValue().isVisible());
+// System.err.println(" * isChild: " + (e.getValue().getParent() == this));
+ e.getValue().resizeRelocate(x, y, width, height);
+ });
+ }
+ }
+
+
+
+ protected void releaseNode(int lineIndex) {
+ synchronized (this.sync) {
+ N node = this.activeNodes.remove(lineIndex);
+ if (node != null) {
+// System.err.println("releasing " + lineIndex + "("+node+")");
+ node.setVisible(false);
+ this.freeNodes.offer(node);
+ if (this.onRelease != null) {
+ this.onRelease.accept(node);
+ }
+ }
+ }
+ }
+
+ protected void updateNode(int lineIndex) {
+// System.err.println("VFlow#updateNode " + lineIndex);
+// Thread.dumpStack();
+ N node = getNode(lineIndex);
+// System.err.println("VFlow#updateNode " + node);
+ node.setVisible(true);
+ this.nodePopulator.accept(node, lineIndex);
+ }
+
+ public Optional<N> getVisibleNode(int lineIndex) {
+ synchronized (this.sync) {
+ N node = this.activeNodes.get(lineIndex);
+ return Optional.ofNullable(node);
+ }
+ }
+
+ protected N getNode(int lineIndex) {
+ synchronized (this.sync) {
+// System.err.println("VFlow#getNode " + lineIndex);
+ N node = this.activeNodes.remove(lineIndex);
+// if (node != null) System.err.println(" from active");
+
+ if (node == null) {
+ if (!this.freeNodes.isEmpty()) {
+ node = this.freeNodes.poll();
+// if (node != null) System.err.println(" from free ("+node+")");
+ }
+ }
+
+ if (node == null) {
+ node = this.nodeFactory.get();
+// System.err.println(" from factory");
+ getChildren().add(node);
+ }
+
+ if (node != null) if (this.onActivate != null) this.onActivate.accept(lineIndex, node);
+ this.activeNodes.put(lineIndex, node);
+
+ return node;
+ }
+ }
+
+ IntStream toIntStream(Range<Integer> range) {
+ return ContiguousSet.create(range, DiscreteDomain.integers()).stream().mapToInt(i->i.intValue());
+ }
+
+}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/model/Annotation.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/model/Annotation.java new file mode 100644 index 000000000..c847402f5 --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/model/Annotation.java @@ -0,0 +1,4 @@ +package org.eclipse.fx.ui.controls.styledtext.model;
+
+public interface Annotation {
+}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/model/AnnotationPresenter.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/model/AnnotationPresenter.java new file mode 100644 index 000000000..0156c84ff --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/model/AnnotationPresenter.java @@ -0,0 +1,13 @@ +package org.eclipse.fx.ui.controls.styledtext.model;
+
+import java.util.Set;
+
+import javafx.scene.Node;
+
+public interface AnnotationPresenter {
+
+ boolean isApplicable(Annotation annotation);
+
+
+ Node createNode();
+}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/model/AnnotationProvider.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/model/AnnotationProvider.java new file mode 100644 index 000000000..ea5c27cab --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/model/AnnotationProvider.java @@ -0,0 +1,15 @@ +package org.eclipse.fx.ui.controls.styledtext.model;
+
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.eclipse.fx.core.Subscription;
+
+import com.google.common.collect.RangeSet;
+
+public interface AnnotationProvider {
+
+ Set<? extends Annotation> computeAnnotations(int index);
+
+ Subscription registerChangeListener(Consumer<RangeSet<Integer>> onChange);
+}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/model/LineRulerAnnotationPresenter.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/model/LineRulerAnnotationPresenter.java new file mode 100644 index 000000000..cf9b326b4 --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/model/LineRulerAnnotationPresenter.java @@ -0,0 +1,28 @@ +package org.eclipse.fx.ui.controls.styledtext.model;
+
+import java.util.Set;
+
+import javafx.beans.property.DoubleProperty;
+import javafx.scene.Node;
+
+// TODO add some kind of layout hinting to position node for example on the right side
+public interface LineRulerAnnotationPresenter extends AnnotationPresenter {
+
+ enum LayoutHint {
+ ALIGN_RIGHT,
+ ALIGN_LEFT,
+ ALIGN_CENTER;
+ }
+
+ LayoutHint getLayoutHint();
+
+ DoubleProperty getWidth();
+
+ int getOrder();
+
+ // TODO use me or remove me
+ boolean isVisible(Set<Annotation> annotation);
+
+
+ void updateNode(Node node, Set<Annotation> annotation);
+}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/model/TextAnnotation.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/model/TextAnnotation.java new file mode 100644 index 000000000..7285ef51e --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/model/TextAnnotation.java @@ -0,0 +1,7 @@ +package org.eclipse.fx.ui.controls.styledtext.model;
+
+import com.google.common.collect.Range;
+
+public interface TextAnnotation extends Annotation {
+ Range<Integer> getRange();
+}
\ No newline at end of file diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/model/TextAnnotationPresenter.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/model/TextAnnotationPresenter.java new file mode 100644 index 000000000..5247c21f1 --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/model/TextAnnotationPresenter.java @@ -0,0 +1,9 @@ +package org.eclipse.fx.ui.controls.styledtext.model;
+
+import javafx.scene.Node;
+
+public interface TextAnnotationPresenter extends AnnotationPresenter {
+
+ boolean isVisible(Annotation annotation);
+ void updateNode(Node node, Annotation annotation);
+}
diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/skin/StyledTextSkin.java b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/skin/StyledTextSkin.java index 587e5b164..3ef956899 100644 --- a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/skin/StyledTextSkin.java +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/skin/StyledTextSkin.java @@ -11,63 +11,81 @@ *******************************************************************************/ package org.eclipse.fx.ui.controls.styledtext.skin; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; - -import org.eclipse.fx.ui.controls.styledtext.StyleRange; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.eclipse.fx.core.Subscription; +import org.eclipse.fx.ui.controls.styledtext.FXBindUtil; import org.eclipse.fx.ui.controls.styledtext.StyledTextArea; -import org.eclipse.fx.ui.controls.styledtext.StyledTextArea.StyledTextLine; -import org.eclipse.fx.ui.controls.styledtext.StyledTextLayoutContainer; -import org.eclipse.fx.ui.controls.styledtext.StyledTextNode; -import org.eclipse.fx.ui.controls.styledtext.TextSelection; +import org.eclipse.fx.ui.controls.styledtext.StyledTextContent.TextChangeListener; +import org.eclipse.fx.ui.controls.styledtext.TextChangedEvent; +import org.eclipse.fx.ui.controls.styledtext.TextChangingEvent; +import org.eclipse.fx.ui.controls.styledtext.VerticalLineFlow; import org.eclipse.fx.ui.controls.styledtext.behavior.StyledTextBehavior; -import org.eclipse.jdt.annotation.NonNull; - -import com.sun.javafx.scene.control.skin.ListViewSkin; -import com.sun.javafx.scene.control.skin.VirtualFlow; - -import javafx.application.Platform; +import org.eclipse.fx.ui.controls.styledtext.internal.ContentView; +import org.eclipse.fx.ui.controls.styledtext.internal.LineHelper; +import org.eclipse.fx.ui.controls.styledtext.internal.LineRuler; +import org.eclipse.fx.ui.controls.styledtext.internal.ScrollbarPane; +import org.eclipse.fx.ui.controls.styledtext.internal.Scroller; +import org.eclipse.fx.ui.controls.styledtext.model.Annotation; +import org.eclipse.fx.ui.controls.styledtext.model.AnnotationPresenter; +import org.eclipse.fx.ui.controls.styledtext.model.AnnotationProvider; +import org.eclipse.fx.ui.controls.styledtext.model.LineRulerAnnotationPresenter; +import org.eclipse.fx.ui.controls.styledtext.model.TextAnnotationPresenter; + +import com.google.common.collect.ContiguousSet; +import com.google.common.collect.DiscreteDomain; +import com.google.common.collect.RangeSet; + +import javafx.beans.InvalidationListener; +import javafx.beans.property.IntegerProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; +import javafx.collections.SetChangeListener; +import javafx.collections.transformation.SortedList; import javafx.geometry.Point2D; -import javafx.geometry.Pos; import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.control.ListCell; -import javafx.scene.control.ListView; -import javafx.scene.control.Skin; import javafx.scene.control.SkinBase; import javafx.scene.layout.HBox; -import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; -import javafx.util.Callback; /** * Styled text skin */ @SuppressWarnings("restriction") public class StyledTextSkin extends SkinBase<StyledTextArea> { - ListView<Line> contentView; - LineRuler lineRuler; - ObservableList<Line> lineList = FXCollections.observableArrayList(); + private ScrollbarPane<ContentView> contentArea; + private ContentView content; + + private Scroller scroller; + + private HBox lineRulerArea; - // private Set<LineCell> visibleCells = new HashSet<>(); - Map<LineCell, LineInfo> lineInfoMap = new HashMap<>(); +// private ObservableList<VerticalLineFlow<Integer, Annotation>> sortedLineRulerFlows; - HBox rootContainer; + private ObservableList<LineRuler> sortedLineRulerFlows; + + + private HBox rootContainer; private final StyledTextBehavior behavior; + private LineHelper lineHelper; + /** * Create a new skin * @@ -90,209 +108,269 @@ public class StyledTextSkin extends SkinBase<StyledTextArea> { super(styledText); this.behavior = behavior; this.rootContainer = new HBox(); + this.rootContainer.setSpacing(0); - this.lineRuler = new LineRuler(); - this.lineRuler.visibleProperty().bind(getSkinnable().lineRulerVisibleProperty()); - this.lineRuler.managedProperty().bind(getSkinnable().lineRulerVisibleProperty()); - this.rootContainer.getChildren().add(this.lineRuler); + this.lineRulerArea = new HBox(); + this.rootContainer.getChildren().add(this.lineRulerArea); - this.contentView = new ListView<Line>() { - @Override - protected Skin<?> createDefaultSkin() { - return new MyListViewSkin(this); - } + + Region spacer = new Region(); + spacer.getStyleClass().addAll("line-ruler", "spacer"); + spacer.setMinWidth(2); + spacer.setMaxWidth(2); + this.rootContainer.getChildren().add(spacer); + + + lineHelper = new LineHelper(getSkinnable()); + this.content = new ContentView(lineHelper); + + this.contentArea = new ScrollbarPane<>(); + + this.contentArea.setCenter(this.content); + + + Map<AnnotationProvider, Subscription> subscriptions = new HashMap<>(); + Consumer<RangeSet<Integer>> onAnnotationChange = r-> { + System.err.println("onAnnotationChange " + r); + content.updateAnnotations(r); + sortedLineRulerFlows.forEach(f->f.update(r)); }; - initializeContentViewer(this.contentView); + getSkinnable().getAnnotationProvider().addListener((SetChangeListener<? super AnnotationProvider>)(c) -> { + if (c.wasAdded()) { + System.err.println("register for2 " + c.getElementAdded()); + Subscription s = c.getElementAdded().registerChangeListener(onAnnotationChange); + subscriptions.put(c.getElementAdded(), s); + } + if (c.wasRemoved()) { + Subscription s = subscriptions.remove(c.getElementRemoved()); + if (s != null) s.dispose(); + } + }); + for (AnnotationProvider p : getSkinnable().getAnnotationProvider()) { + System.err.println("register for " + p); + if (!subscriptions.containsKey(p)) { + Subscription s = p.registerChangeListener(onAnnotationChange); + subscriptions.put(p, s); + } + } - recalculateItems(); + this.content.getStyleClass().addAll("list-view"); - this.contentView.setItems(this.lineList); + // focus delegation + this.content.focusedProperty().addListener((x, o, n)->{ + if (n) { + getSkinnable().requestFocus(); + } + }); - HBox.setHgrow(this.contentView, Priority.ALWAYS); + getBehavior().installContentListeners(this.content); - this.rootContainer.getChildren().addAll(this.contentView); + this.content.contentProperty().bind(getSkinnable().contentProperty()); + + // scroll support + this.content.setOnScroll((e)-> { + this.scroller.scrollBy(Math.round(-e.getDeltaY())); + }); + +// HBox.setHgrow(this.contentView, Priority.ALWAYS); + + HBox.setHgrow(this.contentArea, Priority.ALWAYS); + this.rootContainer.getChildren().addAll(this.contentArea); getChildren().addAll(this.rootContainer); -// styledText.getAnnotations().addListener(new SetChangeListener<StyledTextAnnotation>() { -// @Override -// public void onChanged(javafx.collections.SetChangeListener.Change<? extends StyledTextAnnotation> change) { -// if (change.getElementAdded() != null) { -// StyledTextAnnotation a = change.getElementAdded(); -// addAnnotation(a); -// System.err.println("removed " + a.getId() + " " + a.getType() + ": " + a.getText() + " @ " + a.getStartOffset() + ", " + a.getLength()); -// } -// if (change.getElementRemoved() != null) { -// StyledTextAnnotation a = change.getElementRemoved(); -// removeAnnotation(a); -// System.err.println("added " + a.getId() + " " + a.getType() + ": " + a.getText() + " @ " + a.getStartOffset() + ", " + a.getLength()); -// } -// } -// }); - styledText.caretOffsetProperty().addListener(new ChangeListener<Number>() { + // scroll stuff + this.scroller = new Scroller(); + this.scroller.contentAreaHeightProperty().bind(content.heightProperty()); - @Override - public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) { - int lineIndex = getSkinnable().getContent().getLineAtOffset(newValue.intValue()); - Line lineObject = StyledTextSkin.this.lineList.get(lineIndex); - getFlow().show(lineIndex); - - for (LineCell c : getCurrentVisibleCells()) { - if (c.domainElement == lineObject) { - // Adjust the selection - if (StyledTextSkin.this.contentView.getSelectionModel().getSelectedItem() != c.domainElement) { - StyledTextSkin.this.contentView.getSelectionModel().select(lineObject); - } - - StyledTextLayoutContainer p = (StyledTextLayoutContainer) c.getGraphic(); - p.setCaretIndex(newValue.intValue() - p.getStartOffset()); - p.requestLayout(); - - updateCurrentCursorNode(p); - - return; - } - } + this.scroller.lineHeightProperty().bind(content.lineHeightProperty()); + + this.content.lineHeightProperty().set(16); + + this.content.bindHorizontalScrollbar(this.contentArea.horizontal); + +// getSkinnable().lineCountProperty().addListener((x, o, n)-> {/* for the quantum! */}); + ((IntegerProperty)getSkinnable().lineCountProperty()).bind(content.numberOfLinesProperty()); + +// content.numberOfLinesProperty().addListener((x, o, n)-> System.err.println("FUCKING LINE COUNT CHANGE: " + n)); +// getSkinnable().lineCountProperty().addListener((x, o, n)-> System.err.println("FUCKING LINE COUNT CHANGE2: " + n)); + + + + scroller.lineCountProperty().bind(content.numberOfLinesProperty()); + + this.scroller.bind(this.contentArea.vertical); + + + this.content.textSelectionProperty().bind(getSkinnable().selectionProperty()); + this.content.caretOffsetProperty().bind(getSkinnable().caretOffsetProperty()); + + this.content.visibleLinesProperty().bind(this.scroller.visibleLinesProperty()); + + Consumer<Double> updateOffset = (offset) -> { + com.google.common.collect.Range<Integer> visibleLines = scroller.visibleLinesProperty().get(); + ContiguousSet<Integer> set = ContiguousSet.create(visibleLines, DiscreteDomain.integers()); + double lineHeight = scroller.lineHeightProperty().get(); + for (int index : set) { + + double y = index * lineHeight - offset.doubleValue(); + + for (VerticalLineFlow<Integer, Annotation> flow : sortedLineRulerFlows) { + flow.setLineOffset(index, y); + } } + }; + + content.offsetYProperty().bind(scroller.offsetProperty()); + + scroller.offsetProperty().addListener((x, o, offset)-> { + updateOffset.accept(offset.doubleValue()); + }); + + scroller.visibleLinesProperty().addListener(x->{ + updateOffset.accept(scroller.offsetProperty().get()); }); - styledText.selectionProperty().addListener(new ChangeListener<TextSelection>() { + + getSkinnable().getContent().addTextChangeListener(new TextChangeListener() { @Override - public void changed(ObservableValue<? extends TextSelection> observable, TextSelection oldValue, TextSelection newValue) { - if (newValue == null || newValue.length == 0) { - for (LineCell c : getCurrentVisibleCells()) { - if (c.getGraphic() != null) { - StyledTextLayoutContainer block = (StyledTextLayoutContainer) c.getGraphic(); - block.setSelection(new TextSelection(0, 0)); - } - } - } else { - TextSelection selection = newValue; - for (LineCell c : getCurrentVisibleCells()) { - if (c.getGraphic() != null) { - Line arg0 = c.domainElement; - StyledTextLayoutContainer block = (StyledTextLayoutContainer) c.getGraphic(); - if (selection.length > 0 && block.intersectOffset(selection.offset, selection.offset + selection.length)) { - int start = Math.max(0, selection.offset - arg0.getLineOffset()); - - if (arg0.getLineOffset() + arg0.getLineLength() > selection.offset + selection.length) { - block.setSelection(new TextSelection(start, selection.offset + selection.length - arg0.getLineOffset() - start)); - } else { - block.setSelection(new TextSelection(start, arg0.getLineLength() - start)); - } - } else { - block.setSelection(new TextSelection(0, 0)); - } - } - } - } + public void textSet(TextChangedEvent event) { + scroller.refresh(); + } + + @Override + public void textChanging(TextChangingEvent event) { + + } + + @Override + public void textChanged(TextChangedEvent event) { + } }); - } -// private void addAnnotation(StyledTextAnnotation annotation) { -// int startOffset = annotation.getStartOffset(); -// int endOffset = annotation.getStartOffset() + annotation.getLength(); -// -// int startLineIndex = getSkinnable().getContent().getLineAtOffset(startOffset); -// int endLineIndex = getSkinnable().getContent().getLineAtOffset(endOffset); -// -// Set<Line> affectedLines = IntStream.range(startLineIndex, endLineIndex + 1).mapToObj(StyledTextSkin.this.lineList::get).collect(Collectors.toSet()); -// -// for (LineCell c : getCurrentVisibleCells()) { -// if (affectedLines.contains(c.domainElement)) { -// StyledTextLayoutContainer p = (StyledTextLayoutContainer) c.getGraphic(); -// p.getAnnotations().add(annotation); -// } -// } -// } -// -// private void removeAnnotation(StyledTextAnnotation annotation) { -// int startOffset = annotation.getStartOffset(); -// int endOffset = annotation.getStartOffset() + annotation.getLength(); -// -// int startLineIndex = getSkinnable().getContent().getLineAtOffset(startOffset); -// int endLineIndex = getSkinnable().getContent().getLineAtOffset(endOffset); -// -// Set<Line> affectedLines = IntStream.range(startLineIndex, endLineIndex + 1).mapToObj(StyledTextSkin.this.lineList::get).collect(Collectors.toSet()); -// -// for (LineCell c : getCurrentVisibleCells()) { -// if (affectedLines.contains(c.domainElement)) { -// StyledTextLayoutContainer p = (StyledTextLayoutContainer) c.getGraphic(); -// p.getAnnotations().remove(annotation); -// } -// } -// } + ObservableList<LineRulerAnnotationPresenter> lineRulerPresenters = FXCollections.observableArrayList(); + SortedList<LineRulerAnnotationPresenter> sortedLineRulerPresenters = new SortedList<>(lineRulerPresenters, (a, b)->a.getOrder() - b.getOrder()); + Function<LineRulerAnnotationPresenter, LineRuler> map = (ap) -> { + Function<Integer, Set<Annotation>> converter = (index)-> + lineHelper.getAnnotations(index).stream().filter(ap::isApplicable).collect(Collectors.toSet()); - StyledTextBehavior getBehavior() { - return this.behavior; - } + Predicate<Set<Annotation>> needsPresentation = ap::isVisible; + Supplier<Node> nodeFactory = ap::createNode; + BiConsumer<Node, Set<Annotation>> populator = ap::updateNode; - /** - * Set up the content viewer - * - * @param contentView - * the content viewer - */ - protected void initializeContentViewer(ListView<Line> contentView) { - // this.contentView.getStyleClass().add("styled-text-area"); - // //$NON-NLS-1$ - // listView.setFocusTraversable(false); - contentView.focusedProperty().addListener(new ChangeListener<Boolean>() { + LineRuler flow = new LineRuler(ap.getLayoutHint(), converter, needsPresentation, nodeFactory, populator); +// VerticalLineFlow<Integer, Annotation> flow = new VerticalLineFlow<Integer, Annotation>(converter, needsPresentation, nodeFactory, populator); - @Override - public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) { - if (newValue.booleanValue()) { - getSkinnable().requestFocus(); + flow.visibleLinesProperty().bind(this.scroller.visibleLinesProperty()); + flow.numberOfLinesProperty().bind(this.content.numberOfLinesProperty()); +// flow.getModel().bindContent(this.getModel()); + + flow.minWidthProperty().bind(ap.getWidth()); + flow.prefWidthProperty().bind(ap.getWidth()); + + flow.prefWidthProperty().addListener((x, o, n)->{ + System.err.println("width change! " + o + " -> " + n); + this.rootContainer.requestLayout(); + }); + return flow; + }; + + sortedLineRulerFlows = FXCollections.observableArrayList(); + sortedLineRulerFlows.addListener((InvalidationListener)x-> { + for (VerticalLineFlow<Integer, Annotation> flow : sortedLineRulerFlows) { + flow.getStyleClass().setAll("line-ruler"); + } + }); + FXBindUtil.uniMapBindList(sortedLineRulerPresenters, sortedLineRulerFlows, map); + FXBindUtil.uniMapBindList(sortedLineRulerFlows, lineRulerArea.getChildren(), (flow)->(Node)flow); + sortedLineRulerFlows.addListener((ListChangeListener<? super VerticalLineFlow<Integer, Annotation>>)(c)-> { + while (c.next()) { + if (c.wasRemoved()) { + c.getRemoved().forEach((f)-> { + f.visibleLinesProperty().unbind(); + f.numberOfLinesProperty().unbind(); +// f.getModel().unbind(); + f.prefWidthProperty().unbind(); + }); } } }); - // listView.addEventHandler(KeyEvent.KEY_PRESSED, new - // EventHandler<KeyEvent>() { - // }); - contentView.setCellFactory(new Callback<ListView<Line>, ListCell<Line>>() { - @Override - public ListCell<Line> call(ListView<Line> arg0) { - return new LineCell(); + + + Consumer<AnnotationPresenter> installPresenter = (p) -> { + if (p instanceof LineRulerAnnotationPresenter) { + LineRulerAnnotationPresenter lrp = (LineRulerAnnotationPresenter) p; + lineRulerPresenters.add(lrp); +// installLineRulerAnnotationPresenter.accept(lrp); + } + else if (p instanceof TextAnnotationPresenter) { + TextAnnotationPresenter tp = (TextAnnotationPresenter) p; + this.content.textAnnotationPresenterProperty().add(tp); + } + }; + Consumer<AnnotationPresenter> uninstallPresenter = (p) -> { + if (p instanceof LineRulerAnnotationPresenter) { + LineRulerAnnotationPresenter lrp = (LineRulerAnnotationPresenter) p; + lineRulerPresenters.remove(lrp); + } + else if (p instanceof TextAnnotationPresenter) { + TextAnnotationPresenter tp = (TextAnnotationPresenter) p; + this.content.textAnnotationPresenterProperty().remove(tp); + } + }; + + getSkinnable().getAnnotationPresenter().addListener((SetChangeListener<? super AnnotationPresenter>)(c)-> { + if (c.wasAdded()) { + installPresenter.accept(c.getElementAdded()); + } + if (c.wasRemoved()) { + uninstallPresenter.accept(c.getElementRemoved()); } }); - contentView.setMinHeight(0); - contentView.setMinWidth(0); - // this.contentView.setFixedCellSize(value); - // this.contentView.setFixedCellSize(15); + getSkinnable().getAnnotationPresenter().forEach(installPresenter); - getBehavior().installContentListeners(contentView); } - private StyledTextLayoutContainer currentActiveNode; + private <O> void pluginBinding(ObservableValue<O> property, Consumer<O> consumer, List<Subscription> subscriptions) { + // init + consumer.accept(property.getValue()); + ChangeListener<? super O> l = (x, o, n)->consumer.accept(n); + // bind + property.addListener(l); + subscriptions.add(()->property.removeListener(l)); + } - void updateCurrentCursorNode(StyledTextLayoutContainer node) { - if (this.currentActiveNode != node) { - if (this.currentActiveNode != null) { - this.currentActiveNode.setCaretIndex(-1); - } - this.currentActiveNode = node; + private void scrollColumnIntoView(int colIndex) { + + double w = 8; + + double curOffset = contentArea.horizontal.getValue(); + double colOffset = w * colIndex; + + if (curOffset > colOffset) { + contentArea.horizontal.setValue(colOffset); + } + + double lastColDiff = content.getWidth() - w; + if (curOffset + lastColDiff < colOffset) { + contentArea.horizontal.setValue(colOffset - lastColDiff); } + } - /** - * Refresh the line ruler - */ - public void refreshLineRuler() { - this.lineRuler.refresh(); + private void scrollLineIntoView(int lineIndex) { + this.scroller.scrollIntoView(lineIndex); } - MyVirtualFlow getFlow() { - if (this.contentView == null || this.contentView.getSkin() == null) { - return null; - } - return ((MyListViewSkin) this.contentView.getSkin()).getFlow(); + StyledTextBehavior getBehavior() { + return this.behavior; } /** @@ -303,15 +381,17 @@ public class StyledTextSkin extends SkinBase<StyledTextArea> { * @return the line height */ public double getLineHeight(int caretPosition) { - int lineIndex = getSkinnable().getContent().getLineAtOffset(caretPosition); - Line lineObject = this.lineList.get(lineIndex); + return 16; - for (LineCell c : getCurrentVisibleCells()) { - if (c.domainElement == lineObject) { - return c.getHeight(); - } - } - return 0; +// int lineIndex = getSkinnable().getContent().getLineAtOffset(caretPosition); +// Line lineObject = (Line) this.lineList.get(lineIndex); +// +// for (LineCell c : getCurrentVisibleCells()) { +// if (c.getDomainElement() == lineObject) { +// return c.getHeight(); +// } +// } +// return 0; } /** @@ -326,18 +406,28 @@ public class StyledTextSkin extends SkinBase<StyledTextArea> { return null; } - int lineIndex = getSkinnable().getContent().getLineAtOffset(caretPosition); - Line lineObject = this.lineList.get(lineIndex); - for (LineCell c : getCurrentVisibleCells()) { - if (c.domainElement == lineObject) { - StyledTextLayoutContainer b = (StyledTextLayoutContainer) c.getGraphic(); - Point2D careLocation = b.getCareLocation(caretPosition - b.getStartOffset()); - Point2D tmp = getSkinnable().sceneToLocal(b.localToScene(careLocation)); - return new Point2D(tmp.getX(), getSkinnable().sceneToLocal(b.localToScene(0, b.getHeight())).getY()); - } - } + Optional<Point2D> location = this.content.getLocationInScene(caretPosition); - return null; + return location.map(l -> this.rootContainer.sceneToLocal(l)) + .map(l -> new Point2D(l.getX(), l.getY() + this.content.getLineHeight())) + .orElse(null); + + +// int lineIndex = getSkinnable().getContent().getLineAtOffset(caretPosition); +// +//// Line lineObject = (Line) this.getModel().get(lineIndex); +// +// // TODO =? +//// for (LineCell c : getCurrentVisibleCells()) { +//// if (c.getDomainElement() == lineObject) { +//// StyledTextLayoutContainer b = (StyledTextLayoutContainer) c.getGraphic(); +//// Point2D careLocation = b.getCareLocation(caretPosition - b.getStartOffset()); +//// Point2D tmp = getSkinnable().sceneToLocal(b.localToScene(careLocation)); +//// return new Point2D(tmp.getX(), getSkinnable().sceneToLocal(b.localToScene(0, b.getHeight())).getY()); +//// } +//// } +// +// return null; } /** @@ -348,7 +438,7 @@ public class StyledTextSkin extends SkinBase<StyledTextArea> { * @return the min height */ protected double computeMinHeight(double width) { - return this.contentView.minHeight(width); + return 100; //this.contentView.minHeight(width); } /** @@ -359,556 +449,24 @@ public class StyledTextSkin extends SkinBase<StyledTextArea> { * @return the min width */ protected double computeMinWidth(double height) { - return this.contentView.minWidth(height); - } - - /** - * recalculate the line items - */ - public void recalculateItems() { - if (this.lineList.size() != getSkinnable().getContent().getLineCount()) { - if (this.lineList.size() > getSkinnable().getContent().getLineCount()) { - this.lineList.remove(getSkinnable().getContent().getLineCount(), this.lineList.size()); - } else { - List<Line> tmp = new ArrayList<>(getSkinnable().getContent().getLineCount() - this.lineList.size()); - for (int i = this.lineList.size(); i < getSkinnable().getContent().getLineCount(); i++) { - tmp.add(new Line()); - } - // System.err.println("DOING AN ADD!!!!!"); - this.lineList.addAll(tmp); - // System.err.println("ADD IS DONE!"); - } - } - - redraw(); - } - - /** - * Redraw the lines - */ - public void redraw() { - for (LineCell l : getCurrentVisibleCells()) { - if (l != null) - l.update(); - } - } - - public List<LineCell> getCurrentVisibleCells() { - if (this.contentView == null || this.contentView.getSkin() == null) { - return Collections.emptyList(); - } - return ((MyListViewSkin) this.contentView.getSkin()).getFlow().getCells(); + return 100; //this.contentView.minWidth(height); } public void scrollLineUp() { - MyListViewSkin s = ((MyListViewSkin) this.contentView.getSkin()); - - LineCell top = s.getFlow().getFirstVisibleCellWithinViewPort(); - - this.contentView.scrollTo(top.getIndex() - 1); + this.scroller.scrollBy(-1); } public void scrollLineDown() { - MyListViewSkin s = ((MyListViewSkin) this.contentView.getSkin()); - - LineCell top = s.getFlow().getFirstVisibleCellWithinViewPort(); - - this.contentView.scrollTo(top.getIndex() + 1); - } - - /** - * A line cell - */ - public class LineCell extends ListCell<Line> { - Line domainElement; - List<Segment> currentSegments; - - /** - * A line cell instance - */ - public LineCell() { - getStyleClass().add("styled-text-line"); //$NON-NLS-1$ - } - - /** - * @return the domain element - */ - public Line getDomainElement() { - return this.domainElement; - } - - /** - * Update the item - */ - public void update() { - if (this.domainElement != null) { - updateItem(this.domainElement, false); - } - } - - /** - * Update the caret - */ - public void updateCaret() { - int caretPosition = getSkinnable().getCaretOffset(); - - if (caretPosition < 0) { - return; - } - } - - @Override - protected void updateItem(Line arg0, boolean arg1) { - if (arg0 != null && !arg1) { - this.domainElement = arg0; - LineInfo lineInfo = StyledTextSkin.this.lineInfoMap.get(this); - if (lineInfo == null) { - lineInfo = new LineInfo(); - lineInfo.setDomainElement(this.domainElement); - StyledTextSkin.this.lineInfoMap.put(this, lineInfo); - StyledTextSkin.this.lineRuler.getChildren().add(lineInfo); - StyledTextSkin.this.lineRuler.requestLayout(); - } else { - lineInfo.setDomainElement(this.domainElement); - StyledTextSkin.this.lineRuler.requestLayout(); - } - lineInfo.setLayoutY(getLayoutY()); - - StyledTextLayoutContainer block = (StyledTextLayoutContainer) getGraphic(); - - if (block == null) { - // System.err.println("CREATING NEW GRAPHIC BLOCK: " + this - // + " => " + this.domainElement); - block = new StyledTextLayoutContainer(getSkinnable().focusedProperty()); - block.getStyleClass().add("source-segment-container"); //$NON-NLS-1$ - setGraphic(block); - // getSkinnable().requestLayout(); - } - block.setStartOffset(arg0.getLineOffset()); - - List<Segment> segments = arg0.getSegments(); - if (segments.equals(this.currentSegments)) { - // System.err.println("EQUAL: " + this.currentSegments + " - // vs " + segments); //$NON-NLS-1$ - return; - } else { - // System.err.println("MODIFIED: " + this.currentSegments + - // " vs " + segments); - } - - this.currentSegments = segments; - - List<@NonNull StyledTextNode> texts = new ArrayList<>(); - - for (final Segment seg : this.currentSegments) { - StyledTextNode t = new StyledTextNode(seg.text); - if (seg.style.stylename != null) { - if (seg.style.stylename.contains(".")) { //$NON-NLS-1$ - List<String> styles = new ArrayList<String>(Arrays.asList(seg.style.stylename.split("\\."))); //$NON-NLS-1$ - styles.add(0, "source-segment"); //$NON-NLS-1$ - t.getStyleClass().setAll(styles); - } else { - t.getStyleClass().setAll("source-segment", seg.style.stylename); //$NON-NLS-1$ - } - - } else { - if (seg.style.foreground != null) { - t.getStyleClass().setAll("plain-source-segment"); //$NON-NLS-1$ - } else { - t.getStyleClass().setAll("source-segment"); //$NON-NLS-1$ - } - } - texts.add(t); - } - - if (segments.isEmpty()) { - StyledTextNode t = new StyledTextNode(""); //$NON-NLS-1$ - t.getStyleClass().setAll("source-segment"); //$NON-NLS-1$ - block.getTextNodes().setAll(t); - } else { - block.getTextNodes().setAll(texts); - } - - TextSelection selection = getSkinnable().getSelection(); - - if (selection.length > 0 && block.intersectOffset(selection.offset, selection.offset + selection.length)) { - int start = Math.max(0, selection.offset - arg0.getLineOffset()); - - if (arg0.getLineOffset() + arg0.getLineLength() > selection.offset + selection.length) { - block.setSelection(new TextSelection(start, selection.offset + selection.length - arg0.getLineOffset() - start)); - } else { - block.setSelection(new TextSelection(start, arg0.getLineLength() - start)); - } - } else { - block.setSelection(new TextSelection(0, 0)); - } - - if (arg0.getLineOffset() <= getSkinnable().getCaretOffset() && arg0.getLineOffset() + arg0.getText().length() >= getSkinnable().getCaretOffset()) { - block.setCaretIndex(getSkinnable().getCaretOffset() - arg0.getLineOffset()); - updateCurrentCursorNode(block); - } else { - block.setCaretIndex(-1); - } - } else { - // FIND OUT WHY WE CLEAR SO OFTEN - // System.err.println("CLEARING GRAPHICS: " + this + " => " + - // this.domainElement); - - setGraphic(null); - this.domainElement = null; - this.currentSegments = null; - LineInfo lineInfo = StyledTextSkin.this.lineInfoMap.remove(this); - if (lineInfo != null) { - lineInfo.setDomainElement(null); - } - } - - super.updateItem(arg0, arg1); - } - } - - static class RegionImpl extends Region { - public RegionImpl(Node... nodes) { - getChildren().addAll(nodes); - } - - @Override - public ObservableList<Node> getChildren() { - return super.getChildren(); - } + this.scroller.scrollBy(1); } - /** - * The line domain object - */ - public class Line implements StyledTextLine { - /** - * @return the current text - */ - @Override - public String getText() { - return removeLineending(getSkinnable().getContent().getLine(StyledTextSkin.this.lineList.indexOf(this))); - } - - @Override - public int getLineIndex() { - return StyledTextSkin.this.lineList.indexOf(this); - } - - /** - * @return the line offset - */ - public int getLineOffset() { - int idx = StyledTextSkin.this.lineList.indexOf(this); - return getSkinnable().getContent().getOffsetAtLine(idx); - } - - /** - * @return the line length - */ - public int getLineLength() { - int idx = StyledTextSkin.this.lineList.indexOf(this); - String s = getSkinnable().getContent().getLine(idx); - return s.length(); - } - - /** - * @return the different segments - */ - @SuppressWarnings("null") - public List<Segment> getSegments() { - int idx = StyledTextSkin.this.lineList.indexOf(this); - List<Segment> segments = new ArrayList<>(); - - String line = getSkinnable().getContent().getLine(idx); - if (line != null) { - int start = getSkinnable().getContent().getOffsetAtLine(idx); - int length = line.length(); - - StyleRange[] ranges = getSkinnable().getStyleRanges(start, length, true); - if (ranges == null) { - return Collections.emptyList(); - } - - if (ranges.length == 0 && line.length() > 0) { - StyleRange styleRange = new StyleRange((String) null); - styleRange.start = start; - styleRange.length = line.length(); - - Segment seg = new Segment(); - seg.text = removeLineending(line.substring(0, line.length())); - seg.style = styleRange; - - segments.add(seg); - } else { - int lastIndex = -1; - - if (ranges.length > 0) { - if (ranges[0].start - start > 0) { - StyleRange styleRange = new StyleRange((String) null); - styleRange.start = start; - styleRange.length = ranges[0].start - start; - - Segment seg = new Segment(); - seg.text = removeLineending(line.substring(0, ranges[0].start - start)); - seg.style = styleRange; - - segments.add(seg); - } - } - - for (StyleRange r : ranges) { - int begin = r.start - start; - int end = r.start - start + r.length; - - if (lastIndex != -1 && lastIndex != begin) { - StyleRange styleRange = new StyleRange((String) null); - styleRange.start = start + lastIndex; - styleRange.length = begin - lastIndex; - - Segment seg = new Segment(); - seg.text = removeLineending(line.substring(lastIndex, begin)); - seg.style = styleRange; - - segments.add(seg); - } - - Segment seg = new Segment(); - seg.text = removeLineending(line.substring(begin, end)); - seg.style = r; - - segments.add(seg); - lastIndex = end; - } - - if (lastIndex > 0 && lastIndex < line.length()) { - StyleRange styleRange = new StyleRange((String) null); - styleRange.start = start + lastIndex; - styleRange.length = line.length() - lastIndex; - - Segment seg = new Segment(); - seg.text = removeLineending(line.substring(lastIndex, line.length())); - seg.style = styleRange; - - segments.add(seg); - } - } - } - - return segments; - } - } - - class Segment { - public String text; - public StyleRange style; - - @Override - public String toString() { - return this.text + " => " + this.style; //$NON-NLS-1$ - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + getOuterType().hashCode(); - result = prime * result + ((this.style == null) ? 0 : this.style.hashCode()); - result = prime * result + ((this.text == null) ? 0 : this.text.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - Segment other = (Segment) obj; - if (!getOuterType().equals(other.getOuterType())) - return false; - if (this.style == null) { - if (other.style != null) - return false; - } else if (!this.style.equals(other.style)) - return false; - if (this.text == null) { - if (other.text != null) - return false; - } else if (!this.text.equals(other.text)) - return false; - return true; - } - - private StyledTextSkin getOuterType() { - return StyledTextSkin.this; - } - } static String removeLineending(String s) { return s.replace("\n", "").replace("\r", ""); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ } - class LineInfo extends HBox { - private Label markerLabel; - private Label lineText; - Line line; - - public LineInfo() { - this.markerLabel = new Label(); - this.markerLabel.setPrefWidth(20); - this.lineText = new Label(); - this.lineText.getStyleClass().add("line-ruler-text"); //$NON-NLS-1$ - this.lineText.setMaxWidth(Double.MAX_VALUE); - this.lineText.setMaxHeight(Double.MAX_VALUE); - this.lineText.setAlignment(Pos.CENTER_RIGHT); - HBox.setHgrow(this.lineText, Priority.ALWAYS); - getChildren().addAll(this.markerLabel, this.lineText); - } - - public void setDomainElement(Line line) { - if (line == null) { - this.line = null; - setManaged(false); - StyledTextSkin.this.rootContainer.requestLayout(); - } else { - if (line != this.line) { - this.line = line; - calculateContent(); - } - } - } - - public void calculateContent() { - Line line = this.line; - if (line != null) { - setManaged(true); - String newText = this.line.getLineIndex() + 1 + ""; //$NON-NLS-1$ - String oldText = this.lineText.getText(); - if (oldText == null) { - oldText = ""; //$NON-NLS-1$ - } - this.lineText.setText(newText); - this.markerLabel.setGraphic(getSkinnable().getLineRulerGraphicNodeFactory().call(line)); - if (newText.length() != oldText.length()) { - StyledTextSkin.this.rootContainer.requestLayout(); - } - StyledTextSkin.this.lineRuler.layout(); - } - } - } - - class LineRuler extends Pane { - boolean skipRelayout; - - @Override - protected void layoutChildren() { - if (this.skipRelayout) { - return; - } - super.layoutChildren(); - Set<Node> children = new HashSet<Node>(getChildren()); - List<LineInfo> layouted = new ArrayList<>(); - double maxWidth = 0; - for (LineCell c : getCurrentVisibleCells()) { - if (c.isVisible()) { - LineInfo lineInfo = StyledTextSkin.this.lineInfoMap.get(c); - if (lineInfo != null) { - layouted.add(lineInfo); - maxWidth = Math.max(maxWidth, lineInfo.getWidth()); - lineInfo.relocate(0, c.getLayoutY()); - lineInfo.resize(lineInfo.getWidth(), c.getHeight()); - lineInfo.setVisible(true); - children.remove(lineInfo); - } - } - } - - for (LineInfo l : layouted) { - l.resize(maxWidth, l.getHeight()); - } - - List<Node> toRemove = new ArrayList<>(); - for (Node n : children) { - if (n instanceof LineInfo) { - LineInfo l = (LineInfo) n; - if (l.line == null) { - toRemove.add(l); - } - } - n.setVisible(false); - } - - getChildren().removeAll(toRemove); - } - - public void refresh() { - new ArrayList<>(getChildren()).stream().filter(n -> n instanceof LineInfo).forEach(n -> ((LineInfo) n).calculateContent()); - } - } - - class MyListViewSkin extends ListViewSkin<Line> { - private MyVirtualFlow flow; - - public MyListViewSkin(ListView<Line> listView) { - super(listView); - } - - public MyVirtualFlow getFlow() { - return this.flow; - } - - @SuppressWarnings("unchecked") - @Override - protected VirtualFlow<ListCell<Line>> createVirtualFlow() { - this.flow = new MyVirtualFlow(); - return (VirtualFlow<ListCell<Line>>) ((VirtualFlow<?>) this.flow); - } - - } - - class MyVirtualFlow extends VirtualFlow<LineCell> { - public MyVirtualFlow() { - } - - @Override - protected void positionCell(LineCell cell, double position) { - super.positionCell(cell, position); - LineInfo lineInfo = StyledTextSkin.this.lineInfoMap.get(cell); - if (lineInfo != null) { - lineInfo.setDomainElement(cell.domainElement); - lineInfo.setLayoutY(cell.getLayoutY()); - } - Platform.runLater(new Runnable() { - - @Override - public void run() { - StyledTextSkin.this.lineRuler.requestLayout(); - } - }); - - } - - @Override - public List<LineCell> getCells() { - return super.getCells(); - } - - @Override - public void rebuildCells() { - StyledTextSkin.this.lineRuler.skipRelayout = true; - Platform.runLater(new Runnable() { - - @Override - public void run() { - StyledTextSkin.this.lineRuler.skipRelayout = false; - StyledTextSkin.this.lineRuler.requestLayout(); - } - }); - super.rebuildCells(); - } + public int getOffsetAtPosition(double x, double y) { + return content.getLineIndex(new Point2D(x, y)).orElse(-1); } }
\ No newline at end of file diff --git a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/styledtextarea.css b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/styledtextarea.css index fa0ba875d..65c987e6f 100644 --- a/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/styledtextarea.css +++ b/bundles/runtime/org.eclipse.fx.ui.controls/src/org/eclipse/fx/ui/controls/styledtext/styledtextarea.css @@ -1,19 +1,30 @@ .styled-text-area .source-segment { - -fx-font-family: Courier; - -fx-font-size: 13; + -fx-font-family: Monospace; + -fx-font-size: 12; /* @SuppressWarning */ -styled-text-color: black; -fx-fill: -styled-text-color; } +.styled-text-area .line-ruler { + -fx-background: white; +} + +.styled-text-area .line-ruler.spacer { + -fx-border-width: 0 0.1 0 0; + -fx-border-color: gray; + -fx-border-style: solid; +} + .styled-text-area .plain-source-segment { - -fx-font-family: Courier; - -fx-font-size: 13; + -fx-font-family: Monospace; + -fx-font-size: 12; } .styled-text-area .line-ruler-text { - -fx-font-family: Courier; - -fx-font-size: 13; + -fx-font-family: Monospace; + -fx-font-size: 12; + -fx-fill: gray; } /* FIXME: Fixes layouting issues with ListView */ @@ -37,6 +48,10 @@ -fx-background-color: transparent; } +.styled-text-area .current-line { + -fx-background-color: #e8f2fe +} + .styled-text-area .list-cell { -fx-padding: 0 0 0 5; } |