source: git/src/aventreectrl.cc @ 6b4d2e9

stereo-2025
Last change on this file since 6b4d2e9 was a49a80c0, checked in by Olly Betts <olly@…>, 4 months ago

Clean up inclusion of osalloc.h

  • Property mode set to 100644
File size: 18.4 KB
Line 
1//
2//  aventreectrl.cc
3//
4//  Tree control used for the survey tree.
5//
6//  Copyright (C) 2001, Mark R. Shinwell.
7//  Copyright (C) 2001-2003,2005,2006,2016,2018,2025 Olly Betts
8//  Copyright (C) 2005 Martin Green
9//
10//  This program is free software; you can redistribute it and/or modify
11//  it under the terms of the GNU General Public License as published by
12//  the Free Software Foundation; either version 2 of the License, or
13//  (at your option) any later version.
14//
15//  This program is distributed in the hope that it will be useful,
16//  but WITHOUT ANY WARRANTY; without even the implied warranty of
17//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18//  GNU General Public License for more details.
19//
20//  You should have received a copy of the GNU General Public License
21//  along with this program; if not, write to the Free Software
22//  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
23//
24
25#include <config.h>
26
27#include "aventreectrl.h"
28#include "mainfrm.h"
29
30#include <algorithm>
31#include <stack>
32
33using namespace std;
34
35// STATE_BLANK is used for stations which are siblings of surveys which have
36// select checkboxes.
37enum { STATE_BLANK = 0, STATE_OFF, STATE_ON };
38
39/* XPM */
40static const char *blank_xpm[] = {
41/* columns rows colors chars-per-pixel */
42"15 15 1 1",
43"  c None",
44/* pixels */
45"               ",
46"               ",
47"               ",
48"               ",
49"               ",
50"               ",
51"               ",
52"               ",
53"               ",
54"               ",
55"               ",
56"               ",
57"               ",
58"               ",
59"               "
60};
61
62/* XPM */
63static const char *off_xpm[] = {
64/* columns rows colors chars-per-pixel */
65"15 15 2 1",
66". c #000000",
67"  c None",
68/* pixels */
69"               ",
70"               ",
71" ............  ",
72" .          .  ",
73" .          .  ",
74" .          .  ",
75" .          .  ",
76" .          .  ",
77" .          .  ",
78" .          .  ",
79" .          .  ",
80" .          .  ",
81" .          .  ",
82" ............  ",
83"               "
84};
85
86/* XPM */
87static const char *on_xpm[] = {
88/* columns rows colors chars-per-pixel */
89"15 15 3 1",
90". c #000000",
91"X c #007F28",
92"  c None",
93/* pixels */
94"               ",
95"               ",
96" ............XX",
97" .          XXX",
98" .         XXXX",
99" .        XXXX ",
100" .       XXXX  ",
101" .      XXXX.  ",
102" . XX  XXXX .  ",
103" . XXXXXXX  .  ",
104" .  XXXXX   .  ",
105" .   XXX    .  ",
106" .    X     .  ",
107" ............  ",
108"               "
109};
110
111BEGIN_EVENT_TABLE(AvenTreeCtrl, wxTreeCtrl)
112    EVT_MOTION(AvenTreeCtrl::OnMouseMove)
113    EVT_LEAVE_WINDOW(AvenTreeCtrl::OnLeaveWindow)
114    EVT_TREE_SEL_CHANGED(wxID_ANY, AvenTreeCtrl::OnSelChanged)
115    EVT_TREE_ITEM_ACTIVATED(wxID_ANY, AvenTreeCtrl::OnItemActivated)
116    EVT_CHAR(AvenTreeCtrl::OnKeyPress)
117    EVT_TREE_ITEM_MENU(wxID_ANY, AvenTreeCtrl::OnMenu)
118    EVT_MENU(menu_SURVEY_SHOW_ALL, AvenTreeCtrl::OnRestrict)
119    EVT_MENU(menu_SURVEY_RESTRICT, AvenTreeCtrl::OnRestrict)
120    EVT_MENU(menu_SURVEY_HIDE, AvenTreeCtrl::OnHide)
121    EVT_MENU(menu_SURVEY_SHOW, AvenTreeCtrl::OnShow)
122    EVT_MENU(menu_SURVEY_HIDE_SIBLINGS, AvenTreeCtrl::OnHideSiblings)
123    EVT_MENU(wxID_FIND, AvenTreeCtrl::OnFind)
124    EVT_TREE_STATE_IMAGE_CLICK(wxID_ANY, AvenTreeCtrl::OnStateClick)
125END_EVENT_TABLE()
126
127AvenTreeCtrl::AvenTreeCtrl(MainFrm* parent, wxWindow* window_parent) :
128    wxTreeCtrl(window_parent, wxID_ANY, wxDefaultPosition, wxDefaultSize,
129               wxTR_DEFAULT_STYLE | wxTR_HIDE_ROOT),
130    m_Parent(parent)
131{
132    wxImageList* img_list = new wxImageList(15, 15, 2);
133    img_list->Add(wxBitmap(blank_xpm));
134    img_list->Add(wxBitmap(off_xpm));
135    img_list->Add(wxBitmap(on_xpm));
136    AssignStateImageList(img_list);
137}
138
139void AvenTreeCtrl::FillTree(const wxString& root_name)
140{
141    Freeze();
142    m_Enabled = false;
143    m_LastItem = wxTreeItemId();
144    m_SelValid = false;
145    DeleteAllItems();
146
147    const wxChar separator = m_Parent->GetSeparator();
148    filter.clear();
149    filter.SetSeparator(separator);
150
151    // Create the (hidden) real root of the wxTreeCtrl.
152    wxTreeItemId treeroot = AddRoot(wxString());
153
154    // Create the root of the survey tree.
155    wxTreeItemId surveyroot = AppendItem(treeroot, root_name);
156
157    // Fill the tree of stations and prefixes.
158    stack<wxTreeItemId> previous_ids;
159    wxString current_prefix;
160    wxTreeItemId current_id = surveyroot;
161
162    list<LabelInfo*>::const_iterator pos = m_Parent->GetLabels();
163    while (pos != m_Parent->GetLabelsEnd()) {
164        LabelInfo* label = *pos++;
165
166        if (label->IsAnon()) continue;
167
168        // Determine the current prefix.
169        wxString prefix = label->GetText().BeforeLast(separator);
170
171        // Determine if we're still on the same prefix.
172        if (prefix == current_prefix) {
173            // no need to fiddle with branches...
174        }
175        // If not, then see if we've descended to a new prefix.
176        else if (prefix.length() > current_prefix.length() &&
177                 prefix.StartsWith(current_prefix) &&
178                 (prefix[current_prefix.length()] == separator ||
179                  current_prefix.empty())) {
180            // We have, so start as many new branches as required.
181            int current_prefix_length = current_prefix.length();
182            current_prefix = prefix;
183            size_t next_dot = current_prefix_length;
184            if (!next_dot) --next_dot;
185            do {
186                size_t prev_dot = next_dot + 1;
187
188                // Extract the next bit of prefix.
189                next_dot = prefix.find(separator, prev_dot + 1);
190
191                wxString bit = prefix.substr(prev_dot, next_dot - prev_dot);
192                // Sigh, therion can produce files with empty components in
193                // station names!
194                // assert(!bit.empty());
195
196                // Add the current tree ID to the stack.
197                previous_ids.push(current_id);
198
199                // Append the new item to the tree and set this as the current branch.
200                current_id = AppendItem(current_id, bit);
201                SetItemData(current_id, new TreeData(prefix.substr(0, next_dot)));
202            } while (next_dot != wxString::npos);
203        }
204        // Otherwise, we must have moved up, and possibly then down again.
205        else {
206            size_t count = 0;
207            bool ascent_only = (prefix.length() < current_prefix.length() &&
208                                current_prefix.StartsWith(prefix) &&
209                                (current_prefix[prefix.length()] == separator ||
210                                 prefix.empty()));
211            if (!ascent_only) {
212                // Find out how much of the current prefix and the new prefix
213                // are the same.
214                // Note that we require a match of a whole number of parts
215                // between dots!
216                size_t n = min(prefix.length(), current_prefix.length());
217                size_t i;
218                for (i = 0; i < n && prefix[i] == current_prefix[i]; ++i) {
219                    if (prefix[i] == separator) count = i + 1;
220                }
221            } else {
222                count = prefix.length() + 1;
223            }
224
225            // Extract the part of the current prefix after the bit (if any)
226            // which has matched.
227            // This gives the prefixes to ascend over.
228            wxString prefixes_ascended = current_prefix.substr(count);
229
230            // Count the number of prefixes to ascend over.
231            int num_prefixes = prefixes_ascended.Freq(separator);
232
233            // Reverse up over these prefixes.
234            for (int i = 1; i <= num_prefixes; i++) {
235                previous_ids.pop();
236            }
237            current_id = previous_ids.top();
238            previous_ids.pop();
239
240            if (!ascent_only) {
241                // Add branches for this new part.
242                size_t next_dot = count - 1;
243                do {
244                    size_t prev_dot = next_dot + 1;
245
246                    // Extract the next bit of prefix.
247                    next_dot = prefix.find(separator, prev_dot + 1);
248
249                    wxString bit = prefix.substr(prev_dot, next_dot - prev_dot);
250                    // Sigh, therion can produce files with empty components in
251                    // station names!
252                    // assert(!bit.empty());
253
254                    // Add the current tree ID to the stack.
255                    previous_ids.push(current_id);
256
257                    // Append the new item to the tree and set this as the current branch.
258                    current_id = AppendItem(current_id, bit);
259                    SetItemData(current_id, new TreeData(prefix.substr(0, next_dot)));
260                } while (next_dot != wxString::npos);
261            }
262
263            current_prefix = prefix;
264        }
265
266        // Now add the leaf.
267        wxString bit = label->GetText().AfterLast(separator);
268        // Sigh, therion can produce files with empty components in station
269        // names!
270        // assert(!bit.empty());
271        wxTreeItemId id = AppendItem(current_id, bit);
272        SetItemData(id, new TreeData(label));
273        label->tree_id = id;
274        // Set the colour for an item in the survey tree.
275        if (label->IsEntrance()) {
276            // Entrances are green (like entrance blobs).
277            SetItemTextColour(id, wxColour(0, 255, 40));
278        } else if (label->IsSurface()) {
279            // Surface stations are dark green.
280            SetItemTextColour(id, wxColour(49, 158, 79));
281        }
282    }
283
284    Expand(surveyroot);
285    m_Enabled = true;
286    Thaw();
287}
288
289constexpr auto TREE_MASK = wxTREE_HITTEST_ONITEMLABEL |
290                           wxTREE_HITTEST_ONITEMRIGHT |
291                           wxTREE_HITTEST_ONITEMSTATEICON;
292
293void AvenTreeCtrl::OnMouseMove(wxMouseEvent& event)
294{
295    if (!m_Enabled || m_Parent->Animating())
296        return;
297
298    int flags;
299    wxTreeItemId pos = HitTest(event.GetPosition(), flags);
300    if (!(flags & TREE_MASK)) {
301        pos = wxTreeItemId();
302    }
303    if (pos == m_LastItem) return;
304    if (pos.IsOk()) {
305        const TreeData* data = static_cast<const TreeData*>(GetItemData(pos));
306        m_Parent->DisplayTreeInfo(data);
307        if (data && !data->IsStation()) {
308            // For stations, MainFrm calls back to SetHere(), but for surveys
309            // we need to do that ourselves.
310            SetHere(pos);
311        }
312    } else {
313        m_Parent->DisplayTreeInfo();
314    }
315}
316
317void AvenTreeCtrl::SetHere(wxTreeItemId pos)
318{
319    if (pos == m_LastItem) return;
320
321    if (m_LastItem.IsOk()) {
322        SetItemBackgroundColour(m_LastItem, m_BackgroundColour);
323    }
324    if (pos.IsOk()) {
325        m_BackgroundColour = GetItemBackgroundColour(pos);
326        SetItemBackgroundColour(pos, wxColour(180, 180, 180));
327    }
328    m_LastItem = pos;
329}
330
331void AvenTreeCtrl::OnLeaveWindow(wxMouseEvent&)
332{
333    if (m_LastItem.IsOk()) {
334        SetItemBackgroundColour(m_LastItem, m_BackgroundColour);
335        m_LastItem = wxTreeItemId();
336    }
337    m_Parent->DisplayTreeInfo();
338}
339
340void AvenTreeCtrl::OnSelChanged(wxTreeEvent&)
341{
342    m_SelValid = true;
343}
344
345void AvenTreeCtrl::OnItemActivated(wxTreeEvent& e)
346{
347    if (!m_Enabled) return;
348
349    m_Parent->TreeItemSelected(GetItemData(e.GetItem()));
350}
351
352void AvenTreeCtrl::OnMenu(wxTreeEvent& e)
353{
354    if (!m_Enabled) return;
355
356    const TreeData* data = static_cast<const TreeData*>(GetItemData(e.GetItem()));
357    menu_data = data;
358    menu_item = e.GetItem();
359    if (!data) {
360        // Survey tree root:
361        wxMenu menu;
362        /* TRANSLATORS: In aven's survey tree, right-clicking on the root
363         * gives a pop-up menu and this is an option (but only enabled if
364         * the view is restricted to a subsurvey). It reloads the current
365         * survey file with the who survey visible.
366         */
367        menu.Append(menu_SURVEY_SHOW_ALL, wmsg(/*Show all*/245));
368        if (m_Parent->GetSurvey().empty())
369            menu.Enable(menu_SURVEY_SHOW_ALL, false);
370        PopupMenu(&menu);
371    } else if (data->IsStation()) {
372        // Station: name is data->GetLabel()->GetText()
373        wxMenu menu;
374        menu.Append(wxID_FIND, wmsg(/*Find*/332));
375        PopupMenu(&menu);
376    } else if (ItemHasChildren(menu_item)) {
377        // Survey:
378        wxMenu menu;
379        /* TRANSLATORS: In aven's survey tree, right-clicking on a survey
380         * name gives a pop-up menu and this is an option.  It reloads the
381         * current survey file with the view restricted to the survey
382         * clicked upon.
383         */
384        menu.Append(menu_SURVEY_RESTRICT, wmsg(/*Hide others*/246));
385        menu.AppendSeparator();
386        //menu.Append(menu_SURVEY_HIDE, wmsg(/*&Hide*/407));
387        menu.Append(menu_SURVEY_SHOW, wmsg(/*&Show*/409));
388        //menu.Append(menu_SURVEY_HIDE_SIBLINGS, wmsg(/*Hide si&blings*/388));
389        switch (GetItemState(menu_item)) {
390            case STATE_ON: // Currently shown.
391                menu.Enable(menu_SURVEY_SHOW, false);
392                break;
393#if 0
394            case STATE_HIDDEN: // Currently hidden.
395                menu.Enable(menu_SURVEY_RESTRICT, false);
396                menu.Enable(menu_SURVEY_HIDE, false);
397                menu.Enable(menu_SURVEY_HIDE_SIBLINGS, false);
398                break;
399            case STATE_OFF:
400                menu.Enable(menu_SURVEY_HIDE, false);
401                menu.Enable(menu_SURVEY_HIDE_SIBLINGS, false);
402                break;
403#endif
404        }
405        menu.Append(wxID_FIND, wmsg(/*Find*/332));
406        PopupMenu(&menu);
407    } else {
408        // Overlay - FIXME: menu here?
409    }
410    menu_data = NULL;
411    e.Skip();
412}
413
414bool AvenTreeCtrl::GetSelectionData(wxTreeItemData** data) const
415{
416    assert(m_Enabled);
417    assert(data);
418
419    if (!m_SelValid) {
420        return false;
421    }
422
423    wxTreeItemId id = GetSelection();
424    if (id.IsOk()) {
425        *data = GetItemData(id);
426    }
427
428    return id.IsOk() && *data;
429}
430
431void AvenTreeCtrl::UnselectAll()
432{
433    m_SelValid = false;
434    wxTreeCtrl::UnselectAll();
435}
436
437void AvenTreeCtrl::OnKeyPress(wxKeyEvent &e)
438{
439    switch (e.GetKeyCode()) {
440        case WXK_ESCAPE:
441            m_Parent->ClearTreeSelection();
442            break;
443        case WXK_RETURN: {
444            wxTreeItemId id = GetSelection();
445            if (id.IsOk()) {
446                if (ItemHasChildren(id)) {
447                    // If on a branch, expand/contract it.
448                    if (IsExpanded(id)) {
449                        Collapse(id);
450                    } else {
451                        Expand(id);
452                    }
453                } else {
454                    // If on a station, centre on it by selecting it twice.
455                    m_Parent->TreeItemSelected(GetItemData(id));
456                    m_Parent->TreeItemSelected(GetItemData(id));
457                }
458            }
459            break;
460        }
461        case WXK_LEFT: case WXK_RIGHT: case WXK_UP: case WXK_DOWN:
462        case WXK_HOME: case WXK_END: case WXK_PAGEUP: case WXK_PAGEDOWN:
463            e.Skip();
464            break;
465        default:
466            // Pass key event to MainFrm which will pass to GfxCore which will
467            // pass to GUIControl.
468            m_Parent->OnKeyPress(e);
469            break;
470    }
471}
472
473void AvenTreeCtrl::OnRestrict(wxCommandEvent&)
474{
475    m_Parent->RestrictTo(menu_data && menu_data->IsSurvey() ? menu_data->GetSurvey() : wxString());
476    // FIXME: Overlays
477}
478
479void AvenTreeCtrl::OnHide(wxCommandEvent&)
480{
481    // Shouldn't be available for the root item.
482    wxASSERT(menu_data);
483    // Hide should be disabled unless the item is explicitly shown.
484    wxASSERT(GetItemState(menu_item) == STATE_ON);
485    SetItemState(menu_item, STATE_OFF);
486    // FIXME: Overlays?
487    filter.remove(menu_data->GetSurvey());
488#if 0
489    Freeze();
490    // Show siblings if not already shown or hidden.
491    wxTreeItemId i = menu_item;
492    while ((i = GetPrevSibling(i)).IsOk()) {
493        if (GetItemState(i) == wxTREE_ITEMSTATE_NONE)
494            SetItemState(i, 1);
495    }
496    i = menu_item;
497    while ((i = GetNextSibling(i)).IsOk()) {
498        if (GetItemState(i) == wxTREE_ITEMSTATE_NONE)
499            SetItemState(i, 1);
500    }
501    Thaw();
502#endif
503    m_Parent->ForceFullRedraw();
504}
505
506void AvenTreeCtrl::OnShow(wxCommandEvent&)
507{
508    // Shouldn't be available for the root item.
509    wxASSERT(menu_data);
510    auto old_state = GetItemState(menu_item);
511    // Show should be disabled for an explicitly shown item.
512    wxASSERT(old_state != STATE_ON);
513    Freeze();
514    SetItemState(menu_item, STATE_ON);
515    // FIXME: Overlays?
516    filter.add(menu_data->GetSurvey());
517    if (old_state == wxTREE_ITEMSTATE_NONE) {
518        // Hide siblings if not already shown or hidden.
519        wxTreeItemId i = menu_item;
520        while ((i = GetPrevSibling(i)).IsOk()) {
521            if (GetItemState(i) == wxTREE_ITEMSTATE_NONE) {
522                const TreeData* data = static_cast<const TreeData*>(GetItemData(i));
523                SetItemState(i, data->IsStation() ? STATE_BLANK : STATE_OFF);
524            }
525        }
526        i = menu_item;
527        while ((i = GetNextSibling(i)).IsOk()) {
528            if (GetItemState(i) == wxTREE_ITEMSTATE_NONE) {
529                const TreeData* data = static_cast<const TreeData*>(GetItemData(i));
530                SetItemState(i, data->IsStation() ? STATE_BLANK : STATE_OFF);
531            }
532        }
533    }
534    Thaw();
535    m_Parent->ForceFullRedraw();
536}
537
538void AvenTreeCtrl::OnHideSiblings(wxCommandEvent&)
539{
540    // Shouldn't be available for the root item.
541    wxASSERT(menu_data);
542    Freeze();
543    // FIXME: Overlays?
544    SetItemState(menu_item, STATE_ON);
545    filter.add(menu_data->GetSurvey());
546
547    wxTreeItemId i = menu_item;
548    while ((i = GetPrevSibling(i)).IsOk()) {
549        const TreeData* data = static_cast<const TreeData*>(GetItemData(i));
550        filter.remove(data->GetSurvey());
551        SetItemState(i, data->IsStation() ? STATE_BLANK : STATE_OFF);
552    }
553    i = menu_item;
554    while ((i = GetNextSibling(i)).IsOk()) {
555        const TreeData* data = static_cast<const TreeData*>(GetItemData(i));
556        filter.remove(data->GetSurvey());
557        SetItemState(i, data->IsStation() ? STATE_BLANK : STATE_OFF);
558    }
559    Thaw();
560    m_Parent->ForceFullRedraw();
561}
562
563void AvenTreeCtrl::OnFind(wxCommandEvent&)
564{
565    // Shouldn't be available for the root item.
566    wxASSERT(menu_data);
567    m_Parent->TreeItemSearch(GetItemData(menu_item));
568}
569
570void AvenTreeCtrl::OnStateClick(wxTreeEvent& e)
571{
572    auto item = e.GetItem();
573    const TreeData* data = static_cast<const TreeData*>(GetItemData(item));
574    switch (GetItemState(item)) {
575        case STATE_BLANK:
576            // Click on blank state icon for a station - let the tree handle
577            // this in the same way as a click on the label.
578            return;
579        case STATE_ON:
580            if (!ItemHasChildren(item)) {
581                // Overlay.
582                m_Parent->InvalidateOverlays();
583            } else {
584                // Survey.
585                if (data) filter.remove(data->GetSurvey());
586            }
587            SetItemState(item, STATE_OFF);
588            break;
589        case STATE_OFF:
590            if (!ItemHasChildren(item)) {
591                // Overlay.
592                m_Parent->InvalidateOverlays();
593            } else {
594                // Survey.
595                if (data) filter.add(data->GetSurvey());
596            }
597            SetItemState(item, STATE_ON);
598            break;
599    }
600    e.Skip();
601    m_Parent->ForceFullRedraw();
602}
603
604void AvenTreeCtrl::AddOverlay(const wxString& file)
605{
606    char* leaf = leaf_from_fnm(file.utf8_str());
607    auto id = AppendItem(GetRootItem(), leaf);
608    free(leaf);
609    SetItemState(id, STATE_ON);
610    SetItemData(id, new TreeData(file));
611}
612
613void AvenTreeCtrl::RemoveOverlay(const wxString& file)
614{
615    // If we add an overlay but fail to load it and remove it again, the
616    // overlay will be the last one, so search from the last one back.
617    for (auto item = GetLastChild(GetRootItem());
618         item.IsOk();
619         item = GetPrevSibling(item)) {
620        if (ItemHasChildren(item)) {
621            // Not an overlay.
622            continue;
623        }
624        const TreeData* data = static_cast<const TreeData*>(GetItemData(item));
625        if (data->GetSurvey() == file) {
626            Delete(item);
627            break;
628        }
629    }
630}
631
632wxTreeItemId AvenTreeCtrl::FirstOverlay()
633{
634    wxTreeItemIdValue cookie;
635    auto item = GetFirstChild(GetRootItem(), cookie);
636    while (item.IsOk() &&
637           (ItemHasChildren(item) || GetItemState(item) != STATE_ON)) {
638        item = GetNextSibling(item);
639    }
640    return item;
641}
642
643wxTreeItemId AvenTreeCtrl::NextOverlay(wxTreeItemId item)
644{
645    do {
646        item = GetNextSibling(item);
647    } while (item.IsOk() &&
648             (ItemHasChildren(item) || GetItemState(item) != STATE_ON));
649    return item;
650}
651
652wxTreeItemId AvenTreeCtrl::RemoveOverlay(wxTreeItemId id)
653{
654    wxTreeItemId item = NextOverlay(id);
655    Delete(id);
656    return item;
657}
658
659const wxString& AvenTreeCtrl::GetOverlayFilename(wxTreeItemId item)
660{
661    if (ItemHasChildren(item)) {
662not_an_overlay:
663        static const wxString empty_string;
664        return empty_string;
665    }
666
667    const TreeData* data = static_cast<const TreeData*>(GetItemData(item));
668    if (!data) goto not_an_overlay;
669    return data->GetSurvey();
670}
Note: See TracBrowser for help on using the repository browser.