44import java .beans .PropertyChangeListener ;
55import java .io .File ;
66import java .util .ArrayList ;
7+ import java .util .Arrays ;
78import java .util .Collections ;
89import java .util .EnumSet ;
9- import java .util .Iterator ;
10+ import java .util .HashSet ;
1011import java .util .LinkedHashSet ;
11- import java .util .LinkedList ;
1212import java .util .List ;
13+ import java .util .Map ;
14+ import java .util .Objects ;
1315import java .util .Optional ;
1416import java .util .Set ;
17+ import java .util .stream .Collectors ;
1518
1619import org .bndtools .core .ui .resource .RequirementLabelProvider ;
1720import org .bndtools .utils .dnd .AbstractViewerDropAdapter ;
3437import org .eclipse .jface .window .Window ;
3538import org .eclipse .jface .wizard .WizardDialog ;
3639import org .eclipse .swt .SWT ;
40+ import org .eclipse .swt .custom .StyleRange ;
3741import org .eclipse .swt .events .KeyAdapter ;
3842import org .eclipse .swt .events .KeyEvent ;
3943import org .eclipse .swt .events .SelectionAdapter ;
@@ -76,52 +80,76 @@ public AbstractRequirementListPart(Composite parent, FormToolkit toolkit, int st
7680 super (parent , toolkit , style );
7781 }
7882
79- private final BndPreferences preferences = new BndPreferences ();
80- private final List <Requirement > requires = new ArrayList <>();
83+ private final BndPreferences preferences = new BndPreferences ();
84+ /** Local requirements: written to this file's own merge key. */
85+ private final List <Requirement > requires = new ArrayList <>();
86+ /** Inherited requirements: from included files, shown in gray (read-only). */
87+ private final List <Requirement > inheritedRequires = new ArrayList <>();
8188
8289 private TableViewer viewer ;
8390 private ToolItem addBundleTool ;
8491 private ToolItem removeTool ;
8592
86- private boolean committing = false ;
87- private boolean inherited = false ;
88- private String inheritedProvenance = null ;
93+ private boolean committing = false ;
94+ /** Per-requirement provenance: maps each inherited requirement to the file path that defines it. */
95+ private Map <Requirement , String > inheritedProvenances = new java .util .LinkedHashMap <>();
96+ /** The key used to write local requirements (plain stem or stem.local). */
97+ private String localKey = null ;
98+ /** Extra property key subscribed dynamically (the localKey when it's a suffix). */
99+ private String subscribedLocalKey = null ;
89100
90101 /** Returns the primary bnd property key this part displays (e.g. {@code -runrequires}). */
91102 protected abstract String getPrimaryPropertyKey ();
92103
93- /** A label provider that renders items in grey to indicate they are inherited. */
94- private static class InheritedRequirementLabelProvider extends RequirementLabelProvider {
104+ /** Returns the local write key (plain stem or stem.local) determined during the last refresh. */
105+ protected final String getLocalKey () {
106+ return localKey != null ? localKey : getPrimaryPropertyKey ();
107+ }
108+
109+ /** Colors inherited items gray and local items with the default foreground. */
110+ private class MixedRequirementLabelProvider extends RequirementLabelProvider {
95111 private final Color grey ;
96112
97- InheritedRequirementLabelProvider (Display display ) {
113+ MixedRequirementLabelProvider (Display display ) {
98114 grey = display .getSystemColor (SWT .COLOR_DARK_GRAY );
99115 }
100116
101117 @ Override
102118 public void update (ViewerCell cell ) {
103119 super .update (cell );
104- cell .setForeground (grey );
105- // clear styled ranges so the grey foreground is not overridden
106- cell .setStyleRanges (new org .eclipse .swt .custom .StyleRange [0 ]);
120+ if (inheritedRequires .contains (cell .getElement ())) {
121+ cell .setForeground (grey );
122+ // clear styled ranges so the grey foreground is not overridden
123+ cell .setStyleRanges (new StyleRange [0 ]);
124+ }
107125 }
108126 }
109127
110128 protected TableViewer createViewer (Composite parent , FormToolkit tk ) {
111129 Table table = tk .createTable (parent , SWT .FULL_SELECTION | SWT .MULTI | SWT .BORDER );
112130 viewer = new TableViewer (table );
113131 viewer .setContentProvider (ArrayContentProvider .getInstance ());
114- viewer .setLabelProvider (new RequirementLabelProvider ( ));
132+ viewer .setLabelProvider (new MixedRequirementLabelProvider ( table . getDisplay () ));
115133
116134 // Listeners
117- viewer .addSelectionChangedListener (event -> removeTool .setEnabled (!inherited && !viewer .getSelection ()
118- .isEmpty ()));
135+ viewer .addSelectionChangedListener (event -> {
136+ IStructuredSelection sel = (IStructuredSelection ) viewer .getSelection ();
137+ boolean hasLocalSelected = !sel .isEmpty ()
138+ && sel .toList ().stream ().anyMatch (e -> requires .contains (e ));
139+ removeTool .setEnabled (hasLocalSelected );
140+ });
119141 viewer .addDoubleClickListener (new IDoubleClickListener () {
120142 @ Override
121143 public void doubleClick (DoubleClickEvent event ) {
122- if (inherited && inheritedProvenance != null ) {
123- openProvenanceFile (inheritedProvenance );
124- }
144+ IStructuredSelection sel = (IStructuredSelection ) viewer .getSelection ();
145+ if (sel .isEmpty ())
146+ return ;
147+ Requirement req = (Requirement ) sel .getFirstElement ();
148+ if (!inheritedRequires .contains (req ))
149+ return ;
150+ String prov = inheritedProvenances .get (req );
151+ if (prov != null )
152+ openProvenanceFile (prov );
125153 }
126154 });
127155 table .addKeyListener (new KeyAdapter () {
@@ -237,14 +265,11 @@ private void doAddBundle() {
237265 private void doRemove () {
238266 IStructuredSelection selection = (IStructuredSelection ) viewer .getSelection ();
239267 if (!selection .isEmpty ()) {
240- Iterator <?> elements = selection .iterator ();
241- List <Object > removed = new LinkedList <>();
242- while (elements .hasNext ()) {
243- Object element = elements .next ();
244- if (this .requires .remove (element ))
245- removed .add (element );
246- }
247-
268+ // Only local items may be removed; inherited ones stay in their source file.
269+ @ SuppressWarnings ("unchecked" )
270+ List <Object > removed = ((List <Object >) selection .toList ()).stream ()
271+ .filter (e -> this .requires .remove (e ))
272+ .collect (Collectors .toList ());
248273 if (!removed .isEmpty ()) {
249274 viewer .remove (removed .toArray ());
250275 markDirty ();
@@ -264,44 +289,83 @@ public final void commitToModel(boolean onSave) {
264289
265290 @ Override
266291 protected final void refreshFromModel () {
267- List <Requirement > loadedReqs = doRefreshFromModel ();
268- if (loadedReqs == null )
269- loadedReqs = Collections .emptyList ();
292+ // Local requirements: only what is defined in this file's own merge keys.
293+ List <Requirement > newLocal = doRefreshFromModel ();
294+ if (newLocal == null )
295+ newLocal = Collections .emptyList ();
270296
271- // Determine whether the property is inherited from another file.
272- // Check stem and stem.* variant keys (e.g. -runrequires.shared).
297+ // Inherited requirements: merged view minus local.
273298 String primaryKey = getPrimaryPropertyKey ();
274- boolean nowInherited = primaryKey != null && model != null && !BndEditModelAccessor .hasLocalMergeProperty (model , primaryKey )
275- && !loadedReqs .isEmpty ();
276- Optional <String > provenance = (nowInherited && primaryKey != null && model != null )
277- ? BndEditModelAccessor .getPropertyProvenance (model , primaryKey )
278- : Optional .empty ();
279- inherited = nowInherited ;
280- inheritedProvenance = provenance .orElse (null );
281-
282- // Switch label provider based on inherited state; update tooltip
283- if (inherited ) {
284- viewer .setLabelProvider (new InheritedRequirementLabelProvider (viewer .getControl ()
285- .getDisplay ()));
286- String tip = inheritedProvenance != null
287- ? "Inherited from " + inheritedProvenance + ". Double-click to open source."
288- : "Inherited from an included file. Double-click to open source." ;
289- viewer .getControl ()
290- .setToolTipText (tip );
299+ List <Requirement > newInherited = Collections .emptyList ();
300+ if (primaryKey != null && model != null ) {
301+ List <Requirement > merged = BndEditModelAccessor .getMergedRequirements (model , primaryKey );
302+ if (merged != null && !merged .isEmpty ()) {
303+ Set <Requirement > localSet = new HashSet <>(newLocal );
304+ newInherited = merged .stream ()
305+ .filter (r -> !localSet .contains (r ))
306+ .collect (Collectors .toList ());
307+ }
308+ }
309+
310+ // Determine which key local additions should be written to.
311+ String newLocalKey = primaryKey ;
312+ if (primaryKey != null && model != null ) {
313+ String existingKey = BndEditModelAccessor .findLocalMergeKey (model , primaryKey );
314+ if (existingKey != null ) {
315+ newLocalKey = existingKey ;
316+ } else if (!newInherited .isEmpty ()) {
317+ // No local key yet but inherited items exist: use a suffix so bnd merges them.
318+ newLocalKey = primaryKey + ".local" ;
319+ }
320+ }
321+ localKey = newLocalKey ;
322+
323+ // Keep the property-change subscription aligned with the local key.
324+ if (!Objects .equals (subscribedLocalKey , newLocalKey )) {
325+ if (subscribedLocalKey != null && model != null )
326+ model .removePropertyChangeListener (subscribedLocalKey , this );
327+ subscribedLocalKey = newLocalKey ;
328+ if (newLocalKey != null && model != null
329+ && !Arrays .asList (getProperties ()).contains (newLocalKey ))
330+ model .addPropertyChangeListener (newLocalKey , this );
331+ }
332+
333+ // Update provenance tooltip for inherited items.
334+ if (!newInherited .isEmpty ()) {
335+ inheritedProvenances = BndEditModelAccessor .getInheritedRequirementProvenances (model , primaryKey );
336+ String tip = inheritedProvenances .values ().stream ().findAny ().isPresent ()
337+ ? "Some requirements are inherited from included files. Double-click an inherited item to open its source."
338+ : "Some requirements are inherited from included files." ;
339+ viewer .getControl ().setToolTipText (tip );
291340 } else {
292- viewer .setLabelProvider (new RequirementLabelProvider ());
293- viewer .getControl ()
294- .setToolTipText (null );
341+ inheritedProvenances = Collections .emptyMap ();
342+ viewer .getControl ().setToolTipText (null );
295343 }
296- addBundleTool .setEnabled (!inherited );
344+
345+ addBundleTool .setEnabled (true );
297346 removeTool .setEnabled (false );
298347
299- if (loadedReqs .equals (this .requires ))
348+ if (newInherited . equals ( this . inheritedRequires ) && newLocal .equals (this .requires ))
300349 return ;
301350
351+ this .inheritedRequires .clear ();
352+ this .inheritedRequires .addAll (newInherited );
302353 this .requires .clear ();
303- this .requires .addAll (loadedReqs );
304- viewer .setInput (this .requires );
354+ this .requires .addAll (newLocal );
355+
356+ List <Requirement > combined = new ArrayList <>(this .inheritedRequires .size () + this .requires .size ());
357+ combined .addAll (this .inheritedRequires );
358+ combined .addAll (this .requires );
359+ viewer .setInput (combined );
360+ }
361+
362+ @ Override
363+ public void dispose () {
364+ if (subscribedLocalKey != null && model != null ) {
365+ model .removePropertyChangeListener (subscribedLocalKey , this );
366+ subscribedLocalKey = null ;
367+ }
368+ super .dispose ();
305369 }
306370
307371 @ Override
@@ -336,13 +400,12 @@ private void openProvenanceFile(String absolutePath) {
336400 }
337401 }
338402
339- /** Update the requirements already available with new ones. Already existing requirements will be removed . */
403+ /** Adds new requirements as local items. Items already in inherited or local lists are skipped . */
340404 private boolean updateViewerWithNewRequirements (Set <Requirement > adding ) {
341- // remove duplicates
405+ adding . removeAll ( this . inheritedRequires );
342406 adding .removeAll (this .requires );
343- if (adding .isEmpty ()) {
407+ if (adding .isEmpty ())
344408 return false ;
345- }
346409 this .requires .addAll (adding );
347410 viewer .add (adding .toArray ());
348411 markDirty ();
0 commit comments