6464import java .util .concurrent .ExecutorService ;
6565import java .util .concurrent .Executors ;
6666import java .util .concurrent .Future ;
67+ import java .util .regex .Matcher ;
6768import java .util .regex .Pattern ;
6869import java .util .stream .Collectors ;
6970
@@ -155,9 +156,12 @@ public PathManagerUI(final SNT plugin) {
155156 navToolbar = new NavigationToolBar (tree , scrollPane );
156157 add (scrollPane , BorderLayout .CENTER );
157158
159+ // Search Bar TreeSearchable
160+ searchableBar = new PathManagerUISearchableBar (this );
161+
158162 // Create all the menu items:
159- final SinglePathActionListener singlePathListener = new SinglePathActionListener ();
160163 final MultiPathActionListener multiPathListener = new MultiPathActionListener ();
164+ final SinglePathActionListener singlePathListener = new SinglePathActionListener (multiPathListener );
161165
162166 GuiUtils .removeIcon (this );
163167 menuBar = new JMenuBar ();
@@ -168,9 +172,13 @@ public PathManagerUI(final SNT plugin) {
168172 editMenu .add (getDeleteMenuItem (multiPathListener ));
169173 editMenu .add (getDuplicateMenuItem (singlePathListener ));
170174 editMenu .add (getRenameMenuItem (singlePathListener ));
175+ JMenuItem jmi = new JMenuItem (MultiPathActionListener .RENAME_BY_PATTERN_CMD , IconFactory .menuIcon ('\ue521' , true ));
176+ editMenu .add (jmi );
177+ jmi .setToolTipText ("Renames multiple paths using Find/Replace matching" );
178+ jmi .addActionListener (multiPathListener );
171179 editMenu .addSeparator ();
172180
173- JMenuItem jmi = new JMenuItem (MultiPathActionListener .AUTO_CONNECT_CMD , IconFactory .menuIcon (IconFactory .GLYPH .LINK ));
181+ jmi = new JMenuItem (MultiPathActionListener .AUTO_CONNECT_CMD , IconFactory .menuIcon (IconFactory .GLYPH .LINK ));
174182 jmi .setToolTipText ("<HTML>Connects 2 paths (parent-child) at an inferred fork point.<br>" +
175183 "Suggests parent based on path properties." );
176184 jmi .addActionListener (multiPathListener );
@@ -452,8 +460,7 @@ public PathManagerUI(final SNT plugin) {
452460 jmi .addActionListener (multiPathListener );
453461 advanced .add (jmi );
454462
455- // Search Bar TreeSearchable
456- searchableBar = new PathManagerUISearchableBar (this );
463+
457464 final JPopupMenu popup = new JPopupMenu ();
458465 popup .putClientProperty ("owner" , this ); // see SNTCommandFinder#revealMenuItem()
459466 popup .add (getDeleteMenuItem (multiPathListener ));
@@ -2573,17 +2580,28 @@ private class SinglePathActionListener implements ActionListener {
25732580 private static final String EXPLORE_FIT_CMD = "Explore/Preview Fit" ;
25742581 private static final String STRAIGHTEN = "Straighten..." ;
25752582 private static final String NODE_PROFILER = "Node Profiler..." ;
2583+ private final MultiPathActionListener multiPathActionListener ;
2584+
2585+ SinglePathActionListener (final MultiPathActionListener fallbackMultiPathActionListener ) {
2586+ this .multiPathActionListener = fallbackMultiPathActionListener ;
2587+ }
25762588
25772589 @ Override
25782590 public void actionPerformed (final ActionEvent e ) {
25792591
25802592 // Process nothing without a single path selection
2581- final Collection <Path > selectedPaths = getSelectedPaths (false );
2582- if (selectedPaths .size () != 1 ) {
2593+ final List <Path > selectedPaths = getSelectedPaths (false );
2594+ if (RENAME_CMD .equals (e .getActionCommand ()) && selectedPaths .size () > 1 ) {
2595+ if (guiUtils .getConfirmation ("This command accepts only one path. Run "
2596+ + MultiPathActionListener .RENAME_BY_PATTERN_CMD + " instead?" , "Single Path Command" ))
2597+ multiPathActionListener .commands .get (MultiPathActionListener .RENAME_BY_PATTERN_CMD )
2598+ .execute (selectedPaths , MultiPathActionListener .RENAME_BY_PATTERN_CMD );
2599+ return ;
2600+ } else if (selectedPaths .size () != 1 ) {
25832601 guiUtils .error ("This command accepts only a single path. Please re-run after having only one path selected." );
25842602 return ;
25852603 }
2586- final Path p = selectedPaths .iterator (). next ();
2604+ final Path p = selectedPaths .getFirst ();
25872605 switch (e .getActionCommand ()) {
25882606 case RENAME_CMD -> {
25892607 final String s = guiUtils .getString (
@@ -2904,6 +2922,7 @@ private class MultiPathActionListener implements ActionListener {
29042922 private static final String DOWNSAMPLE_CMD = "Downsample..." ;
29052923 private static final String Z_CORRECTION_CMD = "Correct Z-Shrinkage..." ;
29062924 private static final String CUSTOM_TAG_CMD = "Other..." ;
2925+ private static final String RENAME_BY_PATTERN_CMD = "Rename by Pattern..." ;
29072926 private static final String REPLACE_TAG_CMD = "Replace..." ;
29082927 private static final String LENGTH_TAG_CMD = "Length" ;
29092928 private static final String MEAN_RADIUS_TAG_CMD = "Mean Radius" ;
@@ -3013,6 +3032,7 @@ private void initializeCommands() {
30133032 commands .put (DETECT_LABEL_PROXIMITY_CMD , new DetectLabelProximityCommand ());
30143033
30153034 // Modification commands
3035+ commands .put (RENAME_BY_PATTERN_CMD , new RenameByPatternCommand ());
30163036 commands .put (REVERSE_CMD , new ReverseCommand ());
30173037 commands .put (CONCATENATE_CMD , new ConcatenateCommand ());
30183038 commands .put (COMBINE_CMD , new CombineCommand ());
@@ -3940,6 +3960,58 @@ public boolean canExecute(List<Path> selectedPaths) {
39403960 }
39413961 }
39423962
3963+ private class RenameByPatternCommand implements PathCommand {
3964+ @ Override
3965+ public void execute (List <Path > selectedPaths , String cmd ) {
3966+ final String [] labels = {"<HTML>Find" , "<HTML>Replace " };
3967+ if (getSearchable ().isCaseSensitive ()) labels [0 ] += " <i>[Cc]</i> " ;
3968+ if (getSearchable ().isWildcardEnabled ()) labels [0 ] += " <i>[.*]</i> " ;
3969+ labels [0 ] += " " ;
3970+ final String [] defaults = {PathManagerUI .this .getSearchable ().getSearchingText (), "" };
3971+ final String [] findReplace = guiUtils .getStrings (
3972+ String .format ("Renaming %d Path(s) by Pattern" , selectedPaths .size ()),
3973+ labels , defaults ,
3974+ "<HTML>Use <i>[Cc]</i> and <i>[.*]</i> in the search bar to refine matching" );
3975+ if (findReplace == null || findReplace [0 ] == null || findReplace [0 ].isEmpty ())
3976+ return ; // user pressed cancel or chose no inputs
3977+
3978+ if (findReplace [1 ] == null || findReplace [1 ].isEmpty ())
3979+ return ; // nothing to replace
3980+ if (getSearchable ().isWildcardEnabled ()) {
3981+ findReplace [0 ] = findReplace [0 ].replaceAll ("\\ ?" , ".?" );
3982+ findReplace [0 ] = findReplace [0 ].replaceAll ("\\ *" , ".*" );
3983+ }
3984+ if (!getSearchable ().isCaseSensitive ()) {
3985+ findReplace [0 ] = "(?i)" + findReplace [0 ];
3986+ }
3987+ int replacements = 0 ;
3988+ try {
3989+ final Pattern pattern = Pattern .compile (findReplace [0 ]);
3990+ for (final Path p : selectedPaths ) {
3991+ final Matcher matcher = pattern .matcher (p .getName ());
3992+ if (matcher .find ()) {
3993+ replacements ++;
3994+ p .setName (matcher .replaceAll (findReplace [1 ]));
3995+ }
3996+ }
3997+ if (replacements > 0 ) {
3998+ refreshManager (false , false , selectedPaths );
3999+ plugin .setUnsavedChanges (true );
4000+ guiUtils .floatingMsg (String .format ("%d/%d path(s) renamed." , replacements , selectedPaths .size ()), true );
4001+ } else {
4002+ guiUtils .error (String .format ("None of the %d paths matched the specified pattern." , selectedPaths .size ()));
4003+ }
4004+ } catch (final IllegalArgumentException ex ) {
4005+ guiUtils .error ("Replacement pattern invalid: " + ex .getMessage ());
4006+ }
4007+ }
4008+
4009+ @ Override
4010+ public boolean canExecute (List <Path > selectedPaths ) {
4011+ return !selectedPaths .isEmpty ();
4012+ }
4013+ }
4014+
39434015 private class SliceLabelTagCommand implements PathCommand {
39444016 @ Override
39454017 public void execute (List <Path > selectedPaths , String cmd ) {
@@ -5198,7 +5270,7 @@ private void removeTagsThroughPrompt(final List<Path> selectedPaths) {
51985270 private void replaceTags (final List <Path > selectedPaths ) {
51995271 final String [] labels = { "Find (case-sensitive) " , "Replace with " };
52005272 final String [] defaults = { GuiUtils .toString (extractTagsFromPaths (selectedPaths )), "" };
5201- final String [] findReplace = guiUtils .getStrings ("Replace Tag(s)..." , labels , defaults );
5273+ final String [] findReplace = guiUtils .getStrings ("Replace Tag(s)..." , labels , defaults , null );
52025274 if (findReplace == null || findReplace [0 ] == null || findReplace [0 ].isEmpty () || findReplace [1 ] == null )
52035275 return ; // nothing to replace
52045276 findReplace [1 ] = findReplace [1 ].replace ("[" , "(" ).replace ("]" , ")" )
0 commit comments