Finally got precompiled headers to work.

Now all C++ files need to #include <util/prec.hpp>
 That is why all .cpp files are touched by this commit

Many changes to installers and update checking:
     - the window is now called PackagesWindow, in a new source file
     - update checking is now independent from the PackagesWindow. For update checking only a list of package versions are needed (vector<PackageDependency>). This is much less information to download at each startup.
     - the list of available packages is now a list of available Installers, since an installer can contain multiple packages.
     - moved the logic of dependency checking etc. to data/installer
     - moved the actual installation to util/io/package_manager
     - moved directory iteration/creation logic to util/file_utils
     - added PackageDirectory: the local and global package directory now have their own object (was part of PackageManager)
     - added PackageVersion: for detecting if a package has been modified after it was installed.
     - added PackageDescription: description/header of a package. Basicly the same as what Packaged provides.
     - added DownloadableInstaller: where to find an insaller, what does it contain?
     - added InstallablePackage: brining it all together: installer, package, status, action.

Current status: the insaller is currently broken in a few places, more on that soon.

git-svn-id: svn://svn.code.sf.net/p/magicseteditor/code/trunk@792 0fc631ac-6414-0410-93d0-97cfa31319b6
This commit is contained in:
twanvl
2007-12-29 18:30:41 +00:00
parent 361488b4fe
commit d2196eea09
182 changed files with 2508 additions and 809 deletions
+57 -530
View File
@@ -6,7 +6,9 @@
// ----------------------------------------------------------------------------- : Includes
#include <util/prec.hpp>
#include <gui/update_checker.hpp>
#include <gui/packages_window.hpp>
#include <data/settings.hpp>
#include <data/installer.hpp>
#include <util/io/package_manager.hpp>
@@ -17,26 +19,17 @@
#include <wx/dialup.h>
#include <wx/url.h>
#include <wx/html/htmlwin.h>
#include <wx/vlbox.h>
#include <wx/wfstream.h>
#include <wx/dir.h>
#include <list>
#include <set>
#include <iostream>
DECLARE_POINTER_TYPE(PackageVersionData);
DECLARE_POINTER_TYPE(Installer);
//%DECLARE_POINTER_TYPE(PackageVersionData);
DECLARE_POINTER_TYPE(VersionData);
DECLARE_TYPEOF_COLLECTION(PackageVersionDataP);
DECLARE_TYPEOF_COLLECTION(PackageDependencyP);
DECLARE_TYPEOF(list<PackageVersionDataP>);
DECLARE_TYPEOF(set<String>);
// ----------------------------------------------------------------------------- : Update data
/*
/// Information on available packages
class PackageVersionData : public IntrusivePtrBase<PackageVersionData> {
class PackageVersionData : public IntrusivePtrVirtualBase {
public:
PackageVersionData() {}
@@ -62,23 +55,39 @@ class VersionData : public IntrusivePtrBase<VersionData> {
DECLARE_REFLECTION();
};
*/
IMPLEMENT_REFLECTION(PackageVersionData) {
REFLECT(name);
REFLECT(type);
REFLECT(display_name);
REFLECT(description);
REFLECT(url);
REFLECT(version);
REFLECT(app_version);
REFLECT_N("depends ons", depends);
/// Information on the latest available versions
class VersionData : public IntrusivePtrBase<VersionData> {
public:
vector<PackageDependencyP> packages; ///< Available packages + versions
String new_updates_url; ///< updates url has moved?
DECLARE_REFLECTION();
};
/*
IMPLEMENT_REFLECTION_NO_SCRIPT(PackageVersionData) {
REFLECT_NO_SCRIPT(name);
REFLECT_NO_SCRIPT(type);
REFLECT_NO_SCRIPT(display_name);
REFLECT_NO_SCRIPT(description);
REFLECT_NO_SCRIPT(url);
REFLECT_NO_SCRIPT(version);
REFLECT_NO_SCRIPT(app_version);
REFLECT_NO_SCRIPT_N("depends ons", depends);
}
IMPLEMENT_REFLECTION(VersionData) {
REFLECT(version);
REFLECT(description);
REFLECT(new_updates_url);
REFLECT(packages);
IMPLEMENT_REFLECTION_NO_SCRIPT(VersionData) {
REFLECT_NO_SCRIPT(version);
REFLECT_NO_SCRIPT(description);
REFLECT_NO_SCRIPT(new_updates_url);
REFLECT_NO_SCRIPT(packages);
}*/
IMPLEMENT_REFLECTION_NO_SCRIPT(VersionData) {
REFLECT_NO_SCRIPT(packages);
REFLECT_NO_SCRIPT(new_updates_url);
}
// The information for the latest version
@@ -89,7 +98,18 @@ bool shown_dialog = false;
volatile bool checking_updates = false;
bool update_data_found() { return !!update_version_data; }
bool update_available() { return update_version_data && update_version_data->version > app_version; }
bool update_available() {
if (!update_version_data) return false;
// updates to any installed package?
FOR_EACH_CONST(p, update_version_data->packages) {
if (!settings.check_updates_all && p->package != mse_package) continue;
Version v;
if (packages.installedVersion(p->package, v) && v < p->version) {
return true;
}
}
return false;
}
// ----------------------------------------------------------------------------- : Update checking
@@ -113,17 +133,18 @@ class CheckUpdateThread : public wxThread {
if (checking_updates) return; // don't check multiple times simultaniously
checking_updates = true;
try {
wxURL url(settings.updates_url);
wxURL url(settings.package_versions_url);
wxInputStream* isP = url.GetInputStream();
if (!isP) return; // failed to get data
InputStreamP is(isP);
// Read version data
// ignore errors for forwards compatability
VersionDataP version_data;
Reader reader(is, nullptr, _("updates"));
Reader reader(is, nullptr, _("updates"), true);
reader.handle(version_data);
// has the updates url changed?
if (!version_data->new_updates_url.empty()) {
settings.updates_url = version_data->new_updates_url;
settings.package_versions_url = version_data->new_updates_url;
}
// Make available
update_version_data = version_data;
@@ -174,11 +195,14 @@ struct HtmlWindowToBrowser : public wxHtmlWindow {
void show_update_dialog(Window* parent) {
if (!update_available() || shown_dialog) return; // we already have the latest version, or this has already been displayed.
shown_dialog = true;
(new PackagesWindow(parent))->Show();
/*
// Show update dialog
wxDialog* dlg = new wxDialog(parent, wxID_ANY, _TITLE_("updates available"), wxDefaultPosition);
// controls
wxHtmlWindow* html = new HtmlWindowToBrowser(dlg, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_AUTO | wxSUNKEN_BORDER);
html->SetPage(update_version_data->description);
//% html->SetPage(update_version_data->description);
wxButton* close = new wxButton(dlg, wxID_OK, _BUTTON_("close"));
close->SetDefault();
// layout
@@ -189,502 +213,5 @@ void show_update_dialog(Window* parent) {
dlg->SetSize(400,400);
dlg->Show();
// And never show it again this run
shown_dialog = true;
*/
}
// ----------------------------------------------------------------------------- : PackageUpdateList
class PackageUpdateList : public wxVListBox {
public:
PackageUpdateList(UpdatesWindow* parent)
: wxVListBox (parent, ID_PACKAGE_LIST, wxDefaultPosition, wxSize(540,210), wxNO_BORDER | wxVSCROLL)
, parent(parent)
{
if (!checking_updates && !update_version_data) {
check_updates_now(true);
}
SetItemCount(update_version_data ? update_version_data->packages.size() : 1);
}
virtual void OnDrawItem(wxDC& dc, const wxRect& rect, size_t n) const {
static wxBrush greyBrush(Color(224,224,224));
if (checking_updates) {
String text = _ERROR_("checking updates");
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(greyBrush);
dc.DrawRectangle(rect);
int w, h;
dc.GetTextExtent(text, &w, &h);
dc.DrawText(text, rect.GetLeft() + (rect.GetWidth() - w) / 2, rect.GetTop() + (rect.GetHeight() - h) / 2);
} else if (!update_version_data || update_version_data->packages.empty()) {
String text = _ERROR_("no packages");
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(greyBrush);
dc.DrawRectangle(rect);
int w, h;
dc.GetTextExtent(text, &w, &h);
dc.DrawText(text, rect.GetLeft() + (rect.GetWidth() - w) / 2, rect.GetTop() + (rect.GetHeight() - h) / 2);
} else {
static wxBrush darkBrush(Color(192,224,255));
static wxBrush selectBrush(Color(96,96,192));
PackageVersionDataP pack = update_version_data->packages[n];
UpdatesWindow::PackageStatus status = parent->package_data[pack].first;
UpdatesWindow::PackageAction action = parent->package_data[pack].second;
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(IsSelected(n) ? selectBrush : (n % 2 ? *wxWHITE_BRUSH : darkBrush));
dc.DrawRectangle(rect);
// These two arrays correspond to PackageStatus and PackageAction, respectively
static Color status_colors [] = {
Color(32,160,32)
,Color(32,32,255)
,Color(192,32,32)
};
static Color action_colors [] = {
Color(32,160,32)
,Color(192,32,32)
,Color(192,192,32)
,Color(32,32,255)
,Color(32,32,32)
};
// Ditto here (these are the locale names)
static String status_texts [] = {
_TYPE_("installed")
,_TYPE_("uninstalled")
,_TYPE_("upgradeable")
};
static String action_texts [] = {
_TYPE_("install")
,_TYPE_("uninstall")
,_TYPE_("upgrade")
,_TYPE_("do nothing")
,_TYPE_("new mse")
};
static Color packageFront(64,64,64);
#define SELECT_WHITE(color) (IsSelected(n) ? *wxWHITE : color)
dc.SetClippingRegion(wxRect(rect.x, rect.y, 180, rect.height));
dc.SetTextForeground(SELECT_WHITE(packageFront));
dc.DrawText(pack->display_name, rect.GetLeft() + 1, rect.GetTop());
dc.DestroyClippingRegion();
dc.SetClippingRegion(wxRect(rect.x + 180, rect.y, 120, rect.height));
dc.DrawText(pack->type, rect.GetLeft() + 180, rect.GetTop());
dc.DestroyClippingRegion();
dc.SetTextForeground(SELECT_WHITE(status_colors[status]));
dc.DrawText(status_texts[status], rect.GetLeft() + 300, rect.GetTop());
dc.SetTextForeground(SELECT_WHITE(action_colors[action]));
dc.DrawText(action_texts[action], rect.GetLeft() + 420, rect.GetTop());
#undef SELECT_WHITE
}
}
virtual wxCoord OnMeasureItem(size_t) const {
return (update_version_data && !update_version_data->packages.empty()) ? 15 : 210;
}
void onUpdateCheckingFinished(wxEvent& event) {
SetItemCount(update_version_data ? update_version_data->packages.size() : 1);
}
virtual wxCoord EstimateTotalHeight() const {
return (update_version_data && !update_version_data->packages.empty())
? 15 * (int)update_version_data->packages.size()
: 210;
}
private:
DECLARE_EVENT_TABLE()
UpdatesWindow* parent;
};
BEGIN_EVENT_TABLE(PackageUpdateList, wxVListBox)
EVT_CUSTOM(UPDATE_CHECK_FINISHED_EVT, wxID_ANY, PackageUpdateList::onUpdateCheckingFinished)
END_EVENT_TABLE()
// ----------------------------------------------------------------------------- : RecursiveDelete
// Move somewhere better?
class RecursiveDeleter : public wxDirTraverser {
public:
set<String> to_delete;
String start_dir;
RecursiveDeleter (String start)
: start_dir (start)
{
to_delete.insert(start_dir);
}
wxDirTraverseResult OnFile(const String& filename) {
if (!wxRemoveFile(filename))
handle_error(_("Cannot delete ") + filename + _(". ")
_("The remainder of the package has still been removed, if possible.")
_("Other packages may have been removed, including packages that this on is dependent on. Please remove manually."));
return wxDIR_CONTINUE;
}
wxDirTraverseResult OnDir(const String& dirname) {
to_delete.insert(dirname);
return wxDIR_CONTINUE;
}
void finishDelete() {
FOR_EACH_REVERSE(dir, to_delete) {
wxRmdir(dir);
}
}
};
// ----------------------------------------------------------------------------- : UpdateWindow
UpdatesWindow::UpdatesWindow()
: Frame(nullptr, wxID_ANY, _TITLE_("package list"), wxDefaultPosition, wxSize(540,440), wxDEFAULT_DIALOG_STYLE | wxCLIP_CHILDREN)
{
SetIcon(wxIcon());
wxBoxSizer *v = new wxBoxSizer(wxVERTICAL);
wxBoxSizer *h1 = new wxBoxSizer(wxHORIZONTAL);
wxBoxSizer *h2 = new wxBoxSizer(wxHORIZONTAL);
wxBoxSizer *h3 = new wxBoxSizer(wxHORIZONTAL);
package_list = new PackageUpdateList(this);
description_window = new HtmlWindowToBrowser(this, wxID_ANY, wxDefaultPosition, wxSize(540,100), wxHW_SCROLLBAR_AUTO | wxSUNKEN_BORDER);
setDefaultPackageStatus();
package_title = new wxStaticText(this, wxID_ANY, _TITLE_("package name"), wxDefaultPosition, wxSize(180,15), wxALIGN_LEFT | wxST_NO_AUTORESIZE);
type_title = new wxStaticText(this, wxID_ANY, _TITLE_("package type"), wxDefaultPosition, wxSize(120,15), wxALIGN_LEFT | wxST_NO_AUTORESIZE);
status_title = new wxStaticText(this, wxID_ANY, _TITLE_("package status"), wxDefaultPosition, wxSize(120,15), wxALIGN_LEFT | wxST_NO_AUTORESIZE);
new_title = new wxStaticText(this, wxID_ANY, _TITLE_("new status"), wxDefaultPosition, wxSize(120,15), wxALIGN_LEFT | wxST_NO_AUTORESIZE);
h1->Add(package_title, 3);
h1->Add(type_title, 2);
h1->Add(status_title, 2);
h1->Add(new_title, 2);
(install_button = new wxButton(this, ID_INSTALL, _MENU_("install package")))->Disable();
(upgrade_button = new wxButton(this, ID_UPGRADE, _MENU_("upgrade package")))->Disable();
(remove_button = new wxButton(this, ID_REMOVE, _MENU_("remove package")))->Disable();
(cancel_button = new wxButton(this, ID_CANCEL, _MENU_("cancel changes")))->Disable();
apply_button = new wxButton(this, ID_APPLY, _MENU_("apply changes"));
h2->AddStretchSpacer();
h2->Add(install_button);
h2->AddStretchSpacer();
h2->Add(upgrade_button);
h2->AddStretchSpacer();
h2->Add(remove_button);
h2->AddStretchSpacer();
h2->Add(cancel_button);
h2->AddStretchSpacer();
h3->AddStretchSpacer();
h3->Add(apply_button);
h3->AddStretchSpacer();
v->Add(h1);
v->Add(package_list);
v->AddStretchSpacer(2);
v->Add(description_window);
v->AddStretchSpacer(2);
v->Add(h2);
v->AddStretchSpacer(1);
v->Add(h3);
v->AddStretchSpacer(2);
SetSizer(v);
}
UpdatesWindow::~UpdatesWindow() {
(new WelcomeWindow)->Show();
}
void UpdatesWindow::onUpdateCheckFinished(wxCommandEvent&) {
setDefaultPackageStatus();
}
void UpdatesWindow::onPackageSelect(wxCommandEvent& ev) {
updateButtons(ev.GetInt());
}
void UpdatesWindow::onActionChange(wxCommandEvent& ev) {
PackageVersionDataP pack = update_version_data->packages[package_list->GetSelection()];
PackageAction& action = package_data[pack].second;
switch (ev.GetId()) {
case ID_INSTALL:
action = ACTION_INSTALL;
SelectPackageDependencies(pack);
break;
case ID_REMOVE:
action = ACTION_UNINSTALL;
RemovePackageDependencies(pack);
break;
case ID_UPGRADE:
action = ACTION_UPGRADE;
SelectPackageDependencies(pack);
break;
case ID_CANCEL:
switch (package_data[pack].first) {
case STATUS_INSTALLED:
SelectPackageDependencies(pack);
break;
case STATUS_NOT_INSTALLED:
RemovePackageDependencies(pack);
break;
case STATUS_UPGRADEABLE:
if (action == ACTION_UPGRADE)
DowngradePackageDependencies(pack);
else
SelectPackageDependencies(pack);
break;
}
action = (pack->app_version > file_version) ? ACTION_NEW_MSE : ACTION_NOTHING;
break;
}
updateButtons(package_list->GetSelection());
package_list->Refresh();
}
void UpdatesWindow::onApplyChanges(wxCommandEvent& ev) {
list<PackageVersionDataP> to_install, to_remove;
FOR_EACH(pack, update_version_data->packages) {
switch (package_data[pack].second) {
case ACTION_INSTALL:
to_install.push_back(pack);
break;
case ACTION_UPGRADE:
to_install.push_back(pack);
case ACTION_UNINSTALL:
to_remove.push_back(pack);
default:;
}
}
FOR_EACH(pack, to_remove) {
String filename = packages.openAny(pack->name, true)->absoluteFilename();
if (wxDirExists(filename)) {
wxDir dir(filename);
RecursiveDeleter rd (filename);
dir.Traverse(rd);
rd.finishDelete();
} else {
if (!wxRemoveFile(filename))
handle_error(_("Cannot delete ") + filename + _(" to remove package ") + pack->name + _(". ")
_("Other packages may have been removed, including packages that this on is dependent on. Please remove manually."));
}
}
FOR_EACH(pack, to_install) {
wxURL url(pack->url);
wxInputStream* is = url.GetInputStream();
if (!is) {
handle_error(_("Cannot fetch file ") + pack->url + _(" to install package ") + pack->name + _(". ")
_("Other packages may have been installed, including packages that depend on this one. ")
_("Please remove those packages manually or install this one manually."));
}
wxString filename = wxFileName::CreateTempFileName(_(""));
wxFileOutputStream os (filename);
os.Write(*is);
os.Close();
InstallerP inst(new Installer);
inst->open(filename);
inst->install(isInstallLocal(settings.install_type), false);
delete is;
wxRemoveFile(filename);
}
setDefaultPackageStatus();
updateButtons(package_list->GetSelection());
package_list->Refresh();
handle_pending_errors();
packages.clearPackageCache();
}
void UpdatesWindow::updateButtons(int id) {
PackageVersionDataP pack = update_version_data->packages[id];
description_window->SetPage(pack->description);
PackageStatus status = package_data[pack].first;
PackageAction action = package_data[pack].second;
if (action == ACTION_NEW_MSE) {
install_button->Disable();
remove_button->Enable(status != STATUS_NOT_INSTALLED);
upgrade_button->Disable();
cancel_button->Disable();
} else if (status == STATUS_INSTALLED) {
install_button->Disable();
remove_button->Enable(action != ACTION_UNINSTALL);
upgrade_button->Disable();
cancel_button->Enable(action == ACTION_UNINSTALL);
} else if (status == STATUS_NOT_INSTALLED) {
install_button->Enable(action != ACTION_INSTALL);
remove_button->Disable();
upgrade_button->Disable();
cancel_button->Enable(action == ACTION_INSTALL);
} else /* status == STATUS_UPGRADEABLE */ {
install_button->Disable();
remove_button->Enable(action != ACTION_UNINSTALL);
upgrade_button->Enable(action != ACTION_UPGRADE);
cancel_button->Enable(action != ACTION_NOTHING);
}
}
void UpdatesWindow::setDefaultPackageStatus() {
if (!update_version_data) return;
FOR_EACH(p, update_version_data->packages) {
PackagedP pack;
try { pack = packages.openAny(p->name, true); }
catch (PackageNotFoundError&) { } // We couldn't open a package... no cause for alarm
if (!pack || !(wxFileExists(pack->absoluteFilename()) || wxDirExists(pack->absoluteFilename()))) {
// not installed
if (p->app_version > file_version) {
package_data[p] = PackageData(STATUS_NOT_INSTALLED, ACTION_NEW_MSE);
} else {
package_data[p] = PackageData(STATUS_NOT_INSTALLED, ACTION_NOTHING);
}
} else if (pack->version < p->version) {
// newer version
if (p->app_version > file_version) {
package_data[p] = PackageData(STATUS_UPGRADEABLE, ACTION_NEW_MSE);
} else {
package_data[p] = PackageData(STATUS_UPGRADEABLE, ACTION_UPGRADE);
}
} else {
package_data[p] = PackageData(STATUS_INSTALLED, ACTION_NOTHING);
}
}
}
/// Select the dependencies for a package
/**
* \param pack The package data to check dependencies for.
*
* This function will select all the dependencies for the package, to ensure
* that the user can't install a package without it's dependencies.
*/
void UpdatesWindow::SelectPackageDependencies (PackageVersionDataP pack) {
FOR_EACH(dep, pack->depends) {
if (packages.checkDependency(*dep, false)) //It's already installed.
continue;
FOR_EACH(p, update_version_data->packages) {
if (p->name == dep->package) { //We have a match.
if (p->version >= dep->version) { //Versions line up
PackageStatus& status = package_data[p].first;
PackageAction& action = package_data[p].second;
if (status == STATUS_NOT_INSTALLED)
action = ACTION_INSTALL;
else if (status == STATUS_UPGRADEABLE)
action = ACTION_UPGRADE;
else //status == STATUS_INSTALLED
action = ACTION_NOTHING;
break;
}
}
}
// TODO: Decide what to do if a dependency can't be met.
// It shouldn't happen with a decently maintained updates list.
// But it could, and we need to decide what to do in this situation.
}
}
/// This finds all packages that depend on the one provided and marks them for removal.
void UpdatesWindow::RemovePackageDependencies (PackageVersionDataP pack) {
FOR_EACH(p, update_version_data->packages) {
FOR_EACH(dep, p->depends) {
if (pack->name == dep->package) {
PackageStatus& status = package_data[p].first;
PackageAction& action = package_data[p].second;
if (status != STATUS_NOT_INSTALLED)
action = ACTION_UNINSTALL;
else // status == STATUS_NOT_INSTALLED
action = p->app_version > file_version ? ACTION_NEW_MSE : ACTION_NOTHING;
break;
}
}
}
}
/// This deals with the complexities of a downgrade and its dependencies.
void UpdatesWindow::DowngradePackageDependencies (PackageVersionDataP pack) {
PackagedP old_pack = packages.openAny(pack->name, true);
FOR_EACH(dep, old_pack->dependencies) {
// dependencies the old version has, but the new one might not.
if (packages.checkDependency(*dep, false)) //It's already installed.
continue;
FOR_EACH(p, update_version_data->packages) {
if (p->name == dep->package) { //We have a match.
if (p->version >= dep->version) { //Versions line up
if (p->app_version > file_version) //We can't install this
continue;
PackageStatus& status = package_data[p].first;
PackageAction& action = package_data[p].second;
if (status == STATUS_NOT_INSTALLED)
action = ACTION_INSTALL;
else if (status == STATUS_UPGRADEABLE)
action = ACTION_UPGRADE;
break;
}
}
}
// TODO: Decide what to do if a dependency can't be met.
// It shouldn't happen with a decently maintained updates list.
// But it could, and we need to decide what to do in this situation.
// Ideally, some sort of error should occur, such that we don't have packages
// with unmet dependencies.
}
FOR_EACH(p, update_version_data->packages) {
// dependencies that can no longer be met.
FOR_EACH(dep, p->depends) {
if (pack->name == dep->package) {
if (old_pack->version > dep->version) {
PackageStatus& status = package_data[p].first;
PackageAction& action = package_data[p].second;
if (status != STATUS_NOT_INSTALLED)
action = ACTION_UNINSTALL;
else // status == STATUS_NOT_INSTALLED
action = p->app_version > file_version ? ACTION_NEW_MSE : ACTION_NOTHING;
break;
}
}
}
}
}
BEGIN_EVENT_TABLE(UpdatesWindow, Frame)
EVT_COMMAND(wxID_ANY, UPDATE_CHECK_FINISHED_EVT, UpdatesWindow::onUpdateCheckFinished)
EVT_LISTBOX(ID_PACKAGE_LIST, UpdatesWindow::onPackageSelect)
EVT_BUTTON(ID_INSTALL, UpdatesWindow::onActionChange)
EVT_BUTTON(ID_REMOVE, UpdatesWindow::onActionChange)
EVT_BUTTON(ID_UPGRADE, UpdatesWindow::onActionChange)
EVT_BUTTON(ID_CANCEL, UpdatesWindow::onActionChange)
EVT_BUTTON(ID_APPLY, UpdatesWindow::onApplyChanges)
END_EVENT_TABLE()