|
2 | 2 |
|
3 | 3 | import atlantafx.base.controls.Spacer; |
4 | 4 | import it.unimi.dsi.fastutil.ints.Int2IntArrayMap; |
5 | | -import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap; |
6 | | -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; |
7 | 5 | import jakarta.annotation.Nonnull; |
8 | 6 | import jakarta.enterprise.context.Dependent; |
9 | 7 | import jakarta.inject.Inject; |
|
16 | 14 | import javafx.scene.effect.ColorAdjust; |
17 | 15 | import javafx.scene.effect.Effect; |
18 | 16 | import javafx.scene.effect.Glow; |
19 | | -import javafx.scene.image.WritableImage; |
20 | 17 | import javafx.scene.layout.StackPane; |
21 | 18 | import javafx.scene.paint.Color; |
22 | 19 | import javafx.util.Duration; |
|
34 | 31 | import software.coley.bentofx.control.canvas.PixelCanvas; |
35 | 32 | import software.coley.bentofx.control.canvas.PixelPainter; |
36 | 33 | import software.coley.bentofx.control.canvas.PixelPainterIntArgb; |
37 | | -import software.coley.collections.Unchecked; |
38 | 34 | import software.coley.collections.box.Box; |
39 | 35 | import software.coley.collections.box.IntBox; |
40 | 36 | import software.coley.observables.AbstractObservable; |
|
47 | 43 | import software.coley.recaf.ui.control.richtext.linegraphics.AbstractTextBoundLineGraphicFactory; |
48 | 44 | import software.coley.recaf.ui.control.richtext.linegraphics.LineContainer; |
49 | 45 | import software.coley.recaf.util.Colors; |
50 | | -import software.coley.recaf.util.DesktopUtil; |
51 | 46 | import software.coley.recaf.util.FxThreadUtil; |
52 | | -import software.coley.recaf.util.NumberUtil; |
| 47 | +import software.coley.recaf.util.threading.ThreadUtil; |
53 | 48 |
|
54 | 49 | import java.util.ArrayList; |
55 | 50 | import java.util.Collections; |
|
60 | 55 | import java.util.Map; |
61 | 56 | import java.util.Objects; |
62 | 57 | import java.util.Set; |
| 58 | +import java.util.concurrent.CompletableFuture; |
63 | 59 | import java.util.function.BiConsumer; |
64 | 60 | import java.util.function.Consumer; |
65 | 61 |
|
@@ -166,55 +162,59 @@ protected void onPipelineOutputUpdate() { |
166 | 162 | * Caret pos change. |
167 | 163 | */ |
168 | 164 | private void onCaretMove(@Nonnull Change<Integer> caretChange) { |
169 | | - try { |
170 | | - // Find selected instruction (can be null) |
171 | | - Box<ASTInstruction> selected = extracted(); |
172 | | - |
173 | | - // Check if the selection was a label or supported instruction. |
174 | | - ASTInstruction current = selected.get(); |
175 | | - boolean hasSelection = false; |
176 | | - if (current instanceof ASTLabel) { |
177 | | - hasSelection = true; |
178 | | - } else if (current != null) { |
179 | | - String insnName = current.identifier().content(); |
180 | | - List<ASTElement> arguments = current.arguments(); |
181 | | - if (!arguments.isEmpty() && FLOW_INSN_SET.contains(insnName) || SWITCH_INSNS.contains(insnName)) { |
| 165 | + // Find the selected element off of the FX thread, then update our selection and line draw states on the FX thread. |
| 166 | + CompletableFuture.supplyAsync(this::findSelected, ThreadUtil.executor()).thenAcceptAsync(selected -> { |
| 167 | + try { |
| 168 | + // Check if the selection was a label or supported instruction. |
| 169 | + ASTInstruction current = selected.get(); |
| 170 | + boolean hasSelection = false; |
| 171 | + if (current instanceof ASTLabel) { |
182 | 172 | hasSelection = true; |
| 173 | + } else if (current != null) { |
| 174 | + String insnName = current.identifier().content(); |
| 175 | + List<ASTElement> arguments = current.arguments(); |
| 176 | + if (!arguments.isEmpty() && FLOW_INSN_SET.contains(insnName) || SWITCH_INSNS.contains(insnName)) { |
| 177 | + hasSelection = true; |
| 178 | + } |
183 | 179 | } |
| 180 | + currentInstructionSelection.setValue(current); |
| 181 | + drawLines.setValue(hasSelection); |
| 182 | + } catch (Throwable t) { |
| 183 | + logger.error("Error updating control flow line targets", t); |
184 | 184 | } |
185 | | - currentInstructionSelection.setValue(current); |
186 | | - drawLines.setValue(hasSelection); |
187 | | - } catch (Throwable t) { |
188 | | - logger.error("Error updating control flow line targets", t); |
189 | | - } |
| 185 | + }, FxThreadUtil.executor()); |
190 | 186 | } |
191 | 187 |
|
192 | 188 | @Nonnull |
193 | | - private Box<ASTInstruction> extracted() { |
| 189 | + private Box<ASTInstruction> findSelected() { |
194 | 190 | Box<ASTInstruction> selected = new Box<>(); |
195 | | - int pos = editor.getCodeArea().getCaretPosition(); |
196 | | - int line = editor.getCodeArea().getCurrentParagraph() + 1; |
197 | | - for (ASTElement element : astElements) { |
198 | | - if (element == null) |
199 | | - continue; |
200 | | - if (element.range().within(pos)) { |
201 | | - element.walk(ast -> { |
202 | | - if (ast instanceof ASTInstruction instruction) { |
203 | | - Location location = ast.location(); |
204 | | - if (location != null && location.line() == line) |
205 | | - selected.set(instruction); |
206 | | - else { |
207 | | - String identifier = instruction.identifier().content(); |
208 | | - if (("tableswitch".equals(identifier) |
209 | | - || "lookupswitch".equals(identifier)) |
210 | | - && ast.range().within(pos)) { |
| 191 | + try { |
| 192 | + int pos = editor.getCodeArea().getCaretPosition(); |
| 193 | + int line = editor.getCodeArea().getCurrentParagraph() + 1; |
| 194 | + for (ASTElement element : astElements) { |
| 195 | + if (element == null) |
| 196 | + continue; |
| 197 | + if (element.range().within(pos)) { |
| 198 | + element.walk(ast -> { |
| 199 | + if (ast instanceof ASTInstruction instruction) { |
| 200 | + Location location = ast.location(); |
| 201 | + if (location != null && location.line() == line) |
211 | 202 | selected.set(instruction); |
| 203 | + else { |
| 204 | + String identifier = instruction.identifier().content(); |
| 205 | + if (("tableswitch".equals(identifier) |
| 206 | + || "lookupswitch".equals(identifier)) |
| 207 | + && ast.range().within(pos)) { |
| 208 | + selected.set(instruction); |
| 209 | + } |
212 | 210 | } |
213 | 211 | } |
214 | | - } |
215 | | - return !selected.isSet(); |
216 | | - }); |
| 212 | + return !selected.isSet(); |
| 213 | + }); |
| 214 | + } |
217 | 215 | } |
| 216 | + } catch (Throwable t) { |
| 217 | + logger.warn("Error finding selected AST element", t); |
218 | 218 | } |
219 | 219 | return selected; |
220 | 220 | } |
@@ -336,7 +336,6 @@ private class ControlFlowLineFactory extends AbstractTextBoundLineGraphicFactory |
336 | 336 | private static final int MASK_SOUTH = 2; |
337 | 337 | private static final int MASK_EAST = 4; |
338 | 338 | private final ArrayList<ASTElement> switchDestinations = new ArrayList<>(64); |
339 | | - private final Int2ObjectMap<ImageRecycler> recyclers = new Int2ObjectArrayMap<>(); |
340 | 339 | private final int[] offsets = new int[containerWidth]; |
341 | 340 | private final long rainbowHueRotationDurationMillis = 3000; |
342 | 341 | private final PixelPainter<?> pixelPainter = new PixelPainterIntArgb(); |
@@ -409,10 +408,11 @@ public void apply(@Nonnull StackPane container, int paragraph) { |
409 | 408 |
|
410 | 409 | // If we've gotten to this point we will need a canvas to draw the lines on. |
411 | 410 | if (canvas == null) { |
412 | | - canvas = new CachingPixelCanvas(pixelPainter, (int) width, (int) height); |
| 411 | + canvas = new PixelCanvas(pixelPainter, (int) width, (int) height); |
413 | 412 | canvas.setManaged(false); |
414 | 413 | canvas.setMouseTransparent(true); |
415 | 414 | canvas.resize(width, height); |
| 415 | + canvas.clear(); |
416 | 416 |
|
417 | 417 | // Setup canvas styling for the render mode. |
418 | 418 | var renderMode = config.getRenderMode().getValue(); |
@@ -531,7 +531,6 @@ public void apply(@Nonnull StackPane container, int paragraph) { |
531 | 531 | } |
532 | 532 |
|
533 | 533 | public void cleanup() { |
534 | | - recyclers.clear(); |
535 | 534 | switchDestinations.clear(); |
536 | 535 | } |
537 | 536 |
|
@@ -578,87 +577,6 @@ protected void interpolate(double frac) { |
578 | 577 | } |
579 | 578 | }; |
580 | 579 | } |
581 | | - |
582 | | - /** |
583 | | - * @param width |
584 | | - * Image width. |
585 | | - * @param height |
586 | | - * Image height. |
587 | | - * |
588 | | - * @return An image recycler for the given dimensions. |
589 | | - */ |
590 | | - @Nonnull |
591 | | - private ImageRecycler getRecycler(int width, int height) { |
592 | | - int key = width << 16 | height; |
593 | | - return recyclers.computeIfAbsent(key, i -> new ImageRecycler(width, height)); |
594 | | - } |
595 | | - |
596 | | - /** |
597 | | - * Ring buffer of {@link WritableImage} keyed to a specific width/height. |
598 | | - */ |
599 | | - private static class ImageRecycler { |
600 | | - private static final int MAX_IMAGE_BUFFER; |
601 | | - private final List<WritableImage> images = new ArrayList<>(MAX_IMAGE_BUFFER); |
602 | | - private final int width, height; |
603 | | - private int index; |
604 | | - |
605 | | - static { |
606 | | - // Each row is ~18px so if we divide the screen height by that amount we should |
607 | | - // get the number of images needed to not visually show that they're recycled. |
608 | | - // |
609 | | - // Of course, we want to have some leeway, so we'll round ~18px down a bit. |
610 | | - // And if the UI scale property is assigned, we'll also accommodate for that. |
611 | | - Number scale = Unchecked.getOr(() -> NumberUtil.parse(System.getProperty("sun.java2d.uiScale", "1.0")), 1); |
612 | | - double scaledHeight = DesktopUtil.getLargestScreenSize().height * scale.doubleValue(); |
613 | | - final double rowHeight = 15D; |
614 | | - final double minRows = 72; // I have ~60 rows on a 1080p monitor so this is probably common enough fallback. |
615 | | - MAX_IMAGE_BUFFER = (int) Math.max(minRows, scaledHeight / rowHeight); |
616 | | - } |
617 | | - |
618 | | - /** |
619 | | - * @param width |
620 | | - * Width of images to create. |
621 | | - * @param height |
622 | | - * Height of images to create. |
623 | | - */ |
624 | | - public ImageRecycler(int width, int height) { |
625 | | - this.width = width; |
626 | | - this.height = height; |
627 | | - } |
628 | | - |
629 | | - /** |
630 | | - * @return Next available image. |
631 | | - */ |
632 | | - @Nonnull |
633 | | - public WritableImage next() { |
634 | | - if (index >= MAX_IMAGE_BUFFER) |
635 | | - index = 0; |
636 | | - WritableImage image; |
637 | | - if (index >= images.size()) { |
638 | | - image = new WritableImage(width, height); |
639 | | - images.add(image); |
640 | | - } else { |
641 | | - image = images.get(index); |
642 | | - } |
643 | | - index++; |
644 | | - return image; |
645 | | - } |
646 | | - } |
647 | | - |
648 | | - /** |
649 | | - * Canvas implementation that recycles backing images via {@link ImageRecycler}. |
650 | | - */ |
651 | | - private class CachingPixelCanvas extends PixelCanvas { |
652 | | - public CachingPixelCanvas(@Nonnull PixelPainter<?> pixelPainter, int width, int height) { |
653 | | - super(pixelPainter, width, height); |
654 | | - } |
655 | | - |
656 | | - @Nonnull |
657 | | - @Override |
658 | | - protected WritableImage newImage(int width, int height) { |
659 | | - return getRecycler(width, height).next(); |
660 | | - } |
661 | | - } |
662 | 580 | } |
663 | 581 |
|
664 | 582 | @SuppressWarnings("rawtypes") |
|
0 commit comments