mirror of https://github.com/BOINC/boinc.git
472 lines
17 KiB
C++
472 lines
17 KiB
C++
// This file is part of BOINC.
|
|
// http://boinc.berkeley.edu
|
|
// Copyright (C) 2014 University of California
|
|
//
|
|
// BOINC is free software; you can redistribute it and/or modify it
|
|
// under the terms of the GNU Lesser General Public License
|
|
// as published by the Free Software Foundation,
|
|
// either version 3 of the License, or (at your option) any later version.
|
|
//
|
|
// BOINC is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
// See the GNU Lesser General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public License
|
|
// along with BOINC. If not, see <http://www.gnu.org/licenses/>.
|
|
//
|
|
#if defined(__GNUG__) && !defined(__APPLE__)
|
|
#pragma implementation "DlgExclusiveApps.h"
|
|
#endif
|
|
|
|
#include "stdwx.h"
|
|
#include "BOINCGUIApp.h"
|
|
#include "MainDocument.h"
|
|
#include "BOINCBaseFrame.h"
|
|
#include "SkinManager.h"
|
|
#include "Events.h"
|
|
#include "error_numbers.h"
|
|
#include "version.h"
|
|
#include "DlgExclusiveApps.h"
|
|
|
|
|
|
using std::string;
|
|
|
|
IMPLEMENT_DYNAMIC_CLASS(CDlgExclusiveApps, wxDialog)
|
|
|
|
BEGIN_EVENT_TABLE(CDlgExclusiveApps, wxDialog)
|
|
EVT_COMMAND(ID_LISTBOX_EXCLAPPS,wxEVT_COMMAND_LISTBOX_SELECTED,CDlgExclusiveApps::OnExclusiveAppListEvent)
|
|
EVT_COMMAND(ID_LISTBOX_EXCLGPUAPPS,wxEVT_COMMAND_LISTBOX_SELECTED,CDlgExclusiveApps::OnExclusiveGPUAppListEvent)
|
|
//buttons
|
|
EVT_BUTTON(ID_ADDEXCLUSIVEAPPBUTTON,CDlgExclusiveApps::OnAddExclusiveApp)
|
|
EVT_BUTTON(ID_REMOVEEXCLUSIVEAPPBUTTON,CDlgExclusiveApps::OnRemoveExclusiveApp)
|
|
EVT_BUTTON(ID_ADDEXCLUSIVEGPUAPPBUTTON,CDlgExclusiveApps::OnAddExclusiveGPUApp)
|
|
EVT_BUTTON(ID_REMOVEEXCLUSIVEGPUAPPBUTTON,CDlgExclusiveApps::OnRemoveExclusiveGPUApp)
|
|
EVT_BUTTON(wxID_OK,CDlgExclusiveApps::OnOK)
|
|
EVT_BUTTON(ID_HELPBOINC,CDlgExclusiveApps::OnHelp)
|
|
END_EVENT_TABLE()
|
|
|
|
/* Constructor */
|
|
CDlgExclusiveApps::CDlgExclusiveApps(wxWindow* parent) :
|
|
wxDialog( parent, ID_ANYDIALOG, wxEmptyString, wxDefaultPosition,
|
|
wxDefaultSize, wxDEFAULT_DIALOG_STYLE
|
|
) {
|
|
CSkinAdvanced* pSkinAdvanced = wxGetApp().GetSkinManager()->GetAdvanced();
|
|
wxASSERT(pSkinAdvanced);
|
|
wxASSERT(wxDynamicCast(pSkinAdvanced, CSkinAdvanced));
|
|
|
|
wxString title;
|
|
title.Printf(
|
|
_("%s - Exclusive Applications"),
|
|
pSkinAdvanced->GetApplicationShortName().c_str()
|
|
);
|
|
|
|
SetTitle(title);
|
|
|
|
m_bInInit=false;
|
|
m_bExclusiveAppsDataChanged=false;
|
|
|
|
wxBoxSizer* dialogSizer = new wxBoxSizer( wxVERTICAL );
|
|
dialogSizer->AddSpacer(10);
|
|
|
|
wxStaticBox* exclusiveAppsListStaticBox = new wxStaticBox( this, -1, _("Suspend processor and network usage when these applications are running:") );
|
|
wxStaticBoxSizer* exclusiveAppsListBoxSizer = new wxStaticBoxSizer( exclusiveAppsListStaticBox, wxVERTICAL );
|
|
|
|
m_exclusiveApsListBox = new wxListBox(exclusiveAppsListStaticBox, ID_LISTBOX_EXCLAPPS, wxDefaultPosition, wxSize(-1, 145), 0, NULL, wxLB_EXTENDED|wxLB_NEEDED_SB|wxLB_SORT);
|
|
exclusiveAppsListBoxSizer->Add(m_exclusiveApsListBox, 1, wxALL|wxEXPAND, 5);
|
|
|
|
wxBoxSizer* exclusiveAppsButtonSizer = new wxBoxSizer( wxHORIZONTAL );
|
|
|
|
m_addExclusiveAppButton = new wxButton( exclusiveAppsListStaticBox, ID_ADDEXCLUSIVEAPPBUTTON, _("Add..."), wxDefaultPosition, wxDefaultSize, 0 );
|
|
m_addExclusiveAppButton->SetToolTip( _("Add an application to this list"));
|
|
exclusiveAppsButtonSizer->Add( m_addExclusiveAppButton, 0, wxRIGHT, 5 );
|
|
|
|
exclusiveAppsButtonSizer->AddStretchSpacer();
|
|
|
|
m_removeExclusiveAppButton = new wxButton( exclusiveAppsListStaticBox, ID_REMOVEEXCLUSIVEAPPBUTTON, _("Remove"), wxDefaultPosition, wxDefaultSize, 0 );
|
|
m_removeExclusiveAppButton->SetToolTip( _("Remove an application from this list"));
|
|
exclusiveAppsButtonSizer->Add( m_removeExclusiveAppButton, 0, wxLEFT, 5 );
|
|
|
|
exclusiveAppsListBoxSizer->Add(exclusiveAppsButtonSizer, 0, wxEXPAND|wxLEFT|wxRIGHT, 25 );
|
|
|
|
dialogSizer->Add( exclusiveAppsListBoxSizer, 0, wxALL|wxEXPAND, 1 );
|
|
|
|
dialogSizer->AddSpacer(25);
|
|
|
|
wxStaticBox* exclusiveGPUAppsListStaticBox = new wxStaticBox( this, -1, _("Suspend GPU usage when these applications are running:") );
|
|
wxStaticBoxSizer* exclusiveGPUAppsListBoxSizer = new wxStaticBoxSizer( exclusiveGPUAppsListStaticBox, wxVERTICAL );
|
|
|
|
m_exclusiveGPUApsListBox = new wxListBox(exclusiveGPUAppsListStaticBox, ID_LISTBOX_EXCLGPUAPPS, wxDefaultPosition, wxSize(-1, 145), 0, NULL, wxLB_EXTENDED|wxLB_NEEDED_SB|wxLB_SORT);
|
|
exclusiveGPUAppsListBoxSizer->Add(m_exclusiveGPUApsListBox, 1, wxALL|wxEXPAND, 5);
|
|
|
|
wxBoxSizer* exclusiveGPUAppsButtonSizer = new wxBoxSizer( wxHORIZONTAL );
|
|
|
|
m_addExclusiveGPUAppButton = new wxButton( exclusiveGPUAppsListStaticBox, ID_ADDEXCLUSIVEGPUAPPBUTTON, _("Add..."), wxDefaultPosition, wxDefaultSize, 0 );
|
|
m_addExclusiveGPUAppButton->SetToolTip( _("Add an application to this list"));
|
|
exclusiveGPUAppsButtonSizer->Add( m_addExclusiveGPUAppButton, 0, wxRIGHT, 5 );
|
|
|
|
exclusiveGPUAppsButtonSizer->AddStretchSpacer();
|
|
|
|
m_removeExclusiveGPUAppButton = new wxButton( exclusiveGPUAppsListStaticBox, ID_REMOVEEXCLUSIVEGPUAPPBUTTON, _("Remove"), wxDefaultPosition, wxDefaultSize, 0 );
|
|
m_removeExclusiveGPUAppButton->SetToolTip( _("Remove an application from this list"));
|
|
exclusiveGPUAppsButtonSizer->Add( m_removeExclusiveGPUAppButton, 0, wxLEFT, 5 );
|
|
|
|
exclusiveGPUAppsListBoxSizer->Add(exclusiveGPUAppsButtonSizer, 0, wxEXPAND|wxLEFT|wxRIGHT, 25 );
|
|
|
|
dialogSizer->Add( exclusiveGPUAppsListBoxSizer, 0, wxALL|wxEXPAND, 1 );
|
|
|
|
wxBoxSizer* moreOptionsLinkSizer = new wxBoxSizer( wxHORIZONTAL );
|
|
|
|
moreOptionsLinkSizer->Add(
|
|
new wxStaticText(
|
|
this, wxID_ANY, _("For advanced options, refer to "),
|
|
wxDefaultPosition, wxDefaultSize, wxALIGN_RIGHT
|
|
),
|
|
0, wxLEFT, 5
|
|
);
|
|
|
|
moreOptionsLinkSizer->Add(
|
|
new wxHyperlinkCtrl(
|
|
this, wxID_ANY, wxT("http://boinc.berkeley.edu/wiki/Client_configuration"),
|
|
wxT("http://boinc.berkeley.edu/wiki/Client_configuration"),
|
|
wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE
|
|
),
|
|
#ifdef __WXMAC__
|
|
0, wxLEFT, 5
|
|
#else
|
|
0, wxLEFT, 3
|
|
#endif
|
|
);
|
|
|
|
dialogSizer->Add(moreOptionsLinkSizer, 0, wxALL, 10);
|
|
|
|
m_panelButtons = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0 );
|
|
wxBoxSizer* buttonSizer = new wxBoxSizer( wxHORIZONTAL );
|
|
|
|
m_btnOK = new wxButton( m_panelButtons, wxID_OK, _("OK"), wxDefaultPosition, wxDefaultSize, 0 );
|
|
m_btnOK->SetToolTip( _("save all values and close the dialog") );
|
|
m_btnOK->SetDefault();
|
|
|
|
buttonSizer->Add( m_btnOK, 0, wxALL, 5 );
|
|
|
|
m_btnCancel = new wxButton( m_panelButtons, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxDefaultSize, 0 );
|
|
m_btnCancel->SetToolTip( _("close the dialog without saving") );
|
|
|
|
buttonSizer->Add( m_btnCancel, 0, wxALL, 5 );
|
|
|
|
m_btnHelp = new wxButton( m_panelButtons, ID_HELPBOINC, _("Help"), wxDefaultPosition, wxDefaultSize, 0 );
|
|
m_btnHelp->SetToolTip( _("shows the preferences web page") );
|
|
|
|
buttonSizer->Add( m_btnHelp, 0, wxALL, 5 );
|
|
|
|
m_panelButtons->SetSizer( buttonSizer );
|
|
m_panelButtons->Layout();
|
|
buttonSizer->Fit( m_panelButtons );
|
|
dialogSizer->Add( m_panelButtons, 0, wxALIGN_BOTTOM|wxALIGN_CENTER_HORIZONTAL|wxALL, 1 );
|
|
|
|
m_removeExclusiveAppButton->Disable();
|
|
m_removeExclusiveGPUAppButton->Disable();
|
|
|
|
ReadPreferenceSettings();
|
|
|
|
// CAF SetSizerAndFit(dialogSizer);
|
|
Layout();
|
|
dialogSizer->Fit( this );
|
|
SetSizer( dialogSizer );
|
|
|
|
// CAF Fit();
|
|
Centre();
|
|
}
|
|
|
|
/* destructor */
|
|
CDlgExclusiveApps::~CDlgExclusiveApps() {
|
|
}
|
|
|
|
/* read preferences from core client and initialize control values */
|
|
void CDlgExclusiveApps::ReadPreferenceSettings() {
|
|
m_bInInit=true;//prevent dialog handlers from doing anything
|
|
CMainDocument* pDoc = wxGetApp().GetDocument();
|
|
int retval;
|
|
|
|
wxASSERT(pDoc);
|
|
wxASSERT(wxDynamicCast(pDoc, CMainDocument));
|
|
|
|
// Get cc_config.xml file flags
|
|
log_flags.init();
|
|
config.defaults();
|
|
retval = pDoc->rpc.get_cc_config(config, log_flags);
|
|
if (!retval) {
|
|
for (unsigned int i=0; i<config.exclusive_apps.size(); ++i) {
|
|
wxString appName = wxString(config.exclusive_apps[i].c_str(), wxConvUTF8);
|
|
m_exclusiveApsListBox->Append(appName);
|
|
}
|
|
|
|
for (unsigned int i=0; i<config.exclusive_gpu_apps.size(); ++i) {
|
|
wxString appName = wxString(config.exclusive_gpu_apps[i].c_str(), wxConvUTF8);
|
|
m_exclusiveGPUApsListBox->Append(appName);
|
|
}
|
|
}
|
|
|
|
m_bInInit=false;
|
|
}
|
|
|
|
|
|
bool CDlgExclusiveApps::SavePreferencesSettings() {
|
|
if (m_bExclusiveAppsDataChanged) {
|
|
CMainDocument* pDoc = wxGetApp().GetDocument();
|
|
|
|
wxASSERT(pDoc);
|
|
wxASSERT(wxDynamicCast(pDoc, CMainDocument));
|
|
|
|
wxArrayString appNames = m_exclusiveApsListBox->GetStrings();
|
|
|
|
config.exclusive_apps.clear();
|
|
for (unsigned int i=0; i<appNames.size(); ++i) {
|
|
std::string s = (const char*)appNames[i].mb_str();
|
|
config.exclusive_apps.push_back(s);
|
|
}
|
|
|
|
wxArrayString gpuAppNames = m_exclusiveGPUApsListBox->GetStrings();
|
|
config.exclusive_gpu_apps.clear();
|
|
for (unsigned int i=0; i<gpuAppNames.size(); ++i) {
|
|
std::string s = (const char*)gpuAppNames[i].mb_str();
|
|
config.exclusive_gpu_apps.push_back(s);
|
|
}
|
|
int retval = pDoc->rpc.set_cc_config(config, log_flags);
|
|
if (!retval) {
|
|
pDoc->rpc.read_cc_config();
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ------------ Event handlers starts here
|
|
// ---- Exclusive Apps list box handler
|
|
void CDlgExclusiveApps::OnExclusiveAppListEvent(wxCommandEvent& ev) {
|
|
wxArrayInt selections;
|
|
int numSelected;
|
|
|
|
if(!m_bInInit) {
|
|
numSelected = m_exclusiveApsListBox->GetSelections(selections);
|
|
m_removeExclusiveAppButton->Enable(numSelected > 0);
|
|
}
|
|
ev.Skip();
|
|
}
|
|
|
|
void CDlgExclusiveApps::OnExclusiveGPUAppListEvent(wxCommandEvent& ev) {
|
|
wxArrayInt selections;
|
|
int numSelected;
|
|
|
|
if(!m_bInInit) {
|
|
numSelected = m_exclusiveGPUApsListBox->GetSelections(selections);
|
|
m_removeExclusiveGPUAppButton->Enable(numSelected > 0);
|
|
}
|
|
ev.Skip();
|
|
}
|
|
|
|
// ---- command buttons handlers
|
|
// handles Add button clicked
|
|
void CDlgExclusiveApps::OnAddExclusiveApp(wxCommandEvent&) {
|
|
AddToListBox(m_exclusiveApsListBox);
|
|
}
|
|
|
|
void CDlgExclusiveApps::OnAddExclusiveGPUApp(wxCommandEvent&) {
|
|
AddToListBox(m_exclusiveGPUApsListBox);
|
|
}
|
|
|
|
void CDlgExclusiveApps::AddToListBox(wxListBox * theListBox) {
|
|
wxString strMachineName;
|
|
int i, j, n;
|
|
bool hostIsMac = false;
|
|
bool hostIsWin = false;
|
|
bool isDuplicate;
|
|
wxArrayString appNames;
|
|
wxChar *extension = wxT("");
|
|
wxString errmsg;
|
|
CMainDocument* pDoc = wxGetApp().GetDocument();
|
|
|
|
wxASSERT(pDoc);
|
|
wxASSERT(wxDynamicCast(pDoc, CMainDocument));
|
|
|
|
if (strstr(pDoc->state.host_info.os_name, "Darwin")) {
|
|
hostIsMac = true;
|
|
extension = wxT(".app");
|
|
} else if (strstr(pDoc->state.host_info.os_name, "Microsoft")) {
|
|
hostIsWin = true;
|
|
extension = wxT(".exe");
|
|
}
|
|
|
|
pDoc->GetConnectedComputerName(strMachineName);
|
|
if (pDoc->IsComputerNameLocal(strMachineName)) {
|
|
#ifdef __WXMAC__
|
|
wxFileDialog picker(this, _("Applications to add"),
|
|
wxT("/Applications"), wxT(""), wxT("*.app"),
|
|
wxFD_OPEN|wxFD_FILE_MUST_EXIST|wxFD_MULTIPLE|wxFD_CHANGE_DIR
|
|
);
|
|
#elif defined(__WXMSW__)
|
|
//TODO: fill in the default directory for MSW
|
|
wxFileDialog picker(this, _("Applications to add"),
|
|
wxT("C:/Program Files"), wxT(""), wxT("*.exe"),
|
|
wxFD_OPEN|wxFD_FILE_MUST_EXIST|wxFD_MULTIPLE|wxFD_CHANGE_DIR
|
|
);
|
|
#else
|
|
//TODO: fill in the default directory for Linux
|
|
wxFileDialog picker(this, _("Applications to add"),
|
|
wxT("/usr/bin"), wxT(""), wxT("*"),
|
|
wxFD_OPEN|wxFD_FILE_MUST_EXIST|wxFD_MULTIPLE|wxFD_CHANGE_DIR
|
|
);
|
|
#endif
|
|
if (picker.ShowModal() != wxID_OK) return;
|
|
picker.GetFilenames(appNames);
|
|
|
|
for (i=appNames.Count()-1; i>=0; --i) {
|
|
#ifdef __WXMSW__
|
|
// Under Windows, filename may include paths if a shortcut selected
|
|
wxString appNameOnly = appNames[i].AfterLast('\\');
|
|
appNames[i] = appNameOnly;
|
|
#endif
|
|
wxString directory = picker.GetDirectory();
|
|
wxFileName fn(directory, appNames[i]);
|
|
if (!fn.IsOk() || !fn.IsFileExecutable()) {
|
|
errmsg.Printf(_("'%s' is not an executable application."), appNames[i].c_str());
|
|
wxGetApp().SafeMessageBox(errmsg, _("Add Exclusive App"),
|
|
wxOK | wxICON_EXCLAMATION, this
|
|
);
|
|
appNames.RemoveAt(i);
|
|
continue;
|
|
}
|
|
}
|
|
} else {
|
|
// We can't use file picker if connected to a remote computer,
|
|
// so show a dialog with textedit field so user can type app name
|
|
wxChar path_separator = wxT('/');
|
|
|
|
wxTextEntryDialog dlg(this, _("Name of application to add?"), _("Add exclusive app"));
|
|
if (hostIsMac) {
|
|
dlg.SetValue(extension);
|
|
} else if (hostIsWin) {
|
|
dlg.SetValue(extension);
|
|
path_separator = wxT('\\');
|
|
}
|
|
if (dlg.ShowModal() != wxID_OK) return;
|
|
|
|
wxString theAppName = dlg.GetValue();
|
|
// Strip off path if present
|
|
appNames.Add(theAppName.AfterLast(path_separator));
|
|
}
|
|
|
|
for (i=0; i<(int)appNames.Count(); ++i) {
|
|
// wxFileName::IsFileExecutable() doesn't seem to work on Windows,
|
|
// and we can only perform minimal validation on remote hosts, so
|
|
// check filename extension on Mac and Win
|
|
bool bad_name = false;
|
|
if (hostIsMac) {
|
|
bad_name = !appNames[i].EndsWith(extension);
|
|
} else if (hostIsWin) {
|
|
size_t len = appNames[i].Len();
|
|
size_t xl = 4;
|
|
if (len < xl) {
|
|
bad_name = true;
|
|
} else {
|
|
wxString x = appNames[i].Mid(len-xl);
|
|
if (x.CmpNoCase(extension) != 0) {
|
|
bad_name = true;
|
|
}
|
|
}
|
|
}
|
|
if (bad_name) {
|
|
errmsg.Printf(_("Application names must end with '%s'"), extension);
|
|
wxGetApp().SafeMessageBox(errmsg, _("Add Exclusive App"),
|
|
wxOK | wxICON_EXCLAMATION, this
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (hostIsMac) {
|
|
int suffix = appNames[i].Find('.', true);
|
|
if (suffix != wxNOT_FOUND) {
|
|
appNames[i].Truncate(suffix);
|
|
}
|
|
}
|
|
|
|
// Skip requests for duplicate entries
|
|
isDuplicate = false;
|
|
n = theListBox->GetCount();
|
|
for (j=0; j<n; ++j) {
|
|
if ((theListBox->GetString(j)).Cmp(appNames[i]) == 0) {
|
|
isDuplicate = true;
|
|
break;
|
|
}
|
|
}
|
|
if (isDuplicate) {
|
|
errmsg.Printf(_("'%s' is already in the list."), appNames[i].c_str());
|
|
wxGetApp().SafeMessageBox(errmsg, _("Add Exclusive App"),
|
|
wxOK | wxICON_EXCLAMATION, this
|
|
);
|
|
continue;
|
|
}
|
|
|
|
theListBox->Append(appNames[i]);
|
|
m_bExclusiveAppsDataChanged = true;
|
|
}
|
|
}
|
|
|
|
typedef int (*sortcomparefunc)(int*, int*);
|
|
|
|
static int myCompareInts(int *first, int *second) {
|
|
return *first - *second;
|
|
}
|
|
// handles Remove button clicked
|
|
void CDlgExclusiveApps::OnRemoveExclusiveApp(wxCommandEvent& ev) {
|
|
wxArrayInt selections;
|
|
int numSelected = m_exclusiveApsListBox->GetSelections(selections);
|
|
|
|
// The selection indices are returned in random order.
|
|
// We must sort them to ensure deleting the correct items.
|
|
selections.Sort((sortcomparefunc)&myCompareInts);
|
|
for (int i=numSelected-1; i>=0; --i) {
|
|
m_exclusiveApsListBox->Delete(selections[i]);
|
|
m_bExclusiveAppsDataChanged = true;
|
|
}
|
|
ev.Skip();
|
|
}
|
|
|
|
void CDlgExclusiveApps::OnRemoveExclusiveGPUApp(wxCommandEvent& ev) {
|
|
wxArrayInt selections;
|
|
int numSelected = m_exclusiveGPUApsListBox->GetSelections(selections);
|
|
|
|
// The selection indices are returned in random order.
|
|
// We must sort them to ensure deleting the correct items.
|
|
selections.Sort((sortcomparefunc)&myCompareInts);
|
|
for (int i=numSelected-1; i>=0; --i) {
|
|
m_exclusiveGPUApsListBox->Delete(selections[i]);
|
|
m_bExclusiveAppsDataChanged = true;
|
|
}
|
|
ev.Skip();
|
|
}
|
|
|
|
// handles OK button clicked
|
|
void CDlgExclusiveApps::OnOK(wxCommandEvent& ev) {
|
|
SavePreferencesSettings();
|
|
|
|
ev.Skip();
|
|
}
|
|
|
|
// handles Help button clicked
|
|
void CDlgExclusiveApps::OnHelp(wxCommandEvent& ev) {
|
|
if (IsShown()) {
|
|
|
|
wxString strURL = wxGetApp().GetSkinManager()->GetAdvanced()->GetOrganizationHelpUrl();
|
|
|
|
wxString wxurl;
|
|
wxurl.Printf(
|
|
wxT("%s?target=exclusive_apps&version=%s&controlid=%d"),
|
|
strURL.c_str(),
|
|
wxString(BOINC_VERSION_STRING, wxConvUTF8).c_str(),
|
|
ev.GetId()
|
|
);
|
|
wxLaunchDefaultBrowser(wxurl);
|
|
}
|
|
}
|