source: git/src/aventreectrl.cc @ de7f61a6

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

Improve the station search UI

Use wxSearchCtrl for the control which means it'll now use the
standard search control for platforms which have one. This
allows us to easily set a placeholder text to make it clearer
what the control is for.

  • Property mode set to 100644
File size: 18.5 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        // TRANSLATORS: Menu item in right-click menu in survey tree.
375        menu.Append(wxID_FIND, wmsg(/*Find*/332));
376        PopupMenu(&menu);
377    } else if (ItemHasChildren(menu_item)) {
378        // Survey:
379        wxMenu menu;
380        /* TRANSLATORS: In aven's survey tree, right-clicking on a survey
381         * name gives a pop-up menu and this is an option.  It reloads the
382         * current survey file with the view restricted to the survey
383         * clicked upon.
384         */
385        menu.Append(menu_SURVEY_RESTRICT, wmsg(/*Hide others*/246));
386        menu.AppendSeparator();
387        //menu.Append(menu_SURVEY_HIDE, wmsg(/*&Hide*/407));
388        menu.Append(menu_SURVEY_SHOW, wmsg(/*&Show*/409));
389        //menu.Append(menu_SURVEY_HIDE_SIBLINGS, wmsg(/*Hide si&blings*/388));
390        switch (GetItemState(menu_item)) {
391            case STATE_ON: // Currently shown.
392                menu.Enable(menu_SURVEY_SHOW, false);
393                break;
394#if 0
395            case STATE_HIDDEN: // Currently hidden.
396                menu.Enable(menu_SURVEY_RESTRICT, false);
397                menu.Enable(menu_SURVEY_HIDE, false);
398                menu.Enable(menu_SURVEY_HIDE_SIBLINGS, false);
399                break;
400            case STATE_OFF:
401                menu.Enable(menu_SURVEY_HIDE, false);
402                menu.Enable(menu_SURVEY_HIDE_SIBLINGS, false);
403                break;
404#endif
405        }
406        menu.Append(wxID_FIND, wmsg(/*Find*/332));
407        PopupMenu(&menu);
408    } else {
409        // Overlay - FIXME: menu here?
410    }
411    menu_data = NULL;
412    e.Skip();
413}
414
415bool AvenTreeCtrl::GetSelectionData(wxTreeItemData** data) const
416{
417    assert(m_Enabled);
418    assert(data);
419
420    if (!m_SelValid) {
421        return false;
422    }
423
424    wxTreeItemId id = GetSelection();
425    if (id.IsOk()) {
426        *data = GetItemData(id);
427    }
428
429    return id.IsOk() && *data;
430}
431
432void AvenTreeCtrl::UnselectAll()
433{
434    m_SelValid = false;
435    wxTreeCtrl::UnselectAll();
436}
437
438void AvenTreeCtrl::OnKeyPress(wxKeyEvent &e)
439{
440    switch (e.GetKeyCode()) {
441        case WXK_ESCAPE:
442            m_Parent->ClearTreeSelection();
443            break;
444        case WXK_RETURN: {
445            wxTreeItemId id = GetSelection();
446            if (id.IsOk()) {
447                if (ItemHasChildren(id)) {
448                    // If on a branch, expand/contract it.
449                    if (IsExpanded(id)) {
450                        Collapse(id);
451                    } else {
452                        Expand(id);
453                    }
454                } else {
455                    // If on a station, centre on it by selecting it twice.
456                    m_Parent->TreeItemSelected(GetItemData(id));
457                    m_Parent->TreeItemSelected(GetItemData(id));
458                }
459            }
460            break;
461        }
462        case WXK_LEFT: case WXK_RIGHT: case WXK_UP: case WXK_DOWN:
463        case WXK_HOME: case WXK_END: case WXK_PAGEUP: case WXK_PAGEDOWN:
464            e.Skip();
465            break;
466        default:
467            // Pass key event to MainFrm which will pass to GfxCore which will
468            // pass to GUIControl.
469            m_Parent->OnKeyPress(e);
470            break;
471    }
472}
473
474void AvenTreeCtrl::OnRestrict(wxCommandEvent&)
475{
476    m_Parent->RestrictTo(menu_data && menu_data->IsSurvey() ? menu_data->GetSurvey() : wxString());
477    // FIXME: Overlays
478}
479
480void AvenTreeCtrl::OnHide(wxCommandEvent&)
481{
482    // Shouldn't be available for the root item.
483    wxASSERT(menu_data);
484    // Hide should be disabled unless the item is explicitly shown.
485    wxASSERT(GetItemState(menu_item) == STATE_ON);
486    SetItemState(menu_item, STATE_OFF);
487    // FIXME: Overlays?
488    filter.remove(menu_data->GetSurvey());
489#if 0
490    Freeze();
491    // Show siblings if not already shown or hidden.
492    wxTreeItemId i = menu_item;
493    while ((i = GetPrevSibling(i)).IsOk()) {
494        if (GetItemState(i) == wxTREE_ITEMSTATE_NONE)
495            SetItemState(i, 1);
496    }
497    i = menu_item;
498    while ((i = GetNextSibling(i)).IsOk()) {
499        if (GetItemState(i) == wxTREE_ITEMSTATE_NONE)
500            SetItemState(i, 1);
501    }
502    Thaw();
503#endif
504    m_Parent->ForceFullRedraw();
505}
506
507void AvenTreeCtrl::OnShow(wxCommandEvent&)
508{
509    // Shouldn't be available for the root item.
510    wxASSERT(menu_data);
511    auto old_state = GetItemState(menu_item);
512    // Show should be disabled for an explicitly shown item.
513    wxASSERT(old_state != STATE_ON);
514    Freeze();
515    SetItemState(menu_item, STATE_ON);
516    // FIXME: Overlays?
517    filter.add(menu_data->GetSurvey());
518    if (old_state == wxTREE_ITEMSTATE_NONE) {
519        // Hide siblings if not already shown or hidden.
520        wxTreeItemId i = menu_item;
521        while ((i = GetPrevSibling(i)).IsOk()) {
522            if (GetItemState(i) == wxTREE_ITEMSTATE_NONE) {
523                const TreeData* data = static_cast<const TreeData*>(GetItemData(i));
524                SetItemState(i, data->IsStation() ? STATE_BLANK : STATE_OFF);
525            }
526        }
527        i = menu_item;
528        while ((i = GetNextSibling(i)).IsOk()) {
529            if (GetItemState(i) == wxTREE_ITEMSTATE_NONE) {
530                const TreeData* data = static_cast<const TreeData*>(GetItemData(i));
531                SetItemState(i, data->IsStation() ? STATE_BLANK : STATE_OFF);
532            }
533        }
534    }
535    Thaw();
536    m_Parent->ForceFullRedraw();
537}
538
539void AvenTreeCtrl::OnHideSiblings(wxCommandEvent&)
540{
541    // Shouldn't be available for the root item.
542    wxASSERT(menu_data);
543    Freeze();
544    // FIXME: Overlays?
545    SetItemState(menu_item, STATE_ON);
546    filter.add(menu_data->GetSurvey());
547
548    wxTreeItemId i = menu_item;
549    while ((i = GetPrevSibling(i)).IsOk()) {
550        const TreeData* data = static_cast<const TreeData*>(GetItemData(i));
551        filter.remove(data->GetSurvey());
552        SetItemState(i, data->IsStation() ? STATE_BLANK : STATE_OFF);
553    }
554    i = menu_item;
555    while ((i = GetNextSibling(i)).IsOk()) {
556        const TreeData* data = static_cast<const TreeData*>(GetItemData(i));
557        filter.remove(data->GetSurvey());
558        SetItemState(i, data->IsStation() ? STATE_BLANK : STATE_OFF);
559    }
560    Thaw();
561    m_Parent->ForceFullRedraw();
562}
563
564void AvenTreeCtrl::OnFind(wxCommandEvent&)
565{
566    // Shouldn't be available for the root item.
567    wxASSERT(menu_data);
568    m_Parent->TreeItemSearch(GetItemData(menu_item));
569}
570
571void AvenTreeCtrl::OnStateClick(wxTreeEvent& e)
572{
573    auto item = e.GetItem();
574    const TreeData* data = static_cast<const TreeData*>(GetItemData(item));
575    switch (GetItemState(item)) {
576        case STATE_BLANK:
577            // Click on blank state icon for a station - let the tree handle
578            // this in the same way as a click on the label.
579            return;
580        case STATE_ON:
581            if (!ItemHasChildren(item)) {
582                // Overlay.
583                m_Parent->InvalidateOverlays();
584            } else {
585                // Survey.
586                if (data) filter.remove(data->GetSurvey());
587            }
588            SetItemState(item, STATE_OFF);
589            break;
590        case STATE_OFF:
591            if (!ItemHasChildren(item)) {
592                // Overlay.
593                m_Parent->InvalidateOverlays();
594            } else {
595                // Survey.
596                if (data) filter.add(data->GetSurvey());
597            }
598            SetItemState(item, STATE_ON);
599            break;
600    }
601    e.Skip();
602    m_Parent->ForceFullRedraw();
603}
604
605void AvenTreeCtrl::AddOverlay(const wxString& file)
606{
607    char* leaf = leaf_from_fnm(file.utf8_str());
608    auto id = AppendItem(GetRootItem(), leaf);
609    free(leaf);
610    SetItemState(id, STATE_ON);
611    SetItemData(id, new TreeData(file));
612}
613
614void AvenTreeCtrl::RemoveOverlay(const wxString& file)
615{
616    // If we add an overlay but fail to load it and remove it again, the
617    // overlay will be the last one, so search from the last one back.
618    for (auto item = GetLastChild(GetRootItem());
619         item.IsOk();
620         item = GetPrevSibling(item)) {
621        if (ItemHasChildren(item)) {
622            // Not an overlay.
623            continue;
624        }
625        const TreeData* data = static_cast<const TreeData*>(GetItemData(item));
626        if (data->GetSurvey() == file) {
627            Delete(item);
628            break;
629        }
630    }
631}
632
633wxTreeItemId AvenTreeCtrl::FirstOverlay()
634{
635    wxTreeItemIdValue cookie;
636    auto item = GetFirstChild(GetRootItem(), cookie);
637    while (item.IsOk() &&
638           (ItemHasChildren(item) || GetItemState(item) != STATE_ON)) {
639        item = GetNextSibling(item);
640    }
641    return item;
642}
643
644wxTreeItemId AvenTreeCtrl::NextOverlay(wxTreeItemId item)
645{
646    do {
647        item = GetNextSibling(item);
648    } while (item.IsOk() &&
649             (ItemHasChildren(item) || GetItemState(item) != STATE_ON));
650    return item;
651}
652
653wxTreeItemId AvenTreeCtrl::RemoveOverlay(wxTreeItemId id)
654{
655    wxTreeItemId item = NextOverlay(id);
656    Delete(id);
657    return item;
658}
659
660const wxString& AvenTreeCtrl::GetOverlayFilename(wxTreeItemId item)
661{
662    if (ItemHasChildren(item)) {
663not_an_overlay:
664        static const wxString empty_string;
665        return empty_string;
666    }
667
668    const TreeData* data = static_cast<const TreeData*>(GetItemData(item));
669    if (!data) goto not_an_overlay;
670    return data->GetSurvey();
671}
Note: See TracBrowser for help on using the repository browser.