-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathapply_patch.py
More file actions
executable file
·917 lines (794 loc) · 38.1 KB
/
Copy pathapply_patch.py
File metadata and controls
executable file
·917 lines (794 loc) · 38.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
#!/usr/bin/env python3
"""
Applies patches to ImageJ source code.
This is more reliable than using the patch command with complex patches.
"""
def apply_patches():
import os
print("=" * 60)
print("Applying patches to ImageJ source code")
print("=" * 60)
# First, update build.xml to use Java 8 instead of Java 6
build_xml_path = "ImageJ-build/build.xml"
if os.path.exists(build_xml_path):
with open(build_xml_path, 'r') as f:
build_xml = f.read()
# Replace Java 6 with Java 8
build_xml = build_xml.replace('source="1.6"', 'source="1.8"')
build_xml = build_xml.replace('target="1.6"', 'target="1.8"')
with open(build_xml_path, 'w') as f:
f.write(build_xml)
print("✓ Updated build.xml to use Java 1.8")
print("\n--- Patching Interpreter.java ---")
# Path to the Interpreter.java file
file_path = "ImageJ-build/ij/macro/Interpreter.java"
if not os.path.exists(file_path):
print(f"Error: {file_path} not found")
return False
# Read the original file
with open(file_path, 'r') as f:
lines = f.readlines()
# Modification 1: Add public static field after line 42 (0-indexed: 41)
# Find the line "static Vector imageTable"
for i, line in enumerate(lines):
if 'static Vector imageTable' in line and 'images opened in batch mode' in line:
# Insert after this line
lines.insert(i + 1, '\tpublic static boolean suppressErrorDialogs;\n')
print(f"✓ Added public suppressErrorDialogs static field at line {i+2}")
break
# Modification 2: Add runMacroSilent method after run(String, String) method
# Find the run(String, String) method and add after it
for i in range(len(lines) - 3):
if 'public String run(String macro, String arg)' in lines[i]:
# Find the closing brace of this method
brace_count = 0
method_start = i
for j in range(i, len(lines)):
if '{' in lines[j]:
brace_count += lines[j].count('{')
if '}' in lines[j]:
brace_count -= lines[j].count('}')
if brace_count == 0 and j > method_start:
# Found the closing brace
new_method = '''
\t/** Runs a macro in silent mode, suppressing error dialogs.
\t * Returns a String array: [status, message]
\t * where status is "success" or "error"
\t */
\tpublic static String[] runMacroSilent(String macro) {
\t\tboolean savedSuppressErrorDialogs = suppressErrorDialogs;
\t\tsuppressErrorDialogs = true;
\t\ttry {
\t\t\tInterpreter interp = new Interpreter();
\t\t\tString result = interp.run(macro, null);
\t\t\tsuppressErrorDialogs = savedSuppressErrorDialogs;
\t\t\treturn new String[]{"success", result != null ? result : ""};
\t\t} catch (Throwable e) {
\t\t\tsuppressErrorDialogs = savedSuppressErrorDialogs;
\t\t\tString error = e.getMessage();
\t\t\tif (error == null || error.length() == 0)
\t\t\t\terror = "" + e;
\t\t\treturn new String[]{"error", error};
\t\t}
\t}
'''
lines.insert(j + 1, new_method)
print(f"✓ Added runMacroSilent method at line {j+2}")
break
break
# Modification 3: Modify error() method around line 1357
# Find "void error(String message) {" followed by "errorMessage = message;"
for i in range(len(lines) - 2):
if 'void error(String message)' in lines[i] and 'errorMessage = message;' in lines[i+1]:
# Insert the suppressErrorDialogs check after "errorMessage = message;"
error_check = '''\t\tif (suppressErrorDialogs) {
\t\t\t// Throw exception instead of showing dialog
\t\t\terrorCount++;
\t\t\tthrow new RuntimeException(message);
\t\t}
'''
lines.insert(i + 2, error_check)
print(f"✓ Added suppressErrorDialogs check in error() method at line {i+3}")
break
# Write the modified file
with open(file_path, 'w') as f:
f.writelines(lines)
print("✓ Interpreter.java patched successfully")
# Patch IJ.java to suppress error dialogs
print("\n--- Patching IJ.java ---")
patch_ij_java()
# Patch Tokenizer.java to suppress error dialogs
print("\n--- Patching Tokenizer.java ---")
patch_tokenizer_java()
# Patch GenericDialog.java to suppress dialogs
print("\n--- Patching GenericDialog.java ---")
patch_generic_dialog_java()
# Install SizedLabel and rewrite every `new Label(...)` across the tree
# to work around CheerpJ's inflated java.awt.Label preferred height.
print("\n--- Installing SizedLabel + rewriting Label call sites ---")
patch_label_sizing_global()
# Patch MessageDialog.java to suppress dialogs
print("\n--- Patching MessageDialog.java ---")
patch_message_dialog_java()
# Patch YesNoCancelDialog.java to suppress dialogs
print("\n--- Patching YesNoCancelDialog.java ---")
patch_yes_no_cancel_dialog_java()
# Inject com.hack.viewer.* sources so ImageJ's ant build compiles them
# alongside ij.*. Required before patching ImageWindow / ImageCanvas
# (those patches reference com.hack.viewer.LazyImagePlus by name).
print("\n--- Injecting com.hack.viewer sources into ImageJ build tree ---")
inject_viewer_sources()
# Patch ImageWindow + ImageCanvas to support LazyImagePlus (Google-Maps mode)
print("\n--- Patching ImageWindow + ImageCanvas for LazyImagePlus ---")
patch_lazy_image_plus_hooks()
# Default useJFileChooser=true in the source. Setting it from JS via
# `Prefs.useJFileChooser = true` in CheerpJ library mode silently fails
# to write the primitive static, so File > Open was falling back to the
# AWT FileDialog (which doesn't render in CheerpJ).
print("\n--- Defaulting Prefs.useJFileChooser = true in source ---")
patch_prefs_defaults()
# Force OpenDialog + SaveDialog to always use the inline dispatch-thread
# branch. CheerpJ has a single JS-event-loop "thread" — its
# EventQueue.isDispatchThread() reports inconsistently between the
# jOpen check and the invokeAndWait check inside, so the
# invokeAndWait path always throws "Cannot call invokeAndWait from
# the event dispatcher thread". The inline-dispatch branch works.
print("\n--- Patching OpenDialog/SaveDialog to always use inline dispatch ---")
patch_open_save_dialog_inline()
print("\n" + "=" * 60)
print("✓ Successfully applied all patches!")
print(" - Interpreter.java: Silent macro execution")
print(" - IJ.java: Suppressed IJ.error() and IJ.showMessage()")
print(" - Tokenizer.java: Suppressed parser errors")
print(" - GenericDialog.java: Suppressed generic dialogs")
print(" - MessageDialog.java: Suppressed message dialogs")
print(" - YesNoCancelDialog.java: Suppressed yes/no/cancel dialogs")
print("=" * 60)
return True
def patch_ij_java():
"""Patch IJ.java to suppress error dialogs during silent macro execution"""
import os
file_path = "ImageJ-build/ij/IJ.java"
if not os.path.exists(file_path):
print(f"Warning: {file_path} not found, skipping")
return False
with open(file_path, 'r') as f:
content = f.read()
# Add import for Interpreter at the top
if 'import ij.macro.Interpreter;' not in content:
# Find the package declaration and first import
lines = content.split('\n')
import_inserted = False
for i, line in enumerate(lines):
if line.startswith('import ') and not import_inserted:
lines.insert(i, 'import ij.macro.Interpreter;')
import_inserted = True
break
content = '\n'.join(lines)
print("✓ Added Interpreter import to IJ.java")
# Patch error(String msg) method
error_method_pattern = 'public static void error(String msg) {'
if error_method_pattern in content and 'if (Interpreter.suppressErrorDialogs)' not in content.split(error_method_pattern)[1].split('}')[0]:
content = content.replace(
error_method_pattern,
error_method_pattern + '''
// Check if error dialogs are suppressed (for silent macro execution)
if (Interpreter.suppressErrorDialogs) {
log("Error (suppressed): " + msg);
return;
}'''
)
print("✓ Patched IJ.error(String) method")
# Patch error(String title, String msg) method
error_title_pattern = 'public static void error(String title, String msg) {'
if error_title_pattern in content:
parts = content.split(error_title_pattern)
if len(parts) > 1 and 'if (Interpreter.suppressErrorDialogs)' not in parts[1].split('String title2')[0]:
content = parts[0] + error_title_pattern + '''
// Check if error dialogs are suppressed (for silent macro execution)
if (Interpreter.suppressErrorDialogs) {
log("Error (suppressed): " + (title != null ? title + ": " : "") + msg);
return;
}
''' + parts[1]
print("✓ Patched IJ.error(String, String) method")
# Patch showMessage(String title, String msg) method
show_msg_pattern = 'public static void showMessage(String title, String msg) {'
if show_msg_pattern in content:
parts = content.split(show_msg_pattern)
if len(parts) > 1 and 'if (Interpreter.suppressErrorDialogs)' not in parts[1].split('if (redirectErrorMessages)')[0]:
content = parts[0] + show_msg_pattern + '''
// Check if error dialogs are suppressed (for silent macro execution)
if (Interpreter.suppressErrorDialogs) {
log("Message (suppressed): " + (title != null ? title + ": " : "") + msg);
return;
}
''' + parts[1]
print("✓ Patched IJ.showMessage(String, String) method")
# Patch showMessage(String msg) method
show_msg_single_pattern = 'public static void showMessage(String msg) {'
if show_msg_single_pattern in content:
parts = content.split(show_msg_single_pattern)
if len(parts) > 1 and 'if (Interpreter.suppressErrorDialogs)' not in parts[1].split('showMessage("Message"')[0]:
content = parts[0] + show_msg_single_pattern + '''
// Check if error dialogs are suppressed (for silent macro execution)
if (Interpreter.suppressErrorDialogs) {
log("Message (suppressed): " + msg);
return;
}
''' + parts[1]
print("✓ Patched IJ.showMessage(String) method")
with open(file_path, 'w') as f:
f.write(content)
print("✓ IJ.java patched successfully")
return True
def patch_tokenizer_java():
"""Patch Tokenizer.java to suppress error dialogs during parsing"""
import os
file_path = "ImageJ-build/ij/macro/Tokenizer.java"
if not os.path.exists(file_path):
print(f"Warning: {file_path} not found, skipping")
return False
with open(file_path, 'r') as f:
content = f.read()
# Patch the error() method in Tokenizer
tokenizer_error_pattern = 'void error(String message) {'
if tokenizer_error_pattern in content:
parts = content.split(tokenizer_error_pattern)
if len(parts) > 1 and 'if (Interpreter.suppressErrorDialogs)' not in parts[1].split('String title')[0]:
content = parts[0] + tokenizer_error_pattern + '''
// Check if error dialogs are suppressed (for silent macro execution)
if (Interpreter.suppressErrorDialogs) {
// Throw exception instead of showing dialog
throw new RuntimeException(message + " in line " + lineNumber);
}
''' + parts[1]
print("✓ Patched Tokenizer.error() method")
with open(file_path, 'w') as f:
f.write(content)
print("✓ Tokenizer.java patched successfully")
return True
def patch_generic_dialog_java():
"""Patch GenericDialog.java to suppress dialogs during silent execution"""
import os
file_path = "ImageJ-build/ij/gui/GenericDialog.java"
if not os.path.exists(file_path):
print(f"Warning: {file_path} not found, skipping")
return False
with open(file_path, 'r') as f:
content = f.read()
# Add import for Interpreter at the top (if not already present)
if 'import ij.macro.Interpreter;' not in content:
lines = content.split('\n')
for i, line in enumerate(lines):
if line.startswith('import ') and 'ij.macro.Interpreter' not in line:
lines.insert(i, 'import ij.macro.Interpreter;')
break
content = '\n'.join(lines)
print("✓ Added Interpreter import to GenericDialog.java")
# Patch showDialog() method to check suppressErrorDialogs flag
show_dialog_pattern = 'public void showDialog() {'
if show_dialog_pattern in content:
parts = content.split(show_dialog_pattern)
if len(parts) > 1 and 'if (Interpreter.suppressErrorDialogs)' not in parts[1].split('if (IJ.macroRunning()')[0]:
content = parts[0] + show_dialog_pattern + '''
// Check if dialogs are suppressed (for silent macro execution)
if (Interpreter.suppressErrorDialogs) {
// Don't show dialog, just dispose immediately
wasCanceled = true;
dispose();
return;
}
''' + parts[1]
print("✓ Patched GenericDialog.showDialog() method")
with open(file_path, 'w') as f:
f.write(content)
print("✓ GenericDialog.java patched successfully")
return True
def patch_label_sizing_global():
"""Work around CheerpJ's inflated java.awt.Label preferred height.
CheerpJ's AWT backend returns a Label preferredSize.height ~2-3x larger
than desktop Java (e.g., 39px for a 12pt font, vs the usual ~18px).
AWT-based dialogs and tool windows in ImageJ rely on that preferred
height for layout (GridBagLayout with weighty=0, FlowLayout, etc.), so
every Label-heavy screen ends up with huge vertical gaps and content
that overflows its window.
Root fix: ship a public SizedLabel class (ij.gui.SizedLabel) that
extends java.awt.Label and clamps getPreferredSize().height to
round(fontSize*1.7)+2 — close to desktop Java's ~18px for 12pt and a
no-op on desktop (super already returns a smaller height). Then
rewrite every `new Label(...)` call site in ImageJ's source tree to
instantiate SizedLabel instead. Because SizedLabel IS-A Label,
every existing field/parameter typed as `Label` still works.
Files touched: any .java under ij/ that contains `new Label(`. Imports
for `ij.gui.SizedLabel` are added in files outside package ij.gui
(ij.gui files see SizedLabel by the same-package rule).
"""
import os
import re
root = "ImageJ-build/ij"
if not os.path.isdir(root):
print(f"Warning: {root} not found, skipping")
return False
# 1) Drop SizedLabel.java next to MultiLineLabel in ij/gui.
trimmed_path = os.path.join(root, "gui", "SizedLabel.java")
trimmed_src = '''package ij.gui;
import java.awt.*;
/**
* A {@link java.awt.Label} subclass whose preferred and minimum sizes
* clamp the height to a reasonable multiple of the font size. This works
* around AWT backends such as CheerpJ that return an inflated preferred
* height for Label (e.g. ~39px for a 12pt font vs. the desktop ~18px),
* which otherwise causes dialogs to render with huge vertical gaps and
* content that overflows the window.
*
* <p>Drop-in replacement for {@code new java.awt.Label(...)}. On desktop
* Java the clamp is a no-op: the super class already reports a smaller
* preferred height.
*/
public class SizedLabel extends Label {
\tpublic SizedLabel() { super(); }
\tpublic SizedLabel(String text) { super(text); }
\tpublic SizedLabel(String text, int alignment) { super(text, alignment); }
\tpublic Dimension getPreferredSize() {
\t\tDimension d = super.getPreferredSize();
\t\tif (d != null) {
\t\t\tFont f = getFont();
\t\t\tif (f != null) {
\t\t\t\tint cap = Math.round(f.getSize2D() * 1.7f) + 2;
\t\t\t\tif (d.height > cap) d.height = cap;
\t\t\t}
\t\t}
\t\treturn d;
\t}
\tpublic Dimension getMinimumSize() {
\t\treturn getPreferredSize();
\t}
}
'''
with open(trimmed_path, 'w') as f:
f.write(trimmed_src)
print(f" ✓ Wrote {trimmed_path}")
# 2) Walk every .java under ij/, rewrite `new Label(` -> `new SizedLabel(`
# and add `import ij.gui.SizedLabel;` where needed.
new_label_re = re.compile(r"\bnew\s+Label\s*\(")
files_patched = 0
sites_rewritten = 0
imports_added = 0
for dirpath, _dirnames, filenames in os.walk(root):
for name in filenames:
if not name.endswith(".java"):
continue
path = os.path.join(dirpath, name)
# Skip SizedLabel itself.
if os.path.basename(path) == "SizedLabel.java":
continue
with open(path, 'r') as f:
source = f.read()
if 'new Label(' not in source:
continue
# Rewrite constructor calls. Other uses of the word Label (static
# constants Label.LEFT, type declarations, field names) are
# untouched because the regex requires `new ` immediately before.
new_source, n = new_label_re.subn('new SizedLabel(', source)
if n == 0:
continue
# Add an import if the file is outside package ij.gui and
# doesn't already import SizedLabel.
package_match = re.search(r"^package\s+([\w.]+)\s*;", new_source, re.MULTILINE)
pkg = package_match.group(1) if package_match else ""
if pkg != "ij.gui" and "ij.gui.SizedLabel" not in new_source:
# Insert the import after the last existing import line.
lines = new_source.split('\n')
last_import = -1
for i, line in enumerate(lines):
if line.startswith('import '):
last_import = i
if last_import >= 0:
lines.insert(last_import + 1, 'import ij.gui.SizedLabel;')
else:
# Fallback: after package line.
for i, line in enumerate(lines):
if line.startswith('package '):
lines.insert(i + 1, 'import ij.gui.SizedLabel;')
break
new_source = '\n'.join(lines)
imports_added += 1
with open(path, 'w') as f:
f.write(new_source)
files_patched += 1
sites_rewritten += n
rel = os.path.relpath(path, "ImageJ-build")
print(f" {rel}: {n} call site(s)")
print(f" ✓ Rewrote {sites_rewritten} `new Label(` call sites across "
f"{files_patched} files ({imports_added} imports added)")
return True
def patch_message_dialog_java():
"""Patch MessageDialog.java to suppress dialogs during silent execution"""
import os
file_path = "ImageJ-build/ij/gui/MessageDialog.java"
if not os.path.exists(file_path):
print(f"Warning: {file_path} not found, skipping")
return False
with open(file_path, 'r') as f:
content = f.read()
# Add import for Interpreter
if 'import ij.macro.Interpreter;' not in content:
lines = content.split('\n')
for i, line in enumerate(lines):
if line.startswith('import ') and 'ij.macro.Interpreter' not in line:
lines.insert(i, 'import ij.macro.Interpreter;')
break
content = '\n'.join(lines)
print("✓ Added Interpreter import to MessageDialog.java")
# Patch constructor to check suppressErrorDialogs before showing
# Find the show() call at the end of the constructor
constructor_pattern = 'public MessageDialog(Frame parent, String title, String message) {'
if constructor_pattern in content:
# Find and replace the show() call
# Look for the pattern: show(); or setVisible(true);
lines = content.split('\n')
in_constructor = False
brace_count = 0
constructor_start = -1
for i, line in enumerate(lines):
if constructor_pattern in line:
in_constructor = True
constructor_start = i
brace_count = 0
continue
if in_constructor:
brace_count += line.count('{') - line.count('}')
# Look for show() or setVisible calls
if ('show();' in line or 'setVisible(true)' in line) and 'Interpreter.suppressErrorDialogs' not in ''.join(lines[max(0,i-5):i]):
# Replace with conditional check
indent = line[:len(line) - len(line.lstrip())]
lines[i] = f'''{indent}// Check if dialogs are suppressed (for silent macro execution)
{indent}if (!Interpreter.suppressErrorDialogs) {{
{indent} show();
{indent}}}'''
print(f"✓ Patched MessageDialog constructor show() call at line {i+1}")
break
if brace_count == 0 and i > constructor_start:
# End of constructor
break
content = '\n'.join(lines)
with open(file_path, 'w') as f:
f.write(content)
print("✓ MessageDialog.java patched successfully")
return True
def patch_yes_no_cancel_dialog_java():
"""Patch YesNoCancelDialog.java to suppress dialogs during silent execution"""
import os
file_path = "ImageJ-build/ij/gui/YesNoCancelDialog.java"
if not os.path.exists(file_path):
print(f"Warning: {file_path} not found, skipping")
return False
with open(file_path, 'r') as f:
content = f.read()
# Add import for Interpreter
if 'import ij.macro.Interpreter;' not in content:
lines = content.split('\n')
for i, line in enumerate(lines):
if line.startswith('import ') and 'ij.macro.Interpreter' not in line:
lines.insert(i, 'import ij.macro.Interpreter;')
break
content = '\n'.join(lines)
print("✓ Added Interpreter import to YesNoCancelDialog.java")
# Patch constructor to check suppressErrorDialogs before showing
lines = content.split('\n')
# Find the constructors and patch show() calls
for i, line in enumerate(lines):
if ('show();' in line or 'setVisible(true)' in line) and 'YesNoCancelDialog' in ''.join(lines[max(0,i-30):i]):
# Check if already patched
if 'Interpreter.suppressErrorDialogs' not in ''.join(lines[max(0,i-5):i]):
indent = line[:len(line) - len(line.lstrip())]
lines[i] = f'''{indent}// Check if dialogs are suppressed (for silent macro execution)
{indent}if (!Interpreter.suppressErrorDialogs) {{
{indent} show();
{indent}}}'''
print(f"✓ Patched YesNoCancelDialog show() call at line {i+1}")
content = '\n'.join(lines)
with open(file_path, 'w') as f:
f.write(content)
print("✓ YesNoCancelDialog.java patched successfully")
return True
def inject_viewer_sources():
"""Copy threadhack helper sources (com.hack.viewer.* + com.hack.menu.*)
into ImageJ-build/ and update build.xml so ant compiles them alongside
ij/**."""
import os, shutil, glob
packages = [
("threadhack/java/src/com/hack/viewer", "ImageJ-build/com/hack/viewer"),
("threadhack/java/src/com/hack/menu", "ImageJ-build/com/hack/menu"),
("threadhack/java/src/com/hack/io", "ImageJ-build/com/hack/io"),
]
for src_dir, dst_dir in packages:
if not os.path.isdir(src_dir):
print(f"Warning: {src_dir} not found, skipping")
continue
os.makedirs(dst_dir, exist_ok=True)
for f in glob.glob(os.path.join(src_dir, "*.java")):
shutil.copy2(f, dst_dir)
print(f"✓ Copied {os.path.basename(f)} → {dst_dir}")
# Add ./com as a second source root in build.xml so javac picks it up.
build_xml = "ImageJ-build/build.xml"
if os.path.exists(build_xml):
with open(build_xml, 'r') as f:
content = f.read()
# Replace the single-srcdir <javac ... srcdir="./ij" ...> with one that
# uses nested <src> elements pointing at both roots.
old = '<javac srcdir="./ij"'
new = '<javac srcdir="./ij:./com"'
if old in content and new not in content:
content = content.replace(old, new, 1)
with open(build_xml, 'w') as f:
f.write(content)
print("✓ Updated build.xml javac srcdir → ./ij:./com")
else:
print("⊘ build.xml srcdir already updated (or unrecognised)")
return True
def patch_open_save_dialog_inline():
"""Route File > Open / Save As through the HTML file browser
(com.hack.io.BrowserFilePicker → window.hfs). Stock ImageJ goes via
JFileChooser.showOpenDialog which sits on Swing modal machinery that
calls EventQueue.invokeAndWait — unmeetable under CheerpJ's single-
JS-thread model.
Replaces the body of OpenDialog.jOpen(...) and SaveDialog.jSave(...)
with a call into BrowserFilePicker and a parse of the returned path
into the dir/name fields the rest of ImageJ reads.
"""
import os, re
def patch_jopen(path):
if not os.path.exists(path):
print(f"Warning: {path} not found, skipping")
return False
with open(path, 'r') as f: content = f.read()
if "[threadhack] BrowserFilePicker.open" in content:
print(f"⊘ {os.path.basename(path)} jOpen already patched")
return True
# Match the whole jOpen(String, String, String) method body up to and
# including its close brace. The method is distinctive: it sets
# LookAndFeel and calls jOpenDispatchThread / jOpenInvokeAndWait.
pat = re.compile(
r"(void\s+jOpen\s*\(\s*String\s+title\s*,\s*String\s+path\s*,\s*String\s+fileName\s*\)\s*\{)"
r".*?"
r"(\n\s*\}\s*\n)",
re.DOTALL,
)
replacement = (
r"\1\n"
"\t\t// [threadhack] BrowserFilePicker.open — HTML dialog bypasses\n"
"\t\t// Swing modal (unmeetable under CheerpJ)\n"
"\t\tString picked = com.hack.io.BrowserFilePicker.showOpenDialog(title, path);\n"
"\t\tif (picked == null || picked.length() == 0) { name = null; dir = null; return; }\n"
"\t\tjava.io.File pf = new java.io.File(picked);\n"
"\t\tname = pf.getName();\n"
"\t\tString parent = pf.getParent();\n"
"\t\tdir = (parent == null ? \"\" : parent) + java.io.File.separator;\n"
r"\2"
)
new_content, n = pat.subn(replacement, content, count=1)
if n > 0:
with open(path, 'w') as f: f.write(new_content)
print(f"✓ Patched OpenDialog.jOpen → BrowserFilePicker")
return True
print("⊘ OpenDialog.jOpen method not found")
return False
def patch_jsave(path):
if not os.path.exists(path):
print(f"Warning: {path} not found, skipping")
return False
with open(path, 'r') as f: content = f.read()
if "[threadhack] BrowserFilePicker.save" in content:
print(f"⊘ {os.path.basename(path)} jSave already patched")
return True
pat = re.compile(
r"(void\s+jSave\s*\(\s*String\s+title\s*,\s*String\s+defaultDir\s*,\s*String\s+defaultName\s*\)\s*\{)"
r".*?"
r"(\n\s*\}\s*\n)",
re.DOTALL,
)
replacement = (
r"\1\n"
"\t\t// [threadhack] BrowserFilePicker.save — HTML dialog bypasses Swing modal\n"
"\t\tString picked = com.hack.io.BrowserFilePicker.showSaveDialog(title, defaultDir, defaultName);\n"
"\t\tif (picked == null || picked.length() == 0) { name = null; dir = null; return; }\n"
"\t\tjava.io.File pf = new java.io.File(picked);\n"
"\t\tname = pf.getName();\n"
"\t\tString parent = pf.getParent();\n"
"\t\tdir = (parent == null ? \"\" : parent) + java.io.File.separator;\n"
r"\2"
)
new_content, n = pat.subn(replacement, content, count=1)
if n > 0:
with open(path, 'w') as f: f.write(new_content)
print(f"✓ Patched SaveDialog.jSave → BrowserFilePicker")
return True
print("⊘ SaveDialog.jSave method not found")
return False
ok1 = patch_jopen("ImageJ-build/ij/io/OpenDialog.java")
ok2 = patch_jsave("ImageJ-build/ij/io/SaveDialog.java")
return ok1 or ok2
def patch_prefs_defaults():
"""Set useJFileChooser=true as the field default, since CheerpJ's
library-mode proxy silently no-ops JS writes to primitive Java statics.
Also: loadOptions() reads the bitmask from IJ_Prefs.txt — without an
existing prefs file this overwrites the default back to false. Replace
that line too so the bitmask is OR'd onto our default instead."""
import os
file_path = "ImageJ-build/ij/Prefs.java"
if not os.path.exists(file_path):
print(f"Warning: {file_path} not found, skipping")
return False
with open(file_path, 'r') as f:
content = f.read()
if "[threadhack] useJFileChooser default" in content:
print("⊘ Prefs already patched")
return True
# Field default: public static boolean useJFileChooser; → = true;
old_field = "public static boolean useJFileChooser;"
new_field = "public static boolean useJFileChooser = true; // [threadhack] useJFileChooser default"
if old_field in content:
content = content.replace(old_field, new_field, 1)
print("✓ Patched Prefs.useJFileChooser default → true")
# loadOptions() bitmask: useJFileChooser = (options&JFILE_CHOOSER)!=0;
# Only overwrite from saved prefs if a value was actually present.
old_load = "useJFileChooser = (options&JFILE_CHOOSER)!=0;"
new_load = "if ((options&JFILE_CHOOSER)!=0) useJFileChooser = true; // [threadhack] preserve default"
if old_load in content:
content = content.replace(old_load, new_load, 1)
print("✓ Patched Prefs.loadOptions to preserve useJFileChooser default")
# macOS force-override: `if (IJ.isMacOSX()) useJFileChooser = false;`
# We're in a browser — macOS-ness of the USER OS doesn't matter for the
# CheerpJ-emulated filesystem. Drop it so CheerpJ-on-Mac users also get
# the file chooser.
old_mac = "if (IJ.isMacOSX()) useJFileChooser = false;"
new_mac = "// [threadhack] skip macOS force-off; irrelevant in CheerpJ"
if old_mac in content:
content = content.replace(old_mac, new_mac, 1)
print("✓ Patched Prefs macOS force-off branch")
with open(file_path, 'w') as f:
f.write(content)
return True
def patch_lazy_image_plus_hooks():
"""Patch ImageWindow.mouseWheelMoved for LazyImagePlus so plain wheel
= cursor-anchored zoom (default ImageJ behaviour is pan + Ctrl-zoom).
The rest of the LazyImagePlus interaction (drag, magnifier click,
Roi tools, cursor readout) flows through stock ImageJ paths: our
LazyImageCanvas reports level-0 coords via srcRect + magnification,
so HAND tool pan, Magnifier zoom, and all Roi creation/editing
work natively without any extra patches.
"""
import os
# ---- ImageWindow.java: plain wheel = cursor-anchored zoom ----------
iw_path = "ImageJ-build/ij/gui/ImageWindow.java"
if not os.path.exists(iw_path):
print(f"Warning: {iw_path} not found, skipping")
else:
with open(iw_path, 'r') as f:
content = f.read()
marker = "public synchronized void mouseWheelMoved(MouseWheelEvent e) {"
injection = (
"\n\t\t// [threadhack] LazyImagePlus: smooth cursor-anchored zoom.\n"
"\t\t// Bypasses ic.zoomIn/Out (fixed zoom ladder with a 4.2 %\n"
"\t\t// floor, so we can't see a huge whole-slide at full extent);\n"
"\t\t// also keeps the cursor's level-0 pixel fixed under the mouse.\n"
"\t\tif (imp instanceof com.hack.viewer.LazyImagePlus) {\n"
"\t\t\tint rot = e.getWheelRotation();\n"
"\t\t\tif (rot == 0) return;\n"
"\t\t\tdouble factor = Math.pow(1.2, -rot);\n"
"\t\t\tjava.awt.Point p = ic.getCursorLoc();\n"
"\t\t\tint sx = ic.screenX(p.x), sy = ic.screenY(p.y);\n"
"\t\t\tint cw = ic.getWidth(), ch = ic.getHeight();\n"
"\t\t\tdouble oldMag = ic.getMagnification();\n"
"\t\t\tdouble newMag = Math.max(1e-5, Math.min(32.0, oldMag * factor));\n"
"\t\t\tint l0x = ic.offScreenX(sx), l0y = ic.offScreenY(sy);\n"
"\t\t\tint newSrcW = Math.max(1, (int) Math.round(cw / newMag));\n"
"\t\t\tint newSrcH = Math.max(1, (int) Math.round(ch / newMag));\n"
"\t\t\tint newSrcX = (int) Math.round(l0x - sx / newMag);\n"
"\t\t\tint newSrcY = (int) Math.round(l0y - sy / newMag);\n"
"\t\t\tic.setSourceRect(new java.awt.Rectangle(newSrcX, newSrcY, newSrcW, newSrcH));\n"
"\t\t\tic.repaint();\n"
"\t\t\treturn;\n"
"\t\t}\n"
)
if marker in content and "[threadhack] LazyImagePlus" not in content:
content = content.replace(marker, marker + injection, 1)
with open(iw_path, 'w') as f:
f.write(content)
print("✓ Patched ImageWindow.mouseWheelMoved for LazyImagePlus")
else:
print("⊘ ImageWindow patch already applied or marker missing")
# ---- ImageCanvas.java: the only remaining patch is canEnlarge=null,
# so zoom never resizes the window. All other interaction flows through
# stock ImageJ paths (LazyImageCanvas makes that correct).
ic_path = "ImageJ-build/ij/gui/ImageCanvas.java"
if not os.path.exists(ic_path):
print(f"Warning: {ic_path} not found, skipping")
return False
with open(ic_path, 'r') as f:
content = f.read()
if "[threadhack] never grow the window on zoom" in content:
print("⊘ ImageCanvas canEnlarge patch already applied")
return True
can_enlarge_marker = "protected Dimension canEnlarge(int newWidth, int newHeight) {"
can_enlarge_inject = (
"\n\t\t// [threadhack] never grow the window on zoom — user controls size\n"
"\t\tif (true) return null;\n"
)
if can_enlarge_marker in content:
content = content.replace(can_enlarge_marker, can_enlarge_marker + can_enlarge_inject, 1)
print("✓ Patched ImageCanvas.canEnlarge → always null")
# resizeCanvas guards the actual resize on `painted` being true, but that
# field is private — a custom ImageCanvas subclass can't maintain it in
# its own paint() override, so `painted` stays false and ImageLayout's
# resizeCanvas no-ops on every frame-border drag. Drop the guard: any
# time ImageLayout asks for a new canvas size, apply it. (Stock images
# already have painted=true immediately after first paint, so this is a
# no-op change for them — it only unblocks the custom-canvas case.)
resize_old = ("if (srcRect.width<imageWidth || srcRect.height<imageHeight "
"|| (painted&&(width!=size.width||height!=size.height))) {")
resize_new = ("if (srcRect.width<imageWidth || srcRect.height<imageHeight "
"|| (width!=size.width||height!=size.height)) { // [threadhack] drop `painted` guard")
if resize_old in content:
content = content.replace(resize_old, resize_new, 1)
print("✓ Patched ImageCanvas.resizeCanvas to honour size changes without painted flag")
# After setSize(width,height) inside resizeCanvas, stock writes
# srcRect.width = (int)(dstWidth/magnification);
# srcRect.height = (int)(dstHeight/magnification);
# This clobbers the LazyImagePlus level-0 srcRect (which we keep at
# the full-resolution image extent) with viewport-pixel dimensions.
# Our canvas componentResized → setViewport() path rebuilds srcRect
# correctly from the true geometry; skip the stock writes so we have
# something sane to read when rebuilding.
stock_srcrect_block = (
"\t\t\tsetSize(width, height);\n"
"\t\t\tsrcRect.width = (int)(dstWidth/magnification);\n"
"\t\t\tsrcRect.height = (int)(dstHeight/magnification);\n"
)
patched_srcrect_block = (
"\t\t\tsetSize(width, height);\n"
"\t\t\tif (!(imp instanceof com.hack.viewer.LazyImagePlus)) {\n"
"\t\t\t\tsrcRect.width = (int)(dstWidth/magnification);\n"
"\t\t\t\tsrcRect.height = (int)(dstHeight/magnification);\n"
"\t\t\t}\n"
)
if stock_srcrect_block in content:
content = content.replace(stock_srcrect_block, patched_srcrect_block, 1)
print("✓ Patched ImageCanvas.resizeCanvas to preserve LazyImagePlus srcRect")
# zoomOut() and unzoom() have their OWN setSize() + win.pack() paths that
# bypass canEnlarge. For LazyImagePlus these would visibly shrink / reset
# the window on wheel-out or Image>Zoom>Original. Short-circuit them.
zoom_out_marker = "public void zoomOut(int sx, int sy) {"
zoom_out_inject = (
"\n\t\t// [threadhack] LazyImagePlus: shrink srcRect / adjust mag only; keep window stable\n"
"\t\tif (imp instanceof com.hack.viewer.LazyImagePlus) {\n"
"\t\t\tdouble newMag = getLowerZoomLevel(magnification);\n"
"\t\t\tif (newMag == magnification) return;\n"
"\t\t\tadjustSourceRect(newMag, offScreenX(sx), offScreenY(sy));\n"
"\t\t\trepaint();\n"
"\t\t\treturn;\n"
"\t\t}\n"
)
if zoom_out_marker in content:
content = content.replace(zoom_out_marker, zoom_out_marker + zoom_out_inject, 1)
print("✓ Patched ImageCanvas.zoomOut to avoid window resize on LazyImagePlus")
unzoom_marker = "public void unzoom() {"
unzoom_inject = (
"\n\t\t// [threadhack] LazyImagePlus: reset srcRect only, don't touch window size\n"
"\t\tif (imp instanceof com.hack.viewer.LazyImagePlus) {\n"
"\t\t\tsrcRect = new java.awt.Rectangle(0, 0, imageWidth, imageHeight);\n"
"\t\t\tsetMagnification((double) dstWidth / imageWidth);\n"
"\t\t\trepaint();\n"
"\t\t\treturn;\n"
"\t\t}\n"
)
if unzoom_marker in content:
content = content.replace(unzoom_marker, unzoom_marker + unzoom_inject, 1)
print("✓ Patched ImageCanvas.unzoom to avoid window resize on LazyImagePlus")
with open(ic_path, 'w') as f:
f.write(content)
return True
if __name__ == "__main__":
apply_patches()