//
// aventreectrl.cc
//
// Tree control used for the survey tree.
//
// Copyright (C) 2001, Mark R. Shinwell.
// Copyright (C) 2001-2003,2005,2006,2016,2018,2025 Olly Betts
// Copyright (C) 2005 Martin Green
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, see
// .
//
#include
#include "aventreectrl.h"
#include "mainfrm.h"
#include
#include
using namespace std;
// STATE_BLANK is used for stations which are siblings of surveys which have
// select checkboxes.
enum { STATE_BLANK = 0, STATE_OFF, STATE_ON };
/* XPM */
static const char *blank_xpm[] = {
/* columns rows colors chars-per-pixel */
"15 15 1 1",
" c None",
/* pixels */
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" "
};
/* XPM */
static const char *off_xpm[] = {
/* columns rows colors chars-per-pixel */
"15 15 2 1",
". c #000000",
" c None",
/* pixels */
" ",
" ",
" ............ ",
" . . ",
" . . ",
" . . ",
" . . ",
" . . ",
" . . ",
" . . ",
" . . ",
" . . ",
" . . ",
" ............ ",
" "
};
/* XPM */
static const char *on_xpm[] = {
/* columns rows colors chars-per-pixel */
"15 15 3 1",
". c #000000",
"X c #007F28",
" c None",
/* pixels */
" ",
" ",
" ............XX",
" . XXX",
" . XXXX",
" . XXXX ",
" . XXXX ",
" . XXXX. ",
" . XX XXXX . ",
" . XXXXXXX . ",
" . XXXXX . ",
" . XXX . ",
" . X . ",
" ............ ",
" "
};
BEGIN_EVENT_TABLE(AvenTreeCtrl, wxTreeCtrl)
EVT_MOTION(AvenTreeCtrl::OnMouseMove)
EVT_LEAVE_WINDOW(AvenTreeCtrl::OnLeaveWindow)
EVT_TREE_SEL_CHANGED(wxID_ANY, AvenTreeCtrl::OnSelChanged)
EVT_TREE_ITEM_ACTIVATED(wxID_ANY, AvenTreeCtrl::OnItemActivated)
EVT_CHAR(AvenTreeCtrl::OnKeyPress)
EVT_TREE_ITEM_MENU(wxID_ANY, AvenTreeCtrl::OnMenu)
EVT_MENU(menu_SURVEY_SHOW_ALL, AvenTreeCtrl::OnRestrict)
EVT_MENU(menu_SURVEY_RESTRICT, AvenTreeCtrl::OnRestrict)
EVT_MENU(menu_SURVEY_HIDE, AvenTreeCtrl::OnHide)
EVT_MENU(menu_SURVEY_SHOW, AvenTreeCtrl::OnShow)
EVT_MENU(menu_SURVEY_HIDE_SIBLINGS, AvenTreeCtrl::OnHideSiblings)
EVT_MENU(wxID_FIND, AvenTreeCtrl::OnFind)
EVT_TREE_STATE_IMAGE_CLICK(wxID_ANY, AvenTreeCtrl::OnStateClick)
END_EVENT_TABLE()
AvenTreeCtrl::AvenTreeCtrl(MainFrm* parent, wxWindow* window_parent) :
wxTreeCtrl(window_parent, wxID_ANY, wxDefaultPosition, wxDefaultSize,
wxTR_DEFAULT_STYLE | wxTR_HIDE_ROOT),
m_Parent(parent)
{
wxImageList* img_list = new wxImageList(15, 15, 2);
img_list->Add(wxBitmap(blank_xpm));
img_list->Add(wxBitmap(off_xpm));
img_list->Add(wxBitmap(on_xpm));
AssignStateImageList(img_list);
}
void AvenTreeCtrl::FillTree(const wxString& root_name)
{
Freeze();
m_Enabled = false;
m_LastItem = wxTreeItemId();
m_SelValid = false;
DeleteAllItems();
const wxChar separator = m_Parent->GetSeparator();
filter.clear();
filter.SetSeparator(separator);
// Create the (hidden) real root of the wxTreeCtrl.
wxTreeItemId treeroot = AddRoot(wxString());
// Create the root of the survey tree.
wxTreeItemId surveyroot = AppendItem(treeroot, root_name);
// Fill the tree of stations and prefixes.
stack previous_ids;
wxString current_prefix;
wxTreeItemId current_id = surveyroot;
list::const_iterator pos = m_Parent->GetLabels();
while (pos != m_Parent->GetLabelsEnd()) {
LabelInfo* label = *pos++;
if (label->IsAnon()) continue;
// Determine the current prefix.
wxString prefix = label->GetText().BeforeLast(separator);
// Determine if we're still on the same prefix.
if (prefix == current_prefix) {
// no need to fiddle with branches...
}
// If not, then see if we've descended to a new prefix.
else if (prefix.length() > current_prefix.length() &&
prefix.StartsWith(current_prefix) &&
(prefix[current_prefix.length()] == separator ||
current_prefix.empty())) {
// We have, so start as many new branches as required.
int current_prefix_length = current_prefix.length();
current_prefix = prefix;
size_t next_dot = current_prefix_length;
if (!next_dot) --next_dot;
do {
size_t prev_dot = next_dot + 1;
// Extract the next bit of prefix.
next_dot = prefix.find(separator, prev_dot + 1);
wxString bit = prefix.substr(prev_dot, next_dot - prev_dot);
// Sigh, therion can produce files with empty components in
// station names!
// assert(!bit.empty());
// Add the current tree ID to the stack.
previous_ids.push(current_id);
// Append the new item to the tree and set this as the current branch.
current_id = AppendItem(current_id, bit);
SetItemData(current_id, new TreeData(prefix.substr(0, next_dot)));
} while (next_dot != wxString::npos);
}
// Otherwise, we must have moved up, and possibly then down again.
else {
size_t count = 0;
bool ascent_only = (prefix.length() < current_prefix.length() &&
current_prefix.StartsWith(prefix) &&
(current_prefix[prefix.length()] == separator ||
prefix.empty()));
if (!ascent_only) {
// Find out how much of the current prefix and the new prefix
// are the same.
// Note that we require a match of a whole number of parts
// between dots!
size_t n = min(prefix.length(), current_prefix.length());
size_t i;
for (i = 0; i < n && prefix[i] == current_prefix[i]; ++i) {
if (prefix[i] == separator) count = i + 1;
}
} else {
count = prefix.length() + 1;
}
// Extract the part of the current prefix after the bit (if any)
// which has matched.
// This gives the prefixes to ascend over.
wxString prefixes_ascended = current_prefix.substr(count);
// Count the number of prefixes to ascend over.
int num_prefixes = prefixes_ascended.Freq(separator);
// Reverse up over these prefixes.
for (int i = 1; i <= num_prefixes; i++) {
previous_ids.pop();
}
current_id = previous_ids.top();
previous_ids.pop();
if (!ascent_only) {
// Add branches for this new part.
size_t next_dot = count - 1;
do {
size_t prev_dot = next_dot + 1;
// Extract the next bit of prefix.
next_dot = prefix.find(separator, prev_dot + 1);
wxString bit = prefix.substr(prev_dot, next_dot - prev_dot);
// Sigh, therion can produce files with empty components in
// station names!
// assert(!bit.empty());
// Add the current tree ID to the stack.
previous_ids.push(current_id);
// Append the new item to the tree and set this as the current branch.
current_id = AppendItem(current_id, bit);
SetItemData(current_id, new TreeData(prefix.substr(0, next_dot)));
} while (next_dot != wxString::npos);
}
current_prefix = prefix;
}
// Now add the leaf.
wxString bit = label->GetText().AfterLast(separator);
// Sigh, therion can produce files with empty components in station
// names!
// assert(!bit.empty());
wxTreeItemId id = AppendItem(current_id, bit);
SetItemData(id, new TreeData(label));
label->tree_id = id;
// Set the colour for an item in the survey tree.
if (label->IsEntrance()) {
// Entrances are green (like entrance blobs).
SetItemTextColour(id, wxColour(0, 255, 40));
} else if (label->IsSurface()) {
// Surface stations are dark green.
SetItemTextColour(id, wxColour(49, 158, 79));
}
}
Expand(surveyroot);
m_Enabled = true;
Thaw();
}
constexpr auto TREE_MASK = wxTREE_HITTEST_ONITEMLABEL |
wxTREE_HITTEST_ONITEMRIGHT |
wxTREE_HITTEST_ONITEMSTATEICON;
void AvenTreeCtrl::OnMouseMove(wxMouseEvent& event)
{
if (!m_Enabled || m_Parent->Animating())
return;
int flags;
wxTreeItemId pos = HitTest(event.GetPosition(), flags);
if (!(flags & TREE_MASK)) {
pos = wxTreeItemId();
}
if (pos == m_LastItem) return;
if (pos.IsOk()) {
const TreeData* data = static_cast(GetItemData(pos));
m_Parent->DisplayTreeInfo(data);
if (data && !data->IsStation()) {
// For stations, MainFrm calls back to SetHere(), but for surveys
// we need to do that ourselves.
SetHere(pos);
}
} else {
m_Parent->DisplayTreeInfo();
}
}
void AvenTreeCtrl::SetHere(wxTreeItemId pos)
{
if (pos == m_LastItem) return;
if (m_LastItem.IsOk()) {
SetItemBackgroundColour(m_LastItem, m_BackgroundColour);
}
if (pos.IsOk()) {
m_BackgroundColour = GetItemBackgroundColour(pos);
SetItemBackgroundColour(pos, wxColour(180, 180, 180));
}
m_LastItem = pos;
}
void AvenTreeCtrl::OnLeaveWindow(wxMouseEvent&)
{
if (m_LastItem.IsOk()) {
SetItemBackgroundColour(m_LastItem, m_BackgroundColour);
m_LastItem = wxTreeItemId();
}
m_Parent->DisplayTreeInfo();
}
void AvenTreeCtrl::OnSelChanged(wxTreeEvent&)
{
m_SelValid = true;
}
void AvenTreeCtrl::OnItemActivated(wxTreeEvent& e)
{
if (!m_Enabled) return;
m_Parent->TreeItemSelected(GetItemData(e.GetItem()));
}
void AvenTreeCtrl::OnMenu(wxTreeEvent& e)
{
if (!m_Enabled) return;
const TreeData* data = static_cast(GetItemData(e.GetItem()));
menu_data = data;
menu_item = e.GetItem();
if (!data) {
// Survey tree root:
wxMenu menu;
/* TRANSLATORS: In aven's survey tree, right-clicking on the root
* gives a pop-up menu and this is an option (but only enabled if
* the view is restricted to a subsurvey). It reloads the current
* survey file with the who survey visible.
*/
menu.Append(menu_SURVEY_SHOW_ALL, wmsg(/*Show all*/245));
if (m_Parent->GetSurvey().empty())
menu.Enable(menu_SURVEY_SHOW_ALL, false);
PopupMenu(&menu);
} else if (data->IsStation()) {
// Station: name is data->GetLabel()->GetText()
wxMenu menu;
// TRANSLATORS: Menu item in right-click menu in survey tree.
menu.Append(wxID_FIND, wmsg(/*Find*/332));
PopupMenu(&menu);
} else if (ItemHasChildren(menu_item)) {
// Survey:
wxMenu menu;
/* TRANSLATORS: In aven's survey tree, right-clicking on a survey
* name gives a pop-up menu and this is an option. It reloads the
* current survey file with the view restricted to the survey
* clicked upon.
*/
menu.Append(menu_SURVEY_RESTRICT, wmsg(/*Hide others*/246));
menu.AppendSeparator();
//menu.Append(menu_SURVEY_HIDE, wmsg(/*&Hide*/407));
menu.Append(menu_SURVEY_SHOW, wmsg(/*&Show*/409));
//menu.Append(menu_SURVEY_HIDE_SIBLINGS, wmsg(/*Hide si&blings*/388));
switch (GetItemState(menu_item)) {
case STATE_ON: // Currently shown.
menu.Enable(menu_SURVEY_SHOW, false);
break;
#if 0
case STATE_HIDDEN: // Currently hidden.
menu.Enable(menu_SURVEY_RESTRICT, false);
menu.Enable(menu_SURVEY_HIDE, false);
menu.Enable(menu_SURVEY_HIDE_SIBLINGS, false);
break;
case STATE_OFF:
menu.Enable(menu_SURVEY_HIDE, false);
menu.Enable(menu_SURVEY_HIDE_SIBLINGS, false);
break;
#endif
}
menu.Append(wxID_FIND, wmsg(/*Find*/332));
PopupMenu(&menu);
} else {
// Overlay - FIXME: menu here?
}
menu_data = NULL;
e.Skip();
}
bool AvenTreeCtrl::GetSelectionData(wxTreeItemData** data) const
{
assert(m_Enabled);
assert(data);
if (!m_SelValid) {
return false;
}
wxTreeItemId id = GetSelection();
if (id.IsOk()) {
*data = GetItemData(id);
}
return id.IsOk() && *data;
}
void AvenTreeCtrl::UnselectAll()
{
m_SelValid = false;
wxTreeCtrl::UnselectAll();
}
void AvenTreeCtrl::OnKeyPress(wxKeyEvent &e)
{
switch (e.GetKeyCode()) {
case WXK_ESCAPE:
m_Parent->ClearTreeSelection();
break;
case WXK_RETURN: {
wxTreeItemId id = GetSelection();
if (id.IsOk()) {
if (ItemHasChildren(id)) {
// If on a branch, expand/contract it.
if (IsExpanded(id)) {
Collapse(id);
} else {
Expand(id);
}
} else {
// If on a station, centre on it by selecting it twice.
m_Parent->TreeItemSelected(GetItemData(id));
m_Parent->TreeItemSelected(GetItemData(id));
}
}
break;
}
case WXK_LEFT: case WXK_RIGHT: case WXK_UP: case WXK_DOWN:
case WXK_HOME: case WXK_END: case WXK_PAGEUP: case WXK_PAGEDOWN:
e.Skip();
break;
default:
// Pass key event to MainFrm which will pass to GfxCore which will
// pass to GUIControl.
m_Parent->OnKeyPress(e);
break;
}
}
void AvenTreeCtrl::OnRestrict(wxCommandEvent&)
{
m_Parent->RestrictTo(menu_data && menu_data->IsSurvey() ? menu_data->GetSurvey() : wxString());
// FIXME: Overlays
}
void AvenTreeCtrl::OnHide(wxCommandEvent&)
{
// Shouldn't be available for the root item.
wxASSERT(menu_data);
// Hide should be disabled unless the item is explicitly shown.
wxASSERT(GetItemState(menu_item) == STATE_ON);
SetItemState(menu_item, STATE_OFF);
// FIXME: Overlays?
filter.remove(menu_data->GetSurvey());
#if 0
Freeze();
// Show siblings if not already shown or hidden.
wxTreeItemId i = menu_item;
while ((i = GetPrevSibling(i)).IsOk()) {
if (GetItemState(i) == wxTREE_ITEMSTATE_NONE)
SetItemState(i, 1);
}
i = menu_item;
while ((i = GetNextSibling(i)).IsOk()) {
if (GetItemState(i) == wxTREE_ITEMSTATE_NONE)
SetItemState(i, 1);
}
Thaw();
#endif
m_Parent->ForceFullRedraw();
}
void AvenTreeCtrl::OnShow(wxCommandEvent&)
{
// Shouldn't be available for the root item.
wxASSERT(menu_data);
auto old_state = GetItemState(menu_item);
// Show should be disabled for an explicitly shown item.
wxASSERT(old_state != STATE_ON);
Freeze();
SetItemState(menu_item, STATE_ON);
// FIXME: Overlays?
filter.add(menu_data->GetSurvey());
if (old_state == wxTREE_ITEMSTATE_NONE) {
// Hide siblings if not already shown or hidden.
wxTreeItemId i = menu_item;
while ((i = GetPrevSibling(i)).IsOk()) {
if (GetItemState(i) == wxTREE_ITEMSTATE_NONE) {
const TreeData* data = static_cast(GetItemData(i));
SetItemState(i, data->IsStation() ? STATE_BLANK : STATE_OFF);
}
}
i = menu_item;
while ((i = GetNextSibling(i)).IsOk()) {
if (GetItemState(i) == wxTREE_ITEMSTATE_NONE) {
const TreeData* data = static_cast(GetItemData(i));
SetItemState(i, data->IsStation() ? STATE_BLANK : STATE_OFF);
}
}
}
Thaw();
m_Parent->ForceFullRedraw();
}
void AvenTreeCtrl::OnHideSiblings(wxCommandEvent&)
{
// Shouldn't be available for the root item.
wxASSERT(menu_data);
Freeze();
// FIXME: Overlays?
SetItemState(menu_item, STATE_ON);
filter.add(menu_data->GetSurvey());
wxTreeItemId i = menu_item;
while ((i = GetPrevSibling(i)).IsOk()) {
const TreeData* data = static_cast(GetItemData(i));
filter.remove(data->GetSurvey());
SetItemState(i, data->IsStation() ? STATE_BLANK : STATE_OFF);
}
i = menu_item;
while ((i = GetNextSibling(i)).IsOk()) {
const TreeData* data = static_cast(GetItemData(i));
filter.remove(data->GetSurvey());
SetItemState(i, data->IsStation() ? STATE_BLANK : STATE_OFF);
}
Thaw();
m_Parent->ForceFullRedraw();
}
void AvenTreeCtrl::OnFind(wxCommandEvent&)
{
// Shouldn't be available for the root item.
wxASSERT(menu_data);
m_Parent->TreeItemSearch(GetItemData(menu_item));
}
void AvenTreeCtrl::OnStateClick(wxTreeEvent& e)
{
auto item = e.GetItem();
const TreeData* data = static_cast(GetItemData(item));
switch (GetItemState(item)) {
case STATE_BLANK:
// Click on blank state icon for a station - let the tree handle
// this in the same way as a click on the label.
return;
case STATE_ON:
if (!ItemHasChildren(item)) {
// Overlay.
m_Parent->InvalidateOverlays();
} else {
// Survey.
if (data) filter.remove(data->GetSurvey());
}
SetItemState(item, STATE_OFF);
break;
case STATE_OFF:
if (!ItemHasChildren(item)) {
// Overlay.
m_Parent->InvalidateOverlays();
} else {
// Survey.
if (data) filter.add(data->GetSurvey());
}
SetItemState(item, STATE_ON);
break;
}
e.Skip();
m_Parent->ForceFullRedraw();
}
void AvenTreeCtrl::AddOverlay(const wxString& file)
{
char* leaf = leaf_from_fnm(file.utf8_str());
auto id = AppendItem(GetRootItem(), leaf);
free(leaf);
SetItemState(id, STATE_ON);
SetItemData(id, new TreeData(file));
}
void AvenTreeCtrl::RemoveOverlay(const wxString& file)
{
// If we add an overlay but fail to load it and remove it again, the
// overlay will be the last one, so search from the last one back.
for (auto item = GetLastChild(GetRootItem());
item.IsOk();
item = GetPrevSibling(item)) {
if (ItemHasChildren(item)) {
// Not an overlay.
continue;
}
const TreeData* data = static_cast(GetItemData(item));
if (data->GetSurvey() == file) {
Delete(item);
break;
}
}
}
wxTreeItemId AvenTreeCtrl::FirstOverlay()
{
wxTreeItemIdValue cookie;
auto item = GetFirstChild(GetRootItem(), cookie);
while (item.IsOk() &&
(ItemHasChildren(item) || GetItemState(item) != STATE_ON)) {
item = GetNextSibling(item);
}
return item;
}
wxTreeItemId AvenTreeCtrl::NextOverlay(wxTreeItemId item)
{
do {
item = GetNextSibling(item);
} while (item.IsOk() &&
(ItemHasChildren(item) || GetItemState(item) != STATE_ON));
return item;
}
wxTreeItemId AvenTreeCtrl::RemoveOverlay(wxTreeItemId id)
{
wxTreeItemId item = NextOverlay(id);
Delete(id);
return item;
}
const wxString& AvenTreeCtrl::GetOverlayFilename(wxTreeItemId item)
{
if (ItemHasChildren(item)) {
not_an_overlay:
static const wxString empty_string;
return empty_string;
}
const TreeData* data = static_cast(GetItemData(item));
if (!data) goto not_an_overlay;
return data->GetSurvey();
}