Repository: GrandOrgue/grandorgue
Pull request #2123: Added capability of customising minimum amplitude level of enclosures added internally by GrandOrgue https://github.com/GrandOrgue/grandorgue/issues/782
Author: login: oleg68
Created: 2025-02-17T16:51:21Z | Merged: 2025-03-19T18:33:47Z

#Resolves: #782

This PR adds an Enclosures tab to the Organ Settings dialog.

It shows the Minimal amplitude level of all enclosures and allows the user to change them for all internal enclosures.



Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bdb7fa8bf..1c8ebf0fa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,4 @@
+- Added capability of customising minimum amplitude level of enclosures added internally by GrandOrgue https://github.com/GrandOrgue/grandorgue/issues/782
 - Added resending midi events when exiting from MidiEventDialog https://github.com/GrandOrgue/grandorgue/issues/2062
 - Fixed incorrect default audio channels config https://github.com/GrandOrgue/grandorgue/issues/2115
 - Changed default sample rate, samples per buffer and interpolation type to 48000, 512 and Polyphase https://github.com/GrandOrgue/grandorgue/issues/2115
diff --git a/help/grandorgue.xml b/help/grandorgue.xml
index 255ed72ec..48d5118da 100644
--- a/help/grandorgue.xml
+++ b/help/grandorgue.xml
@@ -5652,6 +5652,49 @@ role="bold">Group</emphasis> attribute are grouped in the same submenu.
 	</sect3>
       </sect2>
     </sect1>
+    <sect1>
+      <title>Enclosures</title>
+      <indexterm><primary>Organ settings.Enclosures</primary></indexterm>
+      <figure>
+	<title>Organ Settings dialog, Enclosures tab</title>
+	<mediaobject>
+	  <imageobject>
+	    <imagedata scalefit="1" width="100%" fileref="images/organ_settings_enclosures.png"/>
+	  </imageobject>
+	</mediaobject>
+      </figure>
+      <para>
+	In its left box, this dialog displays all enclosures: both added 
+	internally by GrandOrgue and the ones found in the definition file. Only
+	the enclosures added internally are allowed to be changed.
+      </para>
+      <simpara>
+	Select the element you wish to customize. The right controls are
+	populated with values picked from the organ definition file or the
+	current preset number if the latter was saved.
+      </simpara>
+      <simpara>
+	It is possible to select multiple elements at once, to e.g. update one
+	setting of multiple elements to the same value in a single operation.
+      </simpara>
+      <sect2>
+	<title>Affected windchests</title>
+	<para>
+	  The list of windchest the enclosure affects.
+	</para>
+      </sect2>
+      <sect2>
+	<title>Minimum amplitude level</title>
+	<para>
+	  Linear sound amplitude scale factor when the enclosure is closed. The
+	  allowed range is 0, 100.
+	</para>
+	<para>
+	  0 means complete silence. 100 means the enclosure does not affect the
+	  windchest volume at all.
+	</para>
+      </sect2>
+    </sect1>
     <sect1>
       <title>Buttons row</title>
       <para>These buttons are used to drive the dialog and always act on the selected element.</para>
diff --git a/help/images/organ_settings_enclosures.png b/help/images/organ_settings_enclosures.png
new file mode 100644
index 000000000..af1d6d6f3
Binary files /dev/null and b/help/images/organ_settings_enclosures.png differ
diff --git a/src/grandorgue/CMakeLists.txt b/src/grandorgue/CMakeLists.txt
index 1129dbfcd..ac2e19d95 100644
--- a/src/grandorgue/CMakeLists.txt
+++ b/src/grandorgue/CMakeLists.txt
@@ -74,8 +74,9 @@ gui/dialogs/midi-event/GOMidiEventDialog.cpp
 gui/dialogs/midi-event/GOMidiEventKeyTab.cpp
 gui/dialogs/midi-event/GOMidiEventRecvTab.cpp
 gui/dialogs/midi-event/GOMidiEventSendTab.cpp
-gui/dialogs/organ-settings/GOOrganSettingsButtonsProxy.cpp
+gui/dialogs/organ-settings/GOOrganSettingsTab.cpp
 gui/dialogs/organ-settings/GOOrganSettingsDialog.cpp
+gui/dialogs/organ-settings/GOOrganSettingsEnclosuresTab.cpp
 gui/dialogs/organ-settings/GOOrganSettingsPipesTab.cpp
 gui/dialogs/settings/GOSettingsAudio.cpp
 gui/dialogs/settings/GOSettingsDeviceMatchDialog.cpp
diff --git a/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsButtonsProxy.cpp b/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsButtonsProxy.cpp
deleted file mode 100644
index ba9b2e696..000000000
--- a/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsButtonsProxy.cpp
+++ /dev/null
@@ -1,13 +0,0 @@
-/*
- * Copyright 2006 Milan Digital Audio LLC
- * Copyright 2009-2025 GrandOrgue contributors (see AUTHORS)
- * License GPL-2.0 or later
- * (https://www.gnu.org/licenses/old-licenses/gpl-2.0.html).
- */
-
-#include "GOOrganSettingsButtonsProxy.h"
-
-void GOOrganSettingsButtonsProxy::NotifyModified(bool newValue) {
-  SetModified(newValue);
-  NotifyButtonStatesChanged();
-}
\ No newline at end of file
diff --git a/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsButtonsProxy.h b/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsButtonsProxy.h
deleted file mode 100644
index eb07fc2ca..000000000
--- a/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsButtonsProxy.h
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright 2006 Milan Digital Audio LLC
- * Copyright 2009-2025 GrandOrgue contributors (see AUTHORS)
- * License GPL-2.0 or later
- * (https://www.gnu.org/licenses/old-licenses/gpl-2.0.html).
- */
-
-#ifndef GOORGANSETTINGSBUTTONSPROXY_H
-#define GOORGANSETTINGSBUTTONSPROXY_H
-
-class GOOrganSettingsButtonsProxy {
-public:
-  class Listener {
-  public:
-    virtual void ButtonStatesChanged() = 0;
-  };
-
-private:
-  Listener &r_listener;
-
-protected:
-  bool m_IsDistributeAudioEnabled = true;
-  bool m_IsDefaultEnabled = true;
-
-private:
-  bool m_IsModified = false;
-
-public:
-  GOOrganSettingsButtonsProxy(Listener &listener) : r_listener(listener) {}
-
-protected:
-  void NotifyButtonStatesChanged() { r_listener.ButtonStatesChanged(); }
-  bool IsModified() const { return m_IsModified; }
-  void SetModified(bool newValue) { m_IsModified = newValue; }
-  void NotifyModified(bool newValue = true);
-
-public:
-  bool IsDistributeAudioEnabled() const { return m_IsDistributeAudioEnabled; }
-  bool IsDefaultEnabled() const { return m_IsDefaultEnabled; }
-  bool IsRevertEnabled() const { return IsModified(); }
-  bool IsApplyEnabled() const { return IsModified(); }
-};
-
-#endif /* GOORGANSETTINGSBUTTONSPROXY_H */
diff --git a/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsDialog.cpp b/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsDialog.cpp
index 0d21eb7f1..445323582 100644
--- a/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsDialog.cpp
+++ b/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsDialog.cpp
@@ -13,6 +13,7 @@
 
 #include "model/GOOrganModel.h"
 
+#include "GOOrganSettingsEnclosuresTab.h"
 #include "GOOrganSettingsPipesTab.h"
 
 enum {
@@ -23,17 +24,19 @@ enum {
   ID_BUTTON_APPLY,
 };
 
-BEGIN_EVENT_TABLE(GOOrganSettingsDialog, GOTabbedDialog)
+BEGIN_EVENT_TABLE(GOOrganSettingsDialog, GOOrganSettingsDialogBase)
 EVT_BUTTON(
   ID_BUTTON_DISTRIBUTE_AUDIO, GOOrganSettingsDialog::OnButtonDistributeAudio)
 EVT_BUTTON(ID_BUTTON_DEFAULT, GOOrganSettingsDialog::OnButtonDefault)
 EVT_BUTTON(ID_BUTTON_DISCARD, GOOrganSettingsDialog::OnButtonDiscard)
 EVT_BUTTON(ID_BUTTON_APPLY, GOOrganSettingsDialog::OnButtonApply)
+EVT_BOOKCTRL_PAGE_CHANGING(wxID_ANY, GOOrganSettingsDialog::OnTabSelecting)
+EVT_BOOKCTRL_PAGE_CHANGED(wxID_ANY, GOOrganSettingsDialog::OnTabSelected)
 END_EVENT_TABLE()
 
 GOOrganSettingsDialog::GOOrganSettingsDialog(
   GOOrganModel &organModel, GODocumentBase *doc, wxWindow *parent)
-  : GOTabbedDialog(
+  : GOOrganSettingsDialogBase(
     parent,
     wxT("Organ settings"),
     _("Organ settings"),
@@ -42,11 +45,6 @@ GOOrganSettingsDialog::GOOrganSettingsDialog(
     0,
     wxHELP | wxCLOSE),
     GOView(doc, this) {
-  wxBookCtrlBase *pNoteBook = GetBookCtrl();
-
-  m_PipesTab = new GOOrganSettingsPipesTab(organModel, pNoteBook, *this);
-  AddTab(m_PipesTab, "Pipes", _("Pipes"));
-
   // add a custom button 'Reason into the space of the standard dialog button
   wxSizer *const pButtonSizer = GetButtonSizer();
 
@@ -73,28 +71,37 @@ GOOrganSettingsDialog::GOOrganSettingsDialog(
     pButtonSizer->InsertSpacer(9, 10);
   }
 
+  // Don't move above because adding tabs raises events that require buttons
+  m_PipesTab = new GOOrganSettingsPipesTab(organModel, this);
+  AddTab(m_PipesTab);
+
+  m_EnclosuresTab = new GOOrganSettingsEnclosuresTab(organModel, this);
+  AddTab(m_EnclosuresTab);
+
   LayoutDialog();
 }
 
 void GOOrganSettingsDialog::ButtonStatesChanged() {
-  m_AudioGroupAssistant->Enable(m_PipesTab->IsDistributeAudioEnabled());
-  m_Default->Enable(m_PipesTab->IsDefaultEnabled());
-  m_Discard->Enable(m_PipesTab->IsRevertEnabled());
-  m_Apply->Enable(m_PipesTab->IsApplyEnabled());
-}
+  auto pTab = dynamic_cast<GOOrganSettingsTab *>(GetBook()->GetCurrentPage());
 
-void GOOrganSettingsDialog::OnButtonDistributeAudio(wxCommandEvent &e) {
-  m_PipesTab->DistributeAudio();
-}
+  if (pTab) {
+    bool isDistributeEnabled = pTab->IsDistributeAudioEnabled();
 
-void GOOrganSettingsDialog::OnButtonDefault(wxCommandEvent &e) {
-  m_PipesTab->ResetToDefault();
+    m_AudioGroupAssistant->Enable(isDistributeEnabled);
+    m_Default->Enable(pTab->IsDefaultEnabled());
+    m_Discard->Enable(pTab->IsRevertEnabled());
+    m_Apply->Enable(pTab->IsApplyEnabled());
+  }
 }
 
-void GOOrganSettingsDialog::OnButtonDiscard(wxCommandEvent &e) {
-  m_PipesTab->DiscardChanges();
+template <typename T>
+T GOOrganSettingsDialog::CallTabFunc(T (GOOrganSettingsTab::*pFunc)()) {
+  auto pTab = dynamic_cast<GOOrganSettingsTab *>(GetBook()->GetCurrentPage());
+
+  return pTab ? (pTab->*pFunc)() : T();
 }
 
-void GOOrganSettingsDialog::OnButtonApply(wxCommandEvent &e) {
-  m_PipesTab->ApplyChanges();
+void GOOrganSettingsDialog::OnTabSelecting(wxBookCtrlEvent &e) {
+  if (CallTabFunc(&GOOrganSettingsTab::CheckForUnapplied))
+    e.Veto();
 }
diff --git a/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsDialog.h b/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsDialog.h
index af630a3da..35476e6fc 100644
--- a/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsDialog.h
+++ b/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsDialog.h
@@ -11,26 +11,27 @@
 #include <wx/event.h>
 
 #include "document-base/GOView.h"
-#include "gui/dialogs/common/GOTabbedDialog.h"
 
-#include "GOOrganSettingsButtonsProxy.h"
+#include "GOOrganSettingsDialogBase.h"
+#include "GOOrganSettingsTab.h"
 
+class wxBookCtrlEvent;
 class wxButton;
 
 class GOOrganModel;
+class GOOrganSettingsEnclosuresTab;
 class GOOrganSettingsPipesTab;
 
-class GOOrganSettingsDialog : public GOTabbedDialog,
-                              public GOView,
-                              private GOOrganSettingsButtonsProxy::Listener {
+class GOOrganSettingsDialog : public GOOrganSettingsDialogBase, public GOView {
 private:
-  GOOrganSettingsPipesTab *m_PipesTab;
-
   wxButton *m_AudioGroupAssistant;
   wxButton *m_Default;
   wxButton *m_Discard;
   wxButton *m_Apply;
 
+  GOOrganSettingsPipesTab *m_PipesTab;
+  GOOrganSettingsEnclosuresTab *m_EnclosuresTab;
+
 public:
   GOOrganSettingsDialog(
     GOOrganModel &organModel, GODocumentBase *doc, wxWindow *parent);
@@ -38,10 +39,26 @@ class GOOrganSettingsDialog : public GOTabbedDialog,
 private:
   void ButtonStatesChanged() override;
 
-  void OnButtonDistributeAudio(wxCommandEvent &e);
-  void OnButtonDefault(wxCommandEvent &e);
-  void OnButtonDiscard(wxCommandEvent &e);
-  void OnButtonApply(wxCommandEvent &e);
+  void OnTabSelecting(wxBookCtrlEvent &e);
+  void OnTabSelected(wxBookCtrlEvent &e) { ButtonStatesChanged(); }
+
+  template <typename T> T CallTabFunc(T (GOOrganSettingsTab::*pFunc)());
+
+  void OnButtonDistributeAudio(wxCommandEvent &e) {
+    CallTabFunc(&GOOrganSettingsTab::DistributeAudio);
+  }
+
+  void OnButtonDefault(wxCommandEvent &e) {
+    CallTabFunc(&GOOrganSettingsTab::ResetToDefault);
+  }
+
+  void OnButtonDiscard(wxCommandEvent &e) {
+    CallTabFunc(&GOOrganSettingsTab::DiscardChanges);
+  }
+
+  void OnButtonApply(wxCommandEvent &e) {
+    CallTabFunc(&GOOrganSettingsTab::ApplyChanges);
+  }
 
   DECLARE_EVENT_TABLE()
 };
diff --git a/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsDialogBase.h b/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsDialogBase.h
new file mode 100644
index 000000000..82dd6c009
--- /dev/null
+++ b/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsDialogBase.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2006 Milan Digital Audio LLC
+ * Copyright 2009-2025 GrandOrgue contributors (see AUTHORS)
+ * License GPL-2.0 or later
+ * (https://www.gnu.org/licenses/old-licenses/gpl-2.0.html).
+ */
+
+#ifndef GOORGANSETTINGSDIALOGBASE_H
+#define GOORGANSETTINGSDIALOGBASE_H
+
+#include "gui/dialogs/common/GOTabbedDialog.h"
+
+class GOOrganSettingsDialogBase : public GOTabbedDialog {
+protected:
+  GOOrganSettingsDialogBase(
+    wxWindow *win,
+    const wxString &name,  // not translated
+    const wxString &title, // translated
+    GODialogSizeSet &dialogSizes,
+    const wxString dialogSelector,
+    long addStyle = 0,
+    long buttonFlags = DEFAULT_BUTTON_FLAGS)
+    : GOTabbedDialog(
+      win, name, title, dialogSizes, dialogSelector, addStyle, buttonFlags) {}
+
+public:
+  virtual void ButtonStatesChanged() = 0;
+};
+
+#endif /* GOORGANSETTINGSDIALOGBASE_H */
diff --git a/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsEnclosuresTab.cpp b/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsEnclosuresTab.cpp
new file mode 100644
index 000000000..ee4a8300f
--- /dev/null
+++ b/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsEnclosuresTab.cpp
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2006 Milan Digital Audio LLC
+ * Copyright 2009-2025 GrandOrgue contributors (see AUTHORS)
+ * License GPL-2.0 or later
+ * (https://www.gnu.org/licenses/old-licenses/gpl-2.0.html).
+ */
+
+#include "GOOrganSettingsEnclosuresTab.h"
+
+#include <wx/checkbox.h>
+#include <wx/gbsizer.h>
+#include <wx/listbox.h>
+#include <wx/stattext.h>
+#include <wx/treectrl.h>
+
+#include "model/GOEnclosure.h"
+#include "model/GOOrganModel.h"
+#include "model/GOWindchest.h"
+
+#include "GOEvent.h"
+
+static const wxSize EDIT_SIZE = wxSize(60, -1);
+static const wxString WX_TAB_CODE = wxT("Enclosures");
+static const wxString WX_TAB_TITLE = _("Enclosures");
+
+enum {
+  ID_EVENT_TREE = 200,
+  ID_EVENT_MIN_AMP_LEVEL,
+};
+
+BEGIN_EVENT_TABLE(GOOrganSettingsEnclosuresTab, wxPanel)
+EVT_TREE_SEL_CHANGING(
+  ID_EVENT_TREE, GOOrganSettingsEnclosuresTab::OnTreeChanging)
+EVT_TREE_SEL_CHANGED(ID_EVENT_TREE, GOOrganSettingsEnclosuresTab::OnTreeChanged)
+EVT_TEXT(
+  ID_EVENT_MIN_AMP_LEVEL, GOOrganSettingsEnclosuresTab::OnMinAmpLevelChanged)
+END_EVENT_TABLE()
+
+class GOOrganSettingsEnclosuresTab::ItemData : public wxTreeItemData {
+public:
+  enum ItemType {
+    ORGAN,
+    WINDCHEST,
+    ENCLOSURE,
+  } m_type;
+
+  union {
+    GOOrganModel *p_organ;
+    GOWindchest *p_windchest;
+    GOEnclosure *p_enclosure;
+  };
+
+  ItemData(GOOrganModel &organModel) : m_type(ORGAN), p_organ(&organModel) {}
+  ItemData(GOWindchest &windchest)
+    : m_type(WINDCHEST), p_windchest(&windchest) {}
+  ItemData(GOEnclosure &enclosure)
+    : m_type(ENCLOSURE), p_enclosure(&enclosure) {}
+};
+
+GOOrganSettingsEnclosuresTab::GOOrganSettingsEnclosuresTab(
+  GOOrganModel &organModel, GOOrganSettingsDialogBase *pDlg)
+  : GOOrganSettingsTab(pDlg, WX_TAB_CODE, WX_TAB_TITLE),
+    r_OrganModel(organModel) {
+  wxGridBagSizer *const mainSizer = new wxGridBagSizer(5, 5);
+
+  // wxListBox would be better but it does not allow to veto selections
+  // so we use wxTreeCtrl instead of wxListBox
+  m_tree = new wxTreeCtrl(
+    this,
+    ID_EVENT_TREE,
+    wxDefaultPosition,
+    wxDefaultSize,
+    wxTR_MULTIPLE | wxTR_HIDE_ROOT);
+  mainSizer->Add(
+    m_tree, wxGBPosition(0, 0), wxGBSpan(4, 1), wxEXPAND | wxALL, 5);
+
+  m_IsOdfDefined = new wxCheckBox(
+    this, wxID_ANY, _("This enclosure is ODF defined and may not be altered"));
+  m_IsOdfDefined->Disable();
+
+  mainSizer->Add(
+    m_IsOdfDefined,
+    wxGBPosition(0, 1),
+    wxGBSpan(1, 3),
+    wxALIGN_LEFT | wxEXPAND | wxALL,
+    5);
+  mainSizer->Add(
+    new wxStaticText(this, wxID_ANY, _("Affected windchests:")),
+    wxGBPosition(1, 1),
+    wxDefaultSpan,
+    wxALIGN_RIGHT | wxTOP | wxLEFT | wxBOTTOM,
+    5);
+  m_WindchestList = new wxListBox(
+    this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0, nullptr, wxLB_SINGLE);
+  m_WindchestList->Disable();
+  mainSizer->Add(
+    m_WindchestList,
+    wxGBPosition(1, 2),
+    wxGBSpan(2, 2),
+    wxEXPAND | wxTOP | wxRIGHT | wxBOTTOM,
+    5);
+  mainSizer->Add(
+    new wxStaticText(this, wxID_ANY, _("Minimum amplitude level:")),
+    wxGBPosition(3, 1),
+    wxDefaultSpan,
+    wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL | wxLEFT | wxBOTTOM,
+    5);
+  m_MinAmpLevelEdit = new wxTextCtrl(
+    this, ID_EVENT_MIN_AMP_LEVEL, wxEmptyString, wxDefaultPosition, EDIT_SIZE);
+  mainSizer->Add(
+    m_MinAmpLevelEdit,
+    wxGBPosition(3, 2),
+    wxDefaultSpan,
+    wxEXPAND | wxRIGHT | wxBOTTOM,
+    5);
+
+  mainSizer->AddGrowableCol(0, 1);
+  mainSizer->AddGrowableCol(3, 1);
+  mainSizer->AddGrowableRow(2, 1);
+  SetSizerAndFit(mainSizer);
+}
+
+bool GOOrganSettingsEnclosuresTab::TransferDataToWindow() {
+  auto rootItem = m_tree->AddRoot(
+    r_OrganModel.GetRootPipeConfigNode().GetName(),
+    -1,
+    -1,
+    new ItemData(r_OrganModel));
+
+  // fill m_WindchestsByEnclosures
+  for (GOWindchest *pW : r_OrganModel.GetWindchests())
+    for (GOEnclosure *pE : pW->GetEnclosures())
+      m_WindchestsByEnclosures[pE].push_back(pW->GetName());
+
+  // list internal enclosures
+  for (GOEnclosure *pE : r_OrganModel.GetEnclosures())
+    if (!pE->IsOdfDefined())
+      m_tree->AppendItem(rootItem, pE->GetName(), -1, -1, new ItemData(*pE));
+
+  // list odf-defined enclosures
+  for (GOEnclosure *pE : r_OrganModel.GetEnclosures())
+    if (pE->IsOdfDefined())
+      m_tree->AppendItem(rootItem, pE->GetName(), -1, -1, new ItemData(*pE));
+  return true;
+}
+
+void GOOrganSettingsEnclosuresTab::OnTreeChanging(wxTreeEvent &e) {
+  if (CheckForUnapplied())
+    e.Veto();
+}
+
+void GOOrganSettingsEnclosuresTab::LoadValues() {
+  wxArrayTreeItemIds entries;
+  GOEnclosure *pSelectedEnclosure = nullptr;
+
+  m_tree->GetSelections(entries);
+
+  const unsigned nSelected = entries.size();
+
+  // set pSelectedEnclosure if only one enclosure is selected
+  if (nSelected == 1) {
+    ItemData *pData = (ItemData *)m_tree->GetItemData(entries[0]);
+
+    if (pData->m_type == ItemData::ENCLOSURE)
+      pSelectedEnclosure = pData->p_enclosure;
+  }
+
+  // display data of the selected enclosure
+  m_WindchestList->Clear();
+  if (pSelectedEnclosure) {
+    if (auto iWindchests = m_WindchestsByEnclosures.find(pSelectedEnclosure);
+        iWindchests != m_WindchestsByEnclosures.end())
+      m_WindchestList->Append(iWindchests->second);
+    m_MinAmpLevelEdit->ChangeValue(
+      wxString::Format("%u", pSelectedEnclosure->GetAmpMinimumLevel()));
+    m_MinAmpLevelEdit->DiscardEdits();
+  } else {
+    m_MinAmpLevelEdit->Clear();
+  }
+
+  // check that all selected items are internal enclosures
+  bool areInternalEnclosuresSelected = false;
+  bool areOnlyEnclosuresSelected = false;
+
+  for (auto id : entries) {
+    ItemData *pData = (ItemData *)m_tree->GetItemData(id);
+    bool isEnclosure = pData->m_type == ItemData::ENCLOSURE;
+    bool isOdfDefined = isEnclosure && pData->p_enclosure->IsOdfDefined();
+
+    if (isEnclosure) {
+      areOnlyEnclosuresSelected = true;
+      if (!isOdfDefined)
+        areInternalEnclosuresSelected = true;
+    }
+
+    if (!isEnclosure || isOdfDefined) {
+      // now we do not allow to change min value of odf-defined enclosures
+      areInternalEnclosuresSelected = false;
+      if (!isEnclosure)
+        areOnlyEnclosuresSelected = false;
+      break;
+    }
+  }
+  m_IsOdfDefined->SetValue(
+    areOnlyEnclosuresSelected && !areInternalEnclosuresSelected);
+  m_MinAmpLevelEdit->Enable(areInternalEnclosuresSelected);
+  m_IsDefaultEnabled = areInternalEnclosuresSelected;
+  NotifyModified(false);
+}
+
+void GOOrganSettingsEnclosuresTab::DoForAllEnclosures(
+  const std::function<void(GOEnclosure &enclosure)> &f) {
+  wxArrayTreeItemIds entries;
+
+  m_tree->GetSelections(entries);
+  for (auto id : entries) {
+    ItemData *pData = (ItemData *)m_tree->GetItemData(id);
+
+    if (pData->m_type == ItemData::ENCLOSURE)
+      f(*(pData->p_enclosure));
+  }
+}
+
+void GOOrganSettingsEnclosuresTab::ResetToDefault() {
+  DoForAllEnclosures([](GOEnclosure &enclosure) {
+    enclosure.SetAmpMinimumLevel(enclosure.GetDefaultAmpMinimumLevel());
+  });
+  LoadValues();
+}
+
+void GOOrganSettingsEnclosuresTab::ApplyChanges() {
+  if (m_MinAmpLevelEdit->IsModified()) {
+    long minAmpVal;
+
+    if (
+      m_MinAmpLevelEdit->GetValue().ToLong(&minAmpVal) && minAmpVal >= 0
+      && minAmpVal <= 100)
+      DoForAllEnclosures([minAmpVal](GOEnclosure &enclosure) {
+        enclosure.SetAmpMinimumLevel(minAmpVal);
+      });
+    else
+      GOMessageBox(
+        _("Minimal amplitude level is invalid"),
+        _("Error"),
+        wxOK | wxICON_ERROR,
+        this);
+    NotifyModified(false);
+  }
+}
diff --git a/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsEnclosuresTab.h b/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsEnclosuresTab.h
new file mode 100644
index 000000000..b757c2b64
--- /dev/null
+++ b/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsEnclosuresTab.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2006 Milan Digital Audio LLC
+ * Copyright 2009-2025 GrandOrgue contributors (see AUTHORS)
+ * License GPL-2.0 or later
+ * (https://www.gnu.org/licenses/old-licenses/gpl-2.0.html).
+ */
+
+#ifndef GOORGANSETTINGSENCLOSURESTAB_H
+#define GOORGANSETTINGSENCLOSURESTAB_H
+
+#include <functional>
+#include <unordered_map>
+#include <vector>
+
+#include <wx/event.h>
+#include <wx/panel.h>
+
+#include "GOOrganSettingsTab.h"
+
+class wxCheckBox;
+class wxListBox;
+class wxTextCtrl;
+class wxTreeCtrl;
+class wxTreeEvent;
+
+class GOEnclosure;
+class GOOrganModel;
+class GOWindchest;
+
+class GOOrganSettingsEnclosuresTab : public GOOrganSettingsTab {
+private:
+  class ItemData;
+
+  GOOrganModel &r_OrganModel;
+
+  wxTreeCtrl *m_tree;
+  wxCheckBox *m_IsOdfDefined;
+  wxListBox *m_WindchestList;
+  wxTextCtrl *m_MinAmpLevelEdit;
+
+  std::unordered_map<GOEnclosure *, wxArrayString> m_WindchestsByEnclosures;
+
+public:
+  GOOrganSettingsEnclosuresTab(
+    GOOrganModel &organModel, GOOrganSettingsDialogBase *pDlg);
+
+private:
+  void LoadValues();
+  bool TransferDataToWindow() override;
+  void OnTreeChanging(wxTreeEvent &e);
+  void OnTreeChanged(wxTreeEvent &e) { LoadValues(); }
+  void OnMinAmpLevelChanged(wxCommandEvent &e) { NotifyModified(); }
+  void DoForAllEnclosures(const std::function<void(GOEnclosure &enclosure)> &f);
+
+public:
+  void ResetToDefault() override;
+  void DiscardChanges() override { LoadValues(); }
+  void ApplyChanges() override;
+
+  DECLARE_EVENT_TABLE()
+};
+
+#endif /* GOORGANSETTINGSENCLOSURESTAB_H */
diff --git a/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsPipesTab.cpp b/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsPipesTab.cpp
index d254c7cc6..d34ef0ba1 100644
--- a/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsPipesTab.cpp
+++ b/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsPipesTab.cpp
@@ -30,6 +30,8 @@ static constexpr unsigned RELEASE_LENGTH_STEP = 50;
 static constexpr unsigned RELEASE_LENGTH_MAX_INDEX
   = RELEASE_LENGTH_MAX / RELEASE_LENGTH_STEP;
 static const wxSize EDIT_SIZE = wxSize(60, -1);
+static const wxString WX_TAB_CODE = wxT("Pipes");
+static const wxString WX_TAB_TITLE = _("Pipes");
 
 class GOOrganSettingsPipesTab::TreeItemData : public wxTreeItemData {
 public:
@@ -111,11 +113,8 @@ EVT_CHOICE(ID_EVENT_RELEASE_LOAD, GOOrganSettingsPipesTab::OnReleaseLoadChanged)
 END_EVENT_TABLE()
 
 GOOrganSettingsPipesTab::GOOrganSettingsPipesTab(
-  GOOrganModel &organModel,
-  wxWindow *parent,
-  GOOrganSettingsButtonsProxy::Listener &listener)
-  : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS),
-    GOOrganSettingsButtonsProxy(listener),
+  GOOrganModel &organModel, GOOrganSettingsDialogBase *pDlg)
+  : GOOrganSettingsTab(pDlg, WX_TAB_CODE, WX_TAB_TITLE),
     r_config(organModel.GetConfig()),
     r_RootNode(organModel.GetRootPipeConfigNode()),
     p_LastTreeItemData(nullptr),
@@ -423,18 +422,6 @@ bool GOOrganSettingsPipesTab::TransferDataToWindow() {
   return true;
 }
 
-bool GOOrganSettingsPipesTab::CheckForUnapplied() {
-  bool res = IsModified();
-
-  if (res)
-    GOMessageBox(
-      _("Please apply or discard changes first"),
-      _("Error"),
-      wxOK | wxICON_ERROR,
-      this);
-  return res;
-}
-
 void GOOrganSettingsPipesTab::SetEmpty(wxChoice *choice) {
   int index = choice->FindString(wxEmptyString);
   if (index == wxNOT_FOUND)
diff --git a/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsPipesTab.h b/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsPipesTab.h
index 39a0210e8..54f01ce72 100644
--- a/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsPipesTab.h
+++ b/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsPipesTab.h
@@ -13,7 +13,7 @@
 #include <wx/event.h>
 #include <wx/panel.h>
 
-#include "GOOrganSettingsButtonsProxy.h"
+#include "GOOrganSettingsTab.h"
 
 class wxCheckBox;
 class wxChoice;
@@ -29,8 +29,7 @@ class GOConfig;
 class GOOrganModel;
 class GOPipeConfigNode;
 
-class GOOrganSettingsPipesTab : public wxPanel,
-                                public GOOrganSettingsButtonsProxy {
+class GOOrganSettingsPipesTab : public GOOrganSettingsTab {
 private:
   class TreeItemData;
 
@@ -77,13 +76,6 @@ class GOOrganSettingsPipesTab : public wxPanel,
   wxTreeItemId FillTree(wxTreeItemId parent, GOPipeConfigNode &config);
   bool TransferDataToWindow() override;
 
-  /**
-   * Checks if all changes have been applied. If some unapplied changes are
-   * present, then display an error message.
-   * Returns if there are unapplied changes
-   */
-  bool CheckForUnapplied();
-
   void SetEmpty(wxChoice *choice);
   void RemoveEmpty(wxChoice *choice);
 
@@ -117,18 +109,14 @@ class GOOrganSettingsPipesTab : public wxPanel,
   void OnAttackLoadChanged(wxCommandEvent &e);
   void OnReleaseLoadChanged(wxCommandEvent &e);
 
-  bool Validate() override { return !CheckForUnapplied(); }
-
 public:
   GOOrganSettingsPipesTab(
-    GOOrganModel &organModel,
-    wxWindow *parent,
-    GOOrganSettingsButtonsProxy::Listener &listener);
-
-  void DistributeAudio();
-  void ResetToDefault();
-  void DiscardChanges();
-  void ApplyChanges();
+    GOOrganModel &organModel, GOOrganSettingsDialogBase *pDlg);
+
+  void DistributeAudio() override;
+  void ResetToDefault() override;
+  void DiscardChanges() override;
+  void ApplyChanges() override;
 
   DECLARE_EVENT_TABLE()
 };
diff --git a/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsTab.cpp b/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsTab.cpp
new file mode 100644
index 000000000..c74e3168f
--- /dev/null
+++ b/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsTab.cpp
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2006 Milan Digital Audio LLC
+ * Copyright 2009-2025 GrandOrgue contributors (see AUTHORS)
+ * License GPL-2.0 or later
+ * (https://www.gnu.org/licenses/old-licenses/gpl-2.0.html).
+ */
+
+#include "GOOrganSettingsTab.h"
+
+#include <wx/intl.h>
+
+#include "gui/dialogs/common/GOTabbedDialog.h"
+
+#include "GOEvent.h"
+#include "GOOrganSettingsDialogBase.h"
+
+GOOrganSettingsTab::GOOrganSettingsTab(
+  GOOrganSettingsDialogBase *pDlg, const wxString &name, const wxString &label)
+  : GODialogTab(pDlg, name, label), p_dlg(pDlg) {}
+
+void GOOrganSettingsTab::NotifyButtonStatesChanged() {
+  p_dlg->ButtonStatesChanged();
+}
+
+void GOOrganSettingsTab::NotifyModified(bool newValue) {
+  SetModified(newValue);
+  NotifyButtonStatesChanged();
+}
+
+bool GOOrganSettingsTab::CheckForUnapplied() {
+  bool res = IsModified();
+
+  if (res)
+    GOMessageBox(
+      _("Please apply or discard changes first"),
+      _("Error"),
+      wxOK | wxICON_ERROR,
+      p_dlg);
+  return res;
+}
diff --git a/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsTab.h b/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsTab.h
new file mode 100644
index 000000000..a6747f69a
--- /dev/null
+++ b/src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsTab.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2006 Milan Digital Audio LLC
+ * Copyright 2009-2025 GrandOrgue contributors (see AUTHORS)
+ * License GPL-2.0 or later
+ * (https://www.gnu.org/licenses/old-licenses/gpl-2.0.html).
+ */
+
+#ifndef GOORGANSETTINGSBUTTONSPROXY_H
+#define GOORGANSETTINGSBUTTONSPROXY_H
+
+#include "gui/dialogs/common/GODialogTab.h"
+
+class GOOrganSettingsDialogBase;
+
+class GOOrganSettingsTab : public GODialogTab {
+public:
+private:
+  GOOrganSettingsDialogBase *p_dlg;
+
+protected:
+  bool m_IsDistributeAudioEnabled = false;
+  bool m_IsDefaultEnabled = true;
+
+private:
+  bool m_IsModified = false;
+
+public:
+  GOOrganSettingsTab(
+    GOOrganSettingsDialogBase *pDlg,
+    const wxString &name,
+    const wxString &label);
+
+protected:
+  void NotifyButtonStatesChanged();
+  bool IsModified() const { return m_IsModified; }
+  void SetModified(bool newValue) { m_IsModified = newValue; }
+  void NotifyModified(bool newValue = true);
+
+public:
+  bool IsDistributeAudioEnabled() const {
+    return m_IsDistributeAudioEnabled && !IsModified();
+  }
+
+  bool IsDefaultEnabled() const { return m_IsDefaultEnabled && !IsModified(); }
+
+  bool IsRevertEnabled() const { return IsModified(); }
+  bool IsApplyEnabled() const { return IsModified(); }
+
+  virtual void DistributeAudio() {}
+  virtual void ResetToDefault() = 0;
+  virtual void DiscardChanges() = 0;
+  virtual void ApplyChanges() = 0;
+
+  /**
+   * Checks if all changes have been applied. If some unapplied changes are
+   * present, then display an error message.
+   * Returns if there are unapplied changes
+   */
+  bool CheckForUnapplied();
+  bool Validate() override { return !CheckForUnapplied(); }
+};
+
+#endif /* GOORGANSETTINGSBUTTONSPROXY_H */
diff --git a/src/grandorgue/model/GOEnclosure.cpp b/src/grandorgue/model/GOEnclosure.cpp
index 984966c03..977233cea 100644
--- a/src/grandorgue/model/GOEnclosure.cpp
+++ b/src/grandorgue/model/GOEnclosure.cpp
@@ -25,6 +25,7 @@ GOEnclosure::GOEnclosure(GOOrganModel &organModel)
     MIDI_SEND_ENCLOSURE,
     MIDI_RECV_ENCLOSURE,
     GOMidiShortcutReceiver::KEY_RECV_ENCLOSURE),
+    m_IsOdfDefined(false),
     m_DefaultAmpMinimumLevel(0),
     m_MIDIInputNumber(0),
     m_Displayed1(false),
@@ -53,6 +54,7 @@ void GOEnclosure::Init(
   const wxString &group,
   const wxString &name,
   uint8_t defaultValue) {
+  m_IsOdfDefined = false;
   m_MIDIInputNumber = 0;
   GOMidiReceivingSendingObject::Init(cfg, group, name);
   m_DefaultAmpMinimumLevel = 0;
@@ -62,6 +64,7 @@ void GOEnclosure::Init(
 void GOEnclosure::Load(
   GOConfigReader &cfg, const wxString &group, int enclosureNb) {
   SetInitialMidiIndex(enclosureNb); // Used in LoadMidiObject
+  m_IsOdfDefined = true;
   m_MIDIInputNumber = cfg.ReadInteger(
     ODFSetting, group, wxT("MIDIInputNumber"), 0, 200, false, 0);
   GOMidiReceivingSendingObject::Load(
diff --git a/src/grandorgue/model/GOEnclosure.h b/src/grandorgue/model/GOEnclosure.h
index b31712c22..11cea3c78 100644
--- a/src/grandorgue/model/GOEnclosure.h
+++ b/src/grandorgue/model/GOEnclosure.h
@@ -22,6 +22,7 @@ class GOOrganModel;
 
 class GOEnclosure : public GOControl, public GOMidiObjectWithShortcut {
 private:
+  bool m_IsOdfDefined;
   uint8_t m_DefaultAmpMinimumLevel;
   uint8_t m_MIDIInputNumber;
   bool m_Displayed1;
@@ -52,6 +53,8 @@ class GOEnclosure : public GOControl, public GOMidiObjectWithShortcut {
 
   GOEnclosure(GOOrganModel &organModel);
 
+  bool IsOdfDefined() const { return m_IsOdfDefined; }
+
   using GOMidiObjectWithShortcut::Init; // for avoiding a warning
   void Init(
     GOConfigReader &cfg,
@@ -60,6 +63,9 @@ class GOEnclosure : public GOControl, public GOMidiObjectWithShortcut {
     uint8_t defValue);
   using GOMidiObject::Load; // for avoiding a warning
   void Load(GOConfigReader &cfg, const wxString &group, int enclosureNb);
+  uint8_t GetDefaultAmpMinimumLevel() const { return m_DefaultAmpMinimumLevel; }
+  uint8_t GetAmpMinimumLevel() const { return m_AmpMinimumLevel; }
+  void SetAmpMinimumLevel(uint8_t v) { m_AmpMinimumLevel = v; }
   void SetEnclosureValue(uint8_t n);
   int GetEnclosureValue() const { return m_MIDIValue; }
   int GetMIDIInputNumber() const { return m_MIDIInputNumber; }
diff --git a/src/grandorgue/model/GOOrganModel.h b/src/grandorgue/model/GOOrganModel.h
index f87db0d78..1e386db49 100644
--- a/src/grandorgue/model/GOOrganModel.h
+++ b/src/grandorgue/model/GOOrganModel.h
@@ -150,12 +150,14 @@ class GOOrganModel : private GOCombinationButtonSet,
   void UpdateTremulant(GOTremulant *tremulant);
   void UpdateVolume();
 
+  const ptr_vector<GOWindchest> &GetWindchests() const { return m_windchests; }
   unsigned GetWindchestCount() const { return m_windchests.size(); }
   // Returns the windchest number starting with 1
   unsigned AddWindchest(GOWindchest *windchest);
   // Returns the windchest by it's index starting with 0
   GOWindchest *GetWindchest(unsigned index) { return m_windchests[index]; }
 
+  const ptr_vector<GOEnclosure> &GetEnclosures() const { return m_enclosures; }
   GOEnclosure *GetEnclosureElement(unsigned index);
   unsigned GetEnclosureCount();
   unsigned AddEnclosure(GOEnclosure *enclosure);
diff --git a/src/grandorgue/model/GOWindchest.h b/src/grandorgue/model/GOWindchest.h
index 7030eb74d..7ce3af72f 100644
--- a/src/grandorgue/model/GOWindchest.h
+++ b/src/grandorgue/model/GOWindchest.h
@@ -1,6 +1,6 @@
 /*
  * Copyright 2006 Milan Digital Audio LLC
- * Copyright 2009-2023 GrandOrgue contributors (see AUTHORS)
+ * Copyright 2009-2025 GrandOrgue contributors (see AUTHORS)
  * License GPL-2.0 or later
  * (https://www.gnu.org/licenses/old-licenses/gpl-2.0.html).
  */
@@ -50,6 +50,9 @@ class GOWindchest : private GOSoundStateHandler {
   void AddRank(GORank *rank);
   void AddPipe(GOPipeWindchestCallback *pipe);
   void AddEnclosure(GOEnclosure *enclosure);
+  const std::vector<GOEnclosure *> &GetEnclosures() const {
+    return m_enclosure;
+  }
   const wxString &GetName();
   GOPipeConfigNode &GetPipeConfig();
 };


Reviews:
user: login: larspalo
body: Personally I'd like to have the "Master" enclosure displayed only at one single position in the tree (at the [Organ] level equivalent root). I wonder if it also would be better to simplify the enclosure tree so that selecting a ("windchest") node would immediately activate/fill the Minimum Amplitude Level for that built in enclosure and all ODF defined ones could just never be displayed... It would be a much cleaner version and if you think the tab name Enclosures would be confusing then it could be re-named as Built-in Enclosures or Panel Enclosures.

At the very least I think it's quite redundant to have first the windchest as a node and below it the (built-in) enclosure with exactly the same name.

So, if you don't want to re-work this completely like I suggest and still want the ODF defined enclosures displayed, I still suggest that selecting the "windchest node" of the tree would automatically set/be the built-in enclosure.

Another thing is that while it's a very nice touch to show the affected windchests for any enclosure I think it would be good if they were not possible to select since it's not really relevant for anything anyway and might actually lead to misunderstandings about what the minimum amplitude level below actually will set (the item in the tree and not the one in the listbox). Since wxListBox doesn't support an non-selectable style by default other tricks would be needed like disabling the control, handling/removing selection, or perhaps more easily by switching to using another control completely.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T17:43:03Z
user: login: larspalo
body: I just suggest a few more help text changes, otherwise I'm happy with this.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T15:44:50Z
user: login: oleg68
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T17:27:37Z
user: login: oleg68
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T17:27:52Z
user: login: rousseldenis
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T09:35:12Z

Review comments:
diff_hunk: @@ -5652,6 +5652,53 @@ role="bold">Group</emphasis> attribute are grouped in the same submenu.
 	</sect3>
       </sect2>
     </sect1>
+    <sect1>
+      <title>Enclosures</title>
+      <indexterm><primary>Organ settings.Enclosures</primary></indexterm>
+      <figure>
+	<title>Organ Settings dialog, Enclosures tab</title>
+	<mediaobject>
+	  <imageobject>
+	    <imagedata scalefit="1" width="100%" fileref="images/organ_settings_enclosures.png"/>
+	  </imageobject>
+	</mediaobject>
+      </figure>
+      <para>
+	In its left box, this dialog displays all enclosures in the three: both
+	found in the definition file or added internally by GrandOrgue. They are
+	grouped by windchest. Now only the enclosures added internally are
+	allowed to change.
+      </para>
+      <simpara>
+	Select the element you wish to customize. The right controls are
+	populated with values picked from the organ definition file or the
+	current preset number if the latter was saved.
+      </simpara>
+      <simpara>
+	It is possible to select multiple elements at once, to e.g. update one
+	setting of multiple elements to the same value in a single operation.
+      </simpara>
+      <sect2>
+	<title>Affected windchests</title>
+	<para>
+	  The list of windchest the enclosure affects.
+	</para>
+	<para>
+	  The same enclosure appears under all the the windchests in tree.
+	</para>
+      </sect2>
+      <sect2>
+	<title>Minimum amplitude level</title>
+	<para>
+	  Linear sound amplitude scale factor when the enclosure is closed. The
+	  allowed range is {0, +10}.
path: help/grandorgue.xml
user: login: larspalo
body: ```suggestion
	  allowed range is 0 - 100.
```
created_at: 2025-03-07T16:36:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5652,6 +5652,53 @@ role="bold">Group</emphasis> attribute are grouped in the same submenu.
 	</sect3>
       </sect2>
     </sect1>
+    <sect1>
+      <title>Enclosures</title>
+      <indexterm><primary>Organ settings.Enclosures</primary></indexterm>
+      <figure>
+	<title>Organ Settings dialog, Enclosures tab</title>
+	<mediaobject>
+	  <imageobject>
+	    <imagedata scalefit="1" width="100%" fileref="images/organ_settings_enclosures.png"/>
+	  </imageobject>
+	</mediaobject>
+      </figure>
+      <para>
+	In its left box, this dialog displays all enclosures in the three: both
+	found in the definition file or added internally by GrandOrgue. They are
+	grouped by windchest. Now only the enclosures added internally are
+	allowed to change.
+      </para>
+      <simpara>
+	Select the element you wish to customize. The right controls are
+	populated with values picked from the organ definition file or the
+	current preset number if the latter was saved.
+      </simpara>
+      <simpara>
+	It is possible to select multiple elements at once, to e.g. update one
+	setting of multiple elements to the same value in a single operation.
+      </simpara>
+      <sect2>
+	<title>Affected windchests</title>
+	<para>
+	  The list of windchest the enclosure affects.
+	</para>
+	<para>
+	  The same enclosure appears under all the the windchests in tree.
+	</para>
+      </sect2>
+      <sect2>
+	<title>Minimum amplitude level</title>
+	<para>
+	  Linear sound amplitude scale factor when the enclosure is closed. The
+	  allowed range is {0, +10}.
+	</para>
+	<para>
+	  0 means full silence. 100 means the enclosure does not affect the
path: help/grandorgue.xml
user: login: larspalo
body: ```suggestion
	  0 means complete silence. 100 means the enclosure does not affect the
```
created_at: 2025-03-07T16:36:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5652,6 +5652,53 @@ role="bold">Group</emphasis> attribute are grouped in the same submenu.
 	</sect3>
       </sect2>
     </sect1>
+    <sect1>
+      <title>Enclosures</title>
+      <indexterm><primary>Organ settings.Enclosures</primary></indexterm>
+      <figure>
+	<title>Organ Settings dialog, Enclosures tab</title>
+	<mediaobject>
+	  <imageobject>
+	    <imagedata scalefit="1" width="100%" fileref="images/organ_settings_enclosures.png"/>
+	  </imageobject>
+	</mediaobject>
+      </figure>
+      <para>
+	In its left box, this dialog displays all enclosures in the three: both
+	found in the definition file or added internally by GrandOrgue. They are
+	grouped by windchest. Now only the enclosures added internally are
+	allowed to change.
+      </para>
+      <simpara>
+	Select the element you wish to customize. The right controls are
+	populated with values picked from the organ definition file or the
+	current preset number if the latter was saved.
+      </simpara>
+      <simpara>
+	It is possible to select multiple elements at once, to e.g. update one
+	setting of multiple elements to the same value in a single operation.
+      </simpara>
+      <sect2>
+	<title>Affected windchests</title>
+	<para>
+	  The list of windchest the enclosure affects.
+	</para>
+	<para>
+	  The same enclosure appears under all the the windchests in tree.
+	</para>
+      </sect2>
+      <sect2>
+	<title>Minimum amplitude level</title>
+	<para>
+	  Linear sound amplitude scale factor when the enclosure is closed. The
+	  allowed range is {0, +10}.
+	</para>
+	<para>
+	  0 means full silence. 100 means the enclosure does not affect the
+	  windchests volume at all.
path: help/grandorgue.xml
user: login: larspalo
body: ```suggestion
	  windchest volume at all.
```
created_at: 2025-03-07T16:37:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,28 +71,37 @@ GOOrganSettingsDialog::GOOrganSettingsDialog(
     pButtonSizer->InsertSpacer(9, 10);
   }
 
+  // Don't move above because adding tabs raises events that require buttons
+  m_PipesTab = new GOOrganSettingsPipesTab(organModel, this);
+  AddTab(m_PipesTab);
+
+  m_EnclosuresTab = new GOOrganSettingsEnclosuresTab(organModel, this);
+  AddTab(m_EnclosuresTab);
+
   LayoutDialog();
 }
 
 void GOOrganSettingsDialog::ButtonStatesChanged() {
-  m_AudioGroupAssistant->Enable(m_PipesTab->IsDistributeAudioEnabled());
-  m_Default->Enable(m_PipesTab->IsDefaultEnabled());
-  m_Discard->Enable(m_PipesTab->IsRevertEnabled());
-  m_Apply->Enable(m_PipesTab->IsApplyEnabled());
-}
+  auto pTab = dynamic_cast<GOOrganSettingsTab *>(GetBook()->GetCurrentPage());
 
-void GOOrganSettingsDialog::OnButtonDistributeAudio(wxCommandEvent &e) {
-  m_PipesTab->DistributeAudio();
-}
+  if (pTab) {
+    bool isDistributeAnabled = pTab->IsDistributeAudioEnabled();
path: src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsDialog.cpp
user: login: larspalo
body: ```suggestion
    bool isDistributeEnabled = pTab->IsDistributeAudioEnabled();
```
created_at: 2025-03-07T16:39:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,28 +71,37 @@ GOOrganSettingsDialog::GOOrganSettingsDialog(
     pButtonSizer->InsertSpacer(9, 10);
   }
 
+  // Don't move above because adding tabs raises events that require buttons
+  m_PipesTab = new GOOrganSettingsPipesTab(organModel, this);
+  AddTab(m_PipesTab);
+
+  m_EnclosuresTab = new GOOrganSettingsEnclosuresTab(organModel, this);
+  AddTab(m_EnclosuresTab);
+
   LayoutDialog();
 }
 
 void GOOrganSettingsDialog::ButtonStatesChanged() {
-  m_AudioGroupAssistant->Enable(m_PipesTab->IsDistributeAudioEnabled());
-  m_Default->Enable(m_PipesTab->IsDefaultEnabled());
-  m_Discard->Enable(m_PipesTab->IsRevertEnabled());
-  m_Apply->Enable(m_PipesTab->IsApplyEnabled());
-}
+  auto pTab = dynamic_cast<GOOrganSettingsTab *>(GetBook()->GetCurrentPage());
 
-void GOOrganSettingsDialog::OnButtonDistributeAudio(wxCommandEvent &e) {
-  m_PipesTab->DistributeAudio();
-}
+  if (pTab) {
+    bool isDistributeAnabled = pTab->IsDistributeAudioEnabled();
 
-void GOOrganSettingsDialog::OnButtonDefault(wxCommandEvent &e) {
-  m_PipesTab->ResetToDefault();
+    m_AudioGroupAssistant->Enable(isDistributeAnabled);
path: src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsDialog.cpp
user: login: larspalo
body: ```suggestion
    m_AudioGroupAssistant->Enable(isDistributeEnabled);
```
created_at: 2025-03-07T16:39:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,244 @@
+/*
+ * Copyright 2006 Milan Digital Audio LLC
+ * Copyright 2009-2025 GrandOrgue contributors (see AUTHORS)
+ * License GPL-2.0 or later
+ * (https://www.gnu.org/licenses/old-licenses/gpl-2.0.html).
+ */
+
+#include "GOOrganSettingsEnclosuresTab.h"
+
+#include <wx/checkbox.h>
+#include <wx/gbsizer.h>
+#include <wx/listbox.h>
+#include <wx/stattext.h>
+#include <wx/treectrl.h>
+
+#include "model/GOEnclosure.h"
+#include "model/GOOrganModel.h"
+#include "model/GOWindchest.h"
+
+#include "GOEvent.h"
+
+static const wxSize EDIT_SIZE = wxSize(60, -1);
+static const wxString WX_TAB_CODE = wxT("Enclosures");
+static const wxString WX_TAB_TITLE = _("Enclosures");
+
+enum {
+  ID_EVENT_TREE = 200,
+  ID_EVENT_MIN_AMP_LEVEL,
+};
+
+BEGIN_EVENT_TABLE(GOOrganSettingsEnclosuresTab, wxPanel)
+EVT_TREE_SEL_CHANGING(
+  ID_EVENT_TREE, GOOrganSettingsEnclosuresTab::OnTreeChanging)
+EVT_TREE_SEL_CHANGED(ID_EVENT_TREE, GOOrganSettingsEnclosuresTab::OnTreeChanged)
+EVT_TEXT(
+  ID_EVENT_MIN_AMP_LEVEL, GOOrganSettingsEnclosuresTab::OnMinAmpLevelChanged)
+END_EVENT_TABLE()
+
+class GOOrganSettingsEnclosuresTab::ItemData : public wxTreeItemData {
+public:
+  enum ItemType {
+    ORGAN,
+    WINDCHEST,
+    ENCLOSURE,
+  } m_type;
+
+  union {
+    GOOrganModel *p_organ;
+    GOWindchest *p_windchest;
+    GOEnclosure *p_enclosure;
+  };
+
+  ItemData(GOOrganModel &organModel) : m_type(ORGAN), p_organ(&organModel) {}
+  ItemData(GOWindchest &windchest)
+    : m_type(WINDCHEST), p_windchest(&windchest) {}
+  ItemData(GOEnclosure &enclosure)
+    : m_type(ENCLOSURE), p_enclosure(&enclosure) {}
+};
+
+GOOrganSettingsEnclosuresTab::GOOrganSettingsEnclosuresTab(
+  GOOrganModel &organModel, GOOrganSettingsDialogBase *pDlg)
+  : GOOrganSettingsTab(pDlg, WX_TAB_CODE, WX_TAB_TITLE),
+    r_OrganModel(organModel) {
+  wxGridBagSizer *const mainSizer = new wxGridBagSizer(5, 5);
+
+  m_tree = new wxTreeCtrl(
+    this,
+    ID_EVENT_TREE,
+    wxDefaultPosition,
+    wxDefaultSize,
+    wxTR_HAS_BUTTONS | wxTR_MULTIPLE);
+  mainSizer->Add(
+    m_tree, wxGBPosition(0, 0), wxGBSpan(4, 1), wxEXPAND | wxALL, 5);
+
+  m_IsOdfDefined = new wxCheckBox(
+    this, wxID_ANY, _("This enclosure is ODF defined and may not be altered"));
+  m_IsOdfDefined->Disable();
+
+  mainSizer->Add(
+    m_IsOdfDefined,
+    wxGBPosition(0, 1),
+    wxGBSpan(1, 3),
+    wxALIGN_LEFT | wxEXPAND | wxALL,
+    5);
+  mainSizer->Add(
+    new wxStaticText(this, wxID_ANY, _("Affected windchests:")),
+    wxGBPosition(1, 1),
+    wxDefaultSpan,
+    wxALIGN_RIGHT | wxTOP | wxLEFT | wxBOTTOM,
+    5);
+  m_WindchestList = new wxListBox(
+    this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0, nullptr, wxLB_SINGLE);
+  mainSizer->Add(
+    m_WindchestList,
+    wxGBPosition(1, 2),
+    wxGBSpan(2, 2),
+    wxEXPAND | wxTOP | wxRIGHT | wxBOTTOM,
+    5);
+  mainSizer->Add(
+    new wxStaticText(this, wxID_ANY, _("Minimal amplitude level:")),
path: src/grandorgue/gui/dialogs/organ-settings/GOOrganSettingsEnclosuresTab.cpp
user: login: larspalo
body: ```suggestion
    new wxStaticText(this, wxID_ANY, _("Minimum amplitude level:")),
```
created_at: 2025-03-07T16:49:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5652,6 +5652,52 @@ role="bold">Group</emphasis> attribute are grouped in the same submenu.
 	</sect3>
       </sect2>
     </sect1>
+    <sect1>
+      <title>Enclosures</title>
+      <indexterm><primary>Organ settings.Enclosures</primary></indexterm>
+      <figure>
+	<title>Organ Settings dialog, Enclosures tab</title>
+	<mediaobject>
+	  <imageobject>
+	    <imagedata scalefit="1" width="100%" fileref="images/organ_settings_enclosures.png"/>
+	  </imageobject>
+	</mediaobject>
+      </figure>
+      <para>
+	In its left box, this dialog displays all enclosures: both added 
+	internally by GrandOrgueand ones found in the definition file. Now only
path: help/grandorgue.xml
user: login: larspalo
body: ```suggestion
	internally by GrandOrgue and the ones found in the definition file. Only
```
created_at: 2025-03-15T15:40:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5652,6 +5652,52 @@ role="bold">Group</emphasis> attribute are grouped in the same submenu.
 	</sect3>
       </sect2>
     </sect1>
+    <sect1>
+      <title>Enclosures</title>
+      <indexterm><primary>Organ settings.Enclosures</primary></indexterm>
+      <figure>
+	<title>Organ Settings dialog, Enclosures tab</title>
+	<mediaobject>
+	  <imageobject>
+	    <imagedata scalefit="1" width="100%" fileref="images/organ_settings_enclosures.png"/>
+	  </imageobject>
+	</mediaobject>
+      </figure>
+      <para>
+	In its left box, this dialog displays all enclosures: both added 
+	internally by GrandOrgueand ones found in the definition file. Now only
+	the enclosures added internally are allowed to be changed.
+      </para>
+      <simpara>
+	Select the element you wish to customize. The right controls are
+	populated with values picked from the organ definition file or the
+	current preset number if the latter was saved.
+      </simpara>
+      <simpara>
+	It is possible to select multiple elements at once, to e.g. update one
+	setting of multiple elements to the same value in a single operation.
+      </simpara>
+      <sect2>
+	<title>Affected windchests</title>
+	<para>
+	  The list of windchest the enclosure affects.
+	</para>
+	<para>
+	  The same enclosure appears under all the the windchests in tree.
path: help/grandorgue.xml
user: login: larspalo
body: I don't exactly understand what this line means right now though...
created_at: 2025-03-15T15:44:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5652,6 +5652,52 @@ role="bold">Group</emphasis> attribute are grouped in the same submenu.
 	</sect3>
       </sect2>
     </sect1>
+    <sect1>
+      <title>Enclosures</title>
+      <indexterm><primary>Organ settings.Enclosures</primary></indexterm>
+      <figure>
+	<title>Organ Settings dialog, Enclosures tab</title>
+	<mediaobject>
+	  <imageobject>
+	    <imagedata scalefit="1" width="100%" fileref="images/organ_settings_enclosures.png"/>
+	  </imageobject>
+	</mediaobject>
+      </figure>
+      <para>
+	In its left box, this dialog displays all enclosures: both added 
+	internally by GrandOrgueand ones found in the definition file. Now only
path: help/grandorgue.xml
user: login: oleg68
body: Fixed
created_at: 2025-03-15T17:27:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5652,6 +5652,52 @@ role="bold">Group</emphasis> attribute are grouped in the same submenu.
 	</sect3>
       </sect2>
     </sect1>
+    <sect1>
+      <title>Enclosures</title>
+      <indexterm><primary>Organ settings.Enclosures</primary></indexterm>
+      <figure>
+	<title>Organ Settings dialog, Enclosures tab</title>
+	<mediaobject>
+	  <imageobject>
+	    <imagedata scalefit="1" width="100%" fileref="images/organ_settings_enclosures.png"/>
+	  </imageobject>
+	</mediaobject>
+      </figure>
+      <para>
+	In its left box, this dialog displays all enclosures: both added 
+	internally by GrandOrgueand ones found in the definition file. Now only
+	the enclosures added internally are allowed to be changed.
+      </para>
+      <simpara>
+	Select the element you wish to customize. The right controls are
+	populated with values picked from the organ definition file or the
+	current preset number if the latter was saved.
+      </simpara>
+      <simpara>
+	It is possible to select multiple elements at once, to e.g. update one
+	setting of multiple elements to the same value in a single operation.
+      </simpara>
+      <sect2>
+	<title>Affected windchests</title>
+	<para>
+	  The list of windchest the enclosure affects.
+	</para>
+	<para>
+	  The same enclosure appears under all the the windchests in tree.
path: help/grandorgue.xml
user: login: oleg68
body: Removed
created_at: 2025-03-15T17:27:52Z
author_association: CONTRIBUTOR

PR comments:
user: login: larspalo
created_at: 2025-02-18T20:04:31Z
body: @oleg68 On Ubuntu 20.04 the build of this PR will crash immediately when I try to open the Audio/Midi->Organ Settings dialog. It doesn't matter what sample set I try to use or what settings I might have changed, GO reliably crashes as soon as I open the Organ Settings dialog.
author_association: CONTRIBUTOR
user: login: oleg68
created_at: 2025-02-22T12:13:17Z
body: @larspalo I fixed the crash
author_association: CONTRIBUTOR
user: login: oleg68
created_at: 2025-02-27T17:57:48Z
body: @larspalo @rousseldenis could you approve this PR?
author_association: CONTRIBUTOR
user: login: oleg68
created_at: 2025-03-05T18:36:03Z
body: @larspalo Do you have any opportunity to review this and another PRs now?
author_association: CONTRIBUTOR
user: login: larspalo
created_at: 2025-03-07T16:15:57Z
body: @oleg68 I'm just back from a vacation trip. I'll start reading the PRs.
author_association: CONTRIBUTOR
user: login: oleg68
created_at: 2025-03-07T16:17:07Z
body: Excelent!
author_association: CONTRIBUTOR
user: login: oleg68
created_at: 2025-03-08T19:32:23Z
body: @larspalo Thank you for your review.

I like that the new Enclosures tab shows the full structure of windchests and enclosures so the user can see the properties of both odf and built-it ones ant can choose what enclosure to use.

I'll move Master to the organ level and I'll try to disable selecting the windchests.
author_association: CONTRIBUTOR
user: login: oleg68
created_at: 2025-03-11T19:44:28Z
body: @larspalo 

I implemented displaying `Master` at the organ level and I disabled selecting affected windchests.

Talking about auto selecting a default inner enclosure when the windchest is selected, I don't want to do it, because it would be very unexpected from the UI point of view: now the user has a full control of selecting and deselecting.

I also fixed your small requests. So I'd like to ask for your approval now.

author_association: CONTRIBUTOR
user: login: larspalo
created_at: 2025-03-11T21:25:42Z
body: > Talking about auto selecting a default inner enclosure when the windchest is selected, I don't want to do it, because it would be very unexpected from the UI point of view: now the user has a full control of selecting and deselecting.

I was hinting more at the option to just use the windchest tree item as the same built-in GO version of that enclosure, thus getting rid of the (child) duplication. I'm ok with having the ODF versions displayed under them, but for ease of use and a neater look I'd prefer using the windchest (parent) also as the built-in enclosure. This would also make adjustments easier/quicker as one wouldn't need to expand the tree items to get to the (built-in) enclosure.

Otherwise, I think that this PR is now much better than before. I'd like to await the thoughts of @rousseldenis too though on the windchest/enclosure hierarchy and the obvious duplication of items/children that this causes. If the current layout is fine for him I'll also approve since the feature as such, in my opinion, is worth much more than the implementation details.
author_association: CONTRIBUTOR
user: login: oleg68
created_at: 2025-03-12T19:26:21Z
body: > I was hinting more at the option to just use the windchest tree item as the same built-in GO version of that enclosure, thus getting rid of the (child) duplication.

I object this idea because it would bring a value to the windchest level but it would lack it's inheritance to the child enclosures.

I've tried to implement your another idea with the list of enclosures instead the windchest-enclosure tree https://github.com/oleg68/GrandOrgue-official/actions/runs/13819598079 , but I don't like it because it hides the windchest-enclosure hierarchy from the user. 

Another issue with that implementation is wrong behavior on changing selection when there are some unapplied changes. Unlike wxTree, wxListBox does not allow to veto selection changes. It can be resolved with restoring the wxTree again with using only the first level items.


author_association: CONTRIBUTOR
user: login: larspalo
created_at: 2025-03-13T19:57:16Z
body: >  windchest level but it would lack it's inheritance to the child enclosures

Dear God! There is no such thing and shouldn't be! There should never ever be an inheritance from windchest to enclosures! In real life enclosures can span multiple windchests or just parts of a single windchest.
author_association: CONTRIBUTOR
user: login: oleg68
created_at: 2025-03-13T20:12:53Z
body: > There should never ever be an inheritance from windchest to enclosures!

Now there are no any settings at the windchest level so there is nothing to inherit. 

But you suggest to use the windchest node not as a windchest but as a default enclosure, so the user may expect that changing the default enclosure settings would affect all child nodes, that are other enclosures.

There is an inheritance on the Pipes tab, and users can expect that if two guis look the same, their behavior also would be the same.
author_association: CONTRIBUTOR
user: login: oleg68
created_at: 2025-03-13T20:15:52Z
body: @larspalo Could you take a look at https://github.com/oleg68/GrandOrgue-official/actions/runs/13841023334 ?

There is an alternative GUI implementation of enclosures. It is not ideal for me but it might be an acceptable compromise.
author_association: CONTRIBUTOR
user: login: larspalo
created_at: 2025-03-13T20:51:25Z
body: > But you suggest to use the windchest node not as a windchest but as a default enclosure, so the user may expect that changing the default enclosure settings would affect all child nodes, that are other enclosures.

> There is an inheritance on the Pipes tab, and users can expect that if two guis look the same, their behavior also would be the same.

This is a valid argument in my opinion, I can agree with that.

I've had a look at the other build with the list. It's much clearer in my opinion and I like it a lot! The only troublesome aspect of it is that the Master enclosure actually does "enclose" all other enclosures.

Thus the list as it currently is done is not an entirely "accurate" display of the hierarchy of the enclosures. Perhaps just moving the Master to the top and then adding all the ODF defined enclosures before the built-in versions would be enough to "clarify" the relationship.

I do like that it's now very quick and easy to select each enclosure directly (as the tab indicates) to see and possibly set the MinimumAmplitudeLevel and also find out exactly what windchests are affected by each enclosure. The exact relationship between individual enclosures could potentially be much more complex but I think that this version might indeed be a good compromise as you said!
author_association: CONTRIBUTOR
user: login: larspalo
created_at: 2025-03-14T16:04:38Z
body: @oleg68 Having tested this a little more I think that it perhaps would be better in the list version to have Master and all the built-in enclosures at top and below them list the ODF defined versions instead of having it the other way around.
author_association: CONTRIBUTOR
user: login: oleg68
created_at: 2025-03-15T12:01:26Z
body: > it perhaps would be better in the list version to have Master and all the built-in enclosures at top and below them list the ODF defined

I pushed the list version here and I implemented your suggestion. Please check here: https://github.com/oleg68/GrandOrgue-official/actions/runs/13872602989
author_association: CONTRIBUTOR
user: login: larspalo
created_at: 2025-03-15T13:17:49Z
body: > I pushed the list version here and I implemented your suggestion.

It's still implemented with a wxTreeCtrl though, but that's of course fine for me. The help image needs to be updated and the help text slightly adjusted to the new grouping of enclosures.

Otherwise I'm very happy with this version. It's easy to use and very clear without any distractions. The initial size of the tree is a bit off for most of my organs but that's easily changed.
author_association: CONTRIBUTOR
user: login: oleg68
created_at: 2025-03-15T14:51:00Z
body: > It's still implemented with a wxTreeCtrl though

I use wxTreeCtrl because wxListBox does not support to forbid change selection when there are some unapplied changes. 
author_association: CONTRIBUTOR
user: login: oleg68
created_at: 2025-03-15T15:17:06Z
body: > The help image needs to be updated and the help text slightly adjusted to the new grouping of enclosures.

Done.
author_association: CONTRIBUTOR
user: login: oleg68
created_at: 2025-03-15T17:28:39Z
body: @rousseldenis could you approve this PR?
author_association: CONTRIBUTOR