OSDN Git Service

Handling key and mouse events in listctrl is improved
[molby/Molby.git] / wxSources / MyListCtrl.cpp
1 /*
2  *  MyListCtrl.cpp
3  *  Molby
4  *
5  *  Created by Toshi Nagata on 2022/09/11.
6  *  Copyright 2022 Toshi Nagata. All rights reserved.
7  *
8  This program is free software; you can redistribute it and/or modify
9  it under the terms of the GNU General Public License as published by
10  the Free Software Foundation version 2 of the License.
11  
12  This program is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  GNU General Public License for more details.
16  */
17
18 #include "MyListCtrl.h"
19 #include "MyMBConv.h"
20
21 #include "wx/dcclient.h"
22 #include "wx/scrolwin.h"
23 #include "wx/glcanvas.h"
24 #include "wx/menu.h"
25 #include "wx/sizer.h"
26 #include "wx/toplevel.h"
27
28 #if wxCHECK_VERSION(3,1,0)
29 #define FromFrameDIP(frame, x) frame->FromDIP(x)
30 #else
31 #define FromFrameDIP(frame, x) (x)
32 #endif
33
34 const wxEventType MyListCtrlEvent = wxNewEventType();
35
36 IMPLEMENT_DYNAMIC_CLASS(MyListCtrl, wxWindow)
37
38 BEGIN_EVENT_TABLE(MyListCtrl, wxWindow)
39 //EVT_COMMAND(MyListCtrlEvent_tableSelectionChanged, MyListCtrlEvent, MyListCtrl::OnTableSelectionChanged)
40 //EVT_COMMAND(MyListCtrlEvent_enableTableSelectionNotification, MyListCtrlEvent, MyListCtrl::OnEnableTableSelectionNotification)
41 END_EVENT_TABLE()
42
43 MyListCtrl::MyListCtrl()
44 {
45         editText = NULL;
46     dataSource = NULL;
47     header = NULL;
48     scroll = NULL;
49     ncols = nrows = 0;
50     headerHeight = rowHeight = 0;
51     pageWidth = pageHeight = 0;
52     editRow = editColumn = -1;
53     dragTargetRow = -1;
54     mouseMode = 0;
55     mouseRow = -1;
56     lastMouseRow = -1;
57     draggingRows = false;
58     selectionChangeNotificationRequired = false;
59     selectionChangeNotificationEnabled = true;
60     lastPopUpColumn = lastPopUpRow = -1;
61
62 #if defined(__WXMAC__)
63         //  On OSX, the default font seems to be 14-point, which is too big.
64 //      wxFont font = this->GetFont();
65 //      font.SetPointSize(12);
66 //      this->SetFont(font);
67 #endif
68 }
69
70 MyListCtrl::~MyListCtrl()
71 {
72         if (editText != NULL) {
73         /*      editText->Destroy(); */  /*  May be unnecessary  */
74                 editText = NULL;
75         }
76 }
77
78 bool
79 MyListCtrl::Create(wxWindow* parent, wxWindowID wid, const wxPoint& pos, const wxSize& size)
80 {
81         this->wxWindow::Create(parent, wid, pos, size);
82
83     header = new wxWindow(this, 1001, wxPoint(0, 0), wxSize(size.x, 16), wxWANTS_CHARS);
84     scroll = new wxScrolledWindow(this, 1002, wxPoint(0, 16), wxSize(size.x, (size.y <= 16 ? -1 : size.y - 16)));
85     
86     //  Connect events
87     header->Bind(wxEVT_PAINT, &MyListCtrl::OnPaintHeader, this);
88     scroll->Bind(wxEVT_PAINT, &MyListCtrl::OnPaint, this);
89     scroll->Bind(wxEVT_LEFT_DOWN, &MyListCtrl::OnLeftDown, this);
90     scroll->Bind(wxEVT_LEFT_UP, &MyListCtrl::OnLeftUp, this);
91     scroll->Bind(wxEVT_LEFT_DCLICK, &MyListCtrl::OnLeftDClick, this);
92     scroll->Bind(wxEVT_MOTION, &MyListCtrl::OnMotion, this);
93     scroll->Bind(wxEVT_SCROLLWIN_BOTTOM, &MyListCtrl::OnScrollWin, this);
94     scroll->Bind(wxEVT_SCROLLWIN_TOP, &MyListCtrl::OnScrollWin, this);
95     scroll->Bind(wxEVT_SCROLLWIN_LINEDOWN, &MyListCtrl::OnScrollWin, this);
96     scroll->Bind(wxEVT_SCROLLWIN_LINEUP, &MyListCtrl::OnScrollWin, this);
97     scroll->Bind(wxEVT_SCROLLWIN_PAGEDOWN, &MyListCtrl::OnScrollWin, this);
98     scroll->Bind(wxEVT_SCROLLWIN_PAGEUP, &MyListCtrl::OnScrollWin, this);
99     scroll->Bind(wxEVT_SCROLLWIN_THUMBRELEASE, &MyListCtrl::OnScrollWin, this);
100     scroll->Bind(wxEVT_SCROLLWIN_THUMBTRACK, &MyListCtrl::OnScrollWin, this);
101     scroll->Bind(wxEVT_CHAR, &MyListCtrl::OnCharInScroll, this);
102     scroll->Bind(wxEVT_SET_FOCUS, &MyListCtrl::OnSetFocusInScroll, this);
103     scroll->Bind(wxEVT_KILL_FOCUS, &MyListCtrl::OnKillFocusInScroll, this);
104
105     //  Set Fonts
106     cellFont = GetFont();
107     headerFont = cellFont.Smaller();
108     header->SetFont(headerFont);
109     {
110         //  Measure line height
111         wxClientDC dc(this);
112         int w, h, descent, leading;
113         dc.GetTextExtent(_T("M"), &w, &h, &descent, &leading, &cellFont);
114         rowHeight = h + FromFrameDIP(scroll, 2);
115         dc.GetTextExtent(_T("M"), &w, &h, &descent, &leading, &headerFont);
116         headerHeight = h + FromFrameDIP(scroll, 2);
117         header->SetSize(wxSize(size.x, headerHeight));
118         pageHeight = rowHeight;
119         pageWidth = rowHeight;
120         scroll->SetScrollbars(rowHeight, rowHeight, 1, 1, true);
121     }
122
123     //  Set sizer
124     wxBoxSizer *vsizer = new wxBoxSizer(wxVERTICAL);
125     vsizer->Add(header, wxSizerFlags(0).Expand().Border(wxALL, 0));
126     vsizer->Add(scroll, wxSizerFlags(1).Expand().Border(wxALL, 0));
127     this->SetSizer(vsizer);
128     
129         selectionChangeNotificationRequired = false;
130         selectionChangeNotificationEnabled = true;
131         return true;
132 }
133
134 void
135 MyListCtrl::SetDataSource(MyListCtrlDataSource *source)
136 {
137         dataSource = source;
138         RefreshTable();
139 }
140
141 int
142 MyListCtrl::GetColumnCount()
143 {
144     return colNames.Count();
145 }
146
147 bool
148 MyListCtrl::DeleteColumn(int col)
149 {
150     if (col < 0 || col >= GetColumnCount())
151         return false;
152     colNames.RemoveAt(col);
153     colWidths.erase(colWidths.begin() + col);
154     colFormats.erase(colFormats.begin() + col);
155     SetNeedsReload();
156     return true;
157 }
158
159 bool
160 MyListCtrl::InsertColumn(int col, const wxString &heading, int format, int width)
161 {
162     if (col < 0 || col > GetColumnCount())
163         return false;
164     colNames.Insert(heading, col);
165     colWidths.insert(colWidths.begin() + col, width);
166     colFormats.insert(colFormats.begin() + col, format);
167     SetNeedsReload();
168     return true;
169 }
170
171 void
172 MyListCtrl::SetColumnWidth(int col, int width)
173 {
174     if (col < 0 || col > GetColumnCount())
175         return;
176     colWidths[col] = width;
177     SetNeedsReload();
178 }
179
180 void
181 MyListCtrl::RefreshTable(bool refreshWindow)
182 {
183     if (dataSource == NULL) {
184         nrows = ncols = 0;
185     } else {
186         int i;
187         wxSize sz = header->GetSize();
188         nrows = dataSource->GetItemCount(this);
189         ncols = GetColumnCount();
190         pageWidth = 0;
191         for (i = 0; i < ncols; i++) {
192             pageWidth += colWidths[i];
193         }
194         // rowHeight = dataSource->GetRowHeight(this);
195         //  "+4" is for drawing marker during cell dragging
196         pageHeight = rowHeight * nrows + FromFrameDIP(this, 4);
197         //  Set the subwindow infos
198         sz.y = headerHeight;
199         header->SetMinSize(sz);
200         scroll->SetVirtualSize(pageWidth, pageHeight);
201         int pageSize = floor((pageWidth + rowHeight - 1) / rowHeight);
202         if (scroll->GetScrollPageSize(wxHORIZONTAL) != pageSize)
203             scroll->SetScrollPageSize(wxHORIZONTAL, pageSize);
204         if (scroll->GetScrollPageSize(wxVERTICAL) != pageSize)
205             scroll->SetScrollPageSize(wxVERTICAL, nrows);
206         Layout();
207     }
208     needsReload = false;
209     if (refreshWindow)
210         Refresh();
211 }
212
213 void
214 MyListCtrl::SetNeedsReload(bool flag)
215 {
216     needsReload = flag;
217     if (needsReload)
218         Refresh();
219 }
220
221 static wxBrush lightBlueBrush(wxColour(128, 128, 255));
222
223 void
224 MyListCtrl::OnPaint(wxPaintEvent &event)
225 {
226     if (dataSource == NULL)
227         return;
228
229     if (needsReload)
230         RefreshTable(false);
231
232     wxColour colour;
233     bool isActive;
234     isActive = scroll->HasFocus();
235     isPaintActive = isActive;
236     wxPaintDC dc(scroll);
237     scroll->DoPrepareDC(dc);
238     int ox, oy;
239     scroll->CalcUnscrolledPosition(0, 0, &ox, &oy);
240     int col = -1, row, basex;
241     wxSize sz = scroll->GetClientSize();
242     bool showDragTarget = (draggingRows && (dragTargetRow != mouseRow && dragTargetRow != mouseRow + 1));
243     //  Draw background
244     dc.SetPen(*wxTRANSPARENT_PEN);
245     dc.SetBrush(*wxWHITE_BRUSH);
246     dc.DrawRectangle(ox, oy, sz.x, sz.y);
247     int i, j;
248     basex = 0;
249     for (i = 0; i < ncols; i++) {
250         basex += colWidths[i];
251         if (basex > ox) {
252             col = i;
253             basex -= colWidths[i];
254             break;
255         }
256     }
257     if (col >= 0) {
258         wxTextAttr attr;
259         wxString str;
260         int x, y;
261         int mg = FromFrameDIP(this, 2);
262         row = floor(oy / rowHeight);
263         for (j = row; j < nrows; j++) {
264             float fg0[4], bg0[4];
265             int n0 = dataSource->SetItemColor(this, j, -1, fg0, bg0);
266             y = j * rowHeight;
267             if (showDragTarget && j >= dragTargetRow) {
268                 y += 5;
269             }
270             if (y > sz.y + oy)
271                 break;
272             x = basex;
273             for (i = col; i < ncols; i++) {
274                 float fg[4], bg[4];
275                 int n;
276                 str = dataSource->GetItemText(this, j, i);
277                 n = dataSource->SetItemColor(this, j, i, fg, bg);
278                 if (IsRowSelected(j)) {
279                     if (showDragTarget) {
280                         if (n & 2) {
281                             bg[0] = bg[0] * 0.5 + 0.25;
282                             bg[1] = bg[1] * 0.5 + 0.25;
283                             bg[2] = bg[2] * 0.5 + 0.5;
284                         } else if (n0 & 2) {
285                             bg[0] = bg0[0] * 0.5 + 0.25;
286                             bg[1] = bg0[1] * 0.5 + 0.25;
287                             bg[2] = bg0[2] * 0.5 + 0.5;
288                         } else {
289                             bg[0] = bg[1] = 0.5;
290                             bg[2] = 1.0;
291                         }
292                     } else {
293                         float bgbase[3] = { 0, 0, 1 };
294                         if (!isActive) {
295                             bgbase[0] = 0.6;
296                             bgbase[1] = 0.6;
297                             bgbase[2] = 0.7;
298                         }
299                         if (n & 2) {
300                             bg[0] = (bg[0] + bgbase[0]) * 0.5;
301                             bg[1] = (bg[1] + bgbase[1]) * 0.5;
302                             bg[2] = (bg[2] + bgbase[2]) * 0.5;
303                         } else if (n0 & 2) {
304                             bg[0] = (bg0[0] + bgbase[0]) * 0.5;
305                             bg[1] = (bg0[1] + bgbase[1]) * 0.5;
306                             bg[2] = (bg0[2] + bgbase[2]) * 0.5;
307                         } else {
308                             bg[0] = bgbase[0];
309                             bg[1] = bgbase[1];
310                             bg[2] = bgbase[2];
311                         }
312                     }
313                     if (n & 1) {
314                         //  Leave fg[] as they are
315                     } else if (n0 & 1) {
316                         fg[0] = fg0[0];
317                         fg[1] = fg0[1];
318                         fg[2] = fg0[2];
319                     } else {
320                         fg[0] = fg[1] = fg[2] = 1.0;
321                     }
322                 } else {
323                     if (n & 2) {
324                         //  Leave bg[] as they are
325                     } else if (n0 & 2) {
326                         bg[0] = bg0[0];
327                         bg[1] = bg0[1];
328                         bg[2] = bg0[2];
329                     } else {
330                         bg[0] = bg[1] = bg[2] = 1.0;
331                     }
332                     if (n & 1) {
333                         //  Leave fg[] as they are
334                     } else if (n & 1) {
335                         fg[0] = fg0[0];
336                         fg[1] = fg0[1];
337                         fg[2] = fg0[2];
338                     } else {
339                         fg[0] = fg[1] = fg[2] = 0.0;
340                     }
341                 }
342                 colour.Set(bg[0] * 255, bg[1] * 255, bg[2] * 255);
343                 dc.SetBrush(*wxTheBrushList->FindOrCreateBrush(colour));
344                 dc.SetPen(*wxTRANSPARENT_PEN);
345                 colour.Set(fg[0] * 255, fg[1] * 255, fg[2] * 255);
346                 dc.SetTextForeground(colour);
347                 dc.DrawRectangle(x, y, colWidths[i], rowHeight - 1);
348                 dc.SetPen(*wxLIGHT_GREY_PEN);
349                 dc.DrawLine(x, y + rowHeight - 1, x + colWidths[i], y + rowHeight - 1);
350                 if (i == ncols - 1) {
351                     dc.DrawLine(x + colWidths[i], y, x + colWidths[i], y + rowHeight - 1);
352                 }
353                 dc.SetClippingRegion(x + mg, y, colWidths[i] - mg * 2, rowHeight - 1);
354                 dc.DrawText(str, x + mg, y);
355                 dc.DestroyClippingRegion();
356                 x += colWidths[i];
357                 if (x > ox + sz.x)
358                     break;
359             }
360             if (showDragTarget) {
361                 y = dragTargetRow * rowHeight + 1;
362                 dc.SetBrush(*wxRED_BRUSH);
363                 dc.SetPen(wxNullPen);
364                 dc.DrawRectangle(basex, y, x - basex, 3);
365             }
366         }
367     }
368 }
369
370 void
371 MyListCtrl::OnPaintHeader(wxPaintEvent &event)
372 {
373     wxPaintDC dc(header);
374     dc.SetPen(*wxGREY_PEN);
375     dc.SetBrush(*wxLIGHT_GREY_BRUSH);
376     wxSize sz = header->GetSize();
377     dc.DrawRectangle(0, 0, sz.x, sz.y);
378     if (dataSource) {
379         int ox, oy;
380         int x, x1;
381         int i;
382         wxTextAttr attr;
383         int mg = FromFrameDIP(this, 2);
384         scroll->CalcUnscrolledPosition(0, 0, &ox, &oy);
385         x = -ox;
386         for (i = 0; i < ncols; i++) {
387             x1 = x + colWidths[i];
388             if (x1 > 0) {
389                 wxString str = colNames[i];
390                 dc.DrawLine(x + colWidths[i], 0, x + colWidths[i], sz.y - 1);
391                 dc.SetClippingRegion(x + mg, 0, colWidths[i] - mg * 2, sz.y);
392                 dc.DrawText(str, x + mg, 0);
393                 dc.DestroyClippingRegion();
394             }
395             x = x1;
396         }
397     }
398 }
399
400 void
401 MyListCtrl::PrepareSelectionChangeNotification()
402 {
403     int i;
404     if (selectionChangeNotificationRequired || !selectionChangeNotificationEnabled)
405         return;  //  Do nothing
406     oldSelection.resize(selection.size());
407     for (i = 0; i < selection.size(); i++)
408         oldSelection[i] = selection[i];
409     selectionChangeNotificationRequired = true;
410 }
411
412 int
413 MyListCtrl::GetItemState(int item, int stateMask)
414 {
415     if (stateMask & wxLIST_STATE_SELECTED)
416         return IsRowSelected(item) ? wxLIST_STATE_SELECTED : 0;
417     else return 0;
418 }
419
420 bool
421 MyListCtrl::SetItemState(int item, int state, int stateMask)
422 {
423     if (stateMask & wxLIST_STATE_SELECTED) {
424         if (state & wxLIST_STATE_SELECTED)
425             SelectRow(item);
426         else
427             UnselectRow(item);
428     }
429     return true;
430 }
431
432 bool
433 MyListCtrl::IsRowSelected(int row)
434 {
435     int i;
436     for (i = 0; i < selection.size(); i++) {
437         if (selection[i] == row)
438             return true;
439     }
440     return false;
441 }
442
443 bool
444 MyListCtrl::SelectRow(int row)
445 {
446     int i;
447     if (!dataSource->IsRowSelectable(this, row))
448         return false;
449     for (i = 0; i < selection.size(); i++) {
450         if (selection[i] == row)
451             return false;
452         if (selection[i] > row)
453             break;
454     }
455     selection.insert(selection.begin() + i, row);
456     Refresh();
457     return true;
458 }
459
460 bool
461 MyListCtrl::UnselectRow(int row)
462 {
463     int i;
464     for (i = 0; i < selection.size(); i++) {
465         if (selection[i] == row) {
466             selection.erase(selection.begin() + i);
467             Refresh();
468             return true;
469         }
470     }
471     return false;
472 }
473
474 void
475 MyListCtrl::UnselectAllRows()
476 {
477     selection.clear();
478     Refresh();
479 }
480
481 //  Mouse down
482 //  1. On a selected row with no modifier
483 //    If dragging starts, then start dragging the selected cells (if dragging is enabled)
484 //    If mouse up on another row, then try to reorder the selected cells (if dragging is enabled)
485 //      (the moved cells are left selected)
486 //    If mouse up on the same row, then deselect all other rows
487 //      (and leave only this row selected)
488 //  2. On a selected row with shift
489 //    Same as 1, except that if mouse up on the same row then do nothing
490 //  3. On a selected row with command (Mac) or control (Win)
491 //    Unselect this row, and do nothing on drag or mouse-up
492 //  4. On an unselected row with no modifier
493 //    Unselect all other rows and select this row (in this handler)
494 //    And do the same way as 2
495 //  5. On an unselected row with shift
496 //    Select all cells between the last-clicked row and this row (in this handler)
497 //    And do the same way as 2
498 //  6. On an unselected row with command or control key
499 //    Select this row (in this handler), and do the same way as 2
500 void
501 MyListCtrl::OnLeftDown(wxMouseEvent &event)
502 {
503     int ux, uy, row, col, modifiers, i, n;
504     wxPoint pos;
505     char **items;
506     bool isRowSelected, selectionChanged = false;
507     if (editText)
508         EndEditText();
509     if (!isPaintActive) {
510         //  The scroll is still displayed in inactive state;
511         //  Set focus to scroll, and do nothing else.
512         //  This flag is used because we actually want to intercept the mouseDown
513         //  for 'just activate the listview', but wxWidgets may not allow processing
514         //  mouseDown event _before_ the scroll view gets focus.
515         scroll->SetFocus();
516         Refresh();
517         return;
518     }
519     pos = event.GetPosition();
520     if (FindItemAtPosition(pos, &row, &col) && dataSource != NULL && (n = dataSource->HasPopUpMenu(this, row, col, &items)) > 0) {
521         wxMenu mnu;
522         for (i = 0; i < n; i++) {
523             char *p = items[i];
524             bool enabled = true;
525             if (*p == '-') {
526                 if (p[1] == 0) {
527                     //  Separator
528                     mnu.AppendSeparator();
529                     p = NULL;
530                 } else {
531                     //  Disabled item
532                     p++;
533                     enabled = false;
534                 }
535             }
536             if (p != NULL) {
537                 wxString itemStr(p, WX_DEFAULT_CONV);
538                 mnu.Append(i + 1, itemStr);
539                 if (!enabled)
540                     mnu.Enable(i + 1, false);
541             }
542             free(items[i]);
543             items[i] = NULL;
544         }
545         free(items);
546         lastPopUpColumn = col;
547         lastPopUpRow = row;
548         mnu.Bind(wxEVT_COMMAND_MENU_SELECTED, &MyListCtrl::OnPopUpMenuSelected, this);
549         PopupMenu(&mnu);
550         n = dataSource->GetItemCount(this);
551         for (i = 0; i < n; i++)
552             SetItemState(i, (i == row ? wxLIST_STATE_SELECTED : 0), wxLIST_STATE_SELECTED);
553         PostSelectionChangeNotification();
554         return;
555     }
556     
557     scroll->CalcUnscrolledPosition(pos.x, pos.y, &ux, &uy);
558     row = floor(uy / rowHeight);
559     isRowSelected = IsRowSelected(row);
560     modifiers = event.GetModifiers();
561     PrepareSelectionChangeNotification();
562     if ((modifiers & wxMOD_SHIFT) != 0)
563         mouseMode = (isRowSelected ? 2 : 5);
564 #if defined(__WXMAC__) || defined(__WXOSX__)
565     else if ((modifiers & wxMOD_CMD) != 0)
566         mouseMode = (isRowSelected ? 3 : 6);
567 #else
568     else if ((modifiers & wxMOD_CONTROL) != 0)
569         mouseMode = (isRowSelected ? 3 : 6);
570 #endif
571     else
572         mouseMode = (isRowSelected ? 1 : 4);
573     mouseRow = row;
574     if (mouseMode == 3) {
575         UnselectRow(row);
576         selectionChanged = true;
577     } else if (mouseMode == 4) {
578         UnselectAllRows();
579         SelectRow(row);
580         selectionChanged = true;
581     } else if (mouseMode == 5) {
582         if (lastMouseRow >= 0) {
583             int rs, re;
584             if (lastMouseRow < row) {
585                 rs = lastMouseRow;
586                 re = row;
587             } else {
588                 rs = row;
589                 re = lastMouseRow;
590             }
591             for (i = rs; i <= re; i++) {
592                 SelectRow(i);
593             }
594             selectionChanged = true;
595         }
596     } else if (mouseMode == 6) {
597         SelectRow(row);
598         selectionChanged = true;
599     }
600     if (selectionChanged) {
601         PostSelectionChangeNotification();
602     } else {
603         //  Actually no change occurred
604         selectionChangeNotificationRequired = false;
605     }
606     Refresh();
607 }
608
609 void
610 MyListCtrl::DragRows(int x, int y)
611 {
612     wxSize sz = scroll->GetClientSize();
613     int ux, uy;
614     if (y < 0)
615         y = 0;
616     else if (y > sz.y)
617         y = sz.y;
618     scroll->CalcUnscrolledPosition(x, y, &ux, &uy);
619     dragTargetRow = floor(uy / rowHeight + 0.5);
620     if (dragTargetRow < 0)
621         dragTargetRow = 0;
622     else if (dragTargetRow > nrows)
623         dragTargetRow = nrows;
624     Refresh();
625 }
626
627 void
628 MyListCtrl::OnMotion(wxMouseEvent &event)
629 {
630     if (event.LeftIsDown()) {
631         //  Dragging
632         if (mouseMode > 0 && !scroll->HasCapture()) {
633             //  Start dragging
634             scroll->CaptureMouse();
635             if (mouseMode != 3 && dataSource->IsDragAndDropEnabled(this, mouseRow)) {
636                 draggingRows = true;
637             }
638         }
639         if (draggingRows) {
640             wxPoint cp = scroll->ScreenToClient(wxGetMousePosition());
641             DragRows(cp.x, cp.y);
642         }
643     }
644 }
645
646 void
647 MyListCtrl::OnLeftDClick(wxMouseEvent &event)
648 {
649     //  Start editing this cell
650     int x, y, ux, uy, row, col, cx, i;
651     x = event.GetX();
652     y = event.GetY();
653     scroll->CalcUnscrolledPosition(x, y, &ux, &uy);
654     row = floor(uy / rowHeight);
655     cx = 0;
656     for (i = 0; i < ncols; i++) {
657         if ((ux >= cx && ux < cx + colWidths[i]) || i == ncols - 1) {
658             col = i;
659             break;
660         }
661         cx += colWidths[i];
662     }
663     if (!dataSource->IsItemEditable(this, row, col))
664         return;
665     StartEditText(row, col);
666 }
667
668 void
669 MyListCtrl::OnLeftUp(wxMouseEvent &event)
670 {
671     int x, y, ux, uy, row;
672     bool dragged = false;
673     bool selectionChanged = selectionChangeNotificationRequired;
674     x = event.GetX();
675     y = event.GetY();
676     scroll->CalcUnscrolledPosition(x, y, &ux, &uy);
677     row = floor(uy / rowHeight);
678     PrepareSelectionChangeNotification();
679     if (scroll->HasCapture()) {
680         scroll->ReleaseMouse();
681         dragged = true;
682         if (row != mouseRow) {
683             if (draggingRows) {
684                 dataSource->DragSelectionToRow(this, dragTargetRow);
685                 selectionChanged = true;
686             }
687         }
688         lastMouseRow = dragTargetRow;
689     }
690     if (!dragged) {
691         if (mouseMode == 1 || mouseMode == 4) {
692             if (selection.size() != 1 || !IsRowSelected(mouseRow)) {
693                 UnselectAllRows();
694                 SelectRow(mouseRow);
695                 selectionChanged = true;
696             }
697         }
698         lastMouseRow = row;
699     }
700     if (selectionChanged)
701         PostSelectionChangeNotification();
702     else
703         selectionChangeNotificationRequired = false;
704     mouseMode = 0;
705     mouseRow = -1;
706     draggingRows = false;
707     Refresh();
708 }
709
710 void
711 MyListCtrl::OnScrollWin(wxScrollWinEvent &event)
712 {
713     wxPoint cp = scroll->ScreenToClient(wxGetMousePosition());
714     wxSize sz = scroll->GetClientSize();
715     wxEventType etype = event.GetEventType();
716     int vx, vy;
717     int step = rowHeight;
718     scroll->CalcUnscrolledPosition(0, 0, &vx, &vy);
719     wxSize vs = scroll->GetVirtualSize();
720     int orient = event.GetOrientation();
721     
722     if (scroll->IsAutoScrolling()) {
723         //  Autoscrolling
724         if (mouseMode == 3) {
725             return;  //  Mouse is captured but do not autoscroll
726         }
727         if (etype == wxEVT_SCROLLWIN_LINEUP || etype == wxEVT_SCROLLWIN_LINEDOWN) {
728             if (orient == wxHORIZONTAL) {
729                 //  Is the mouse outside the client width?
730                 if (cp.x < 0) {
731                     etype = wxEVT_SCROLLWIN_LINEUP;
732                     if (vx < step)
733                         etype = 0;
734                 } else if (cp.x > sz.x) {
735                     etype = wxEVT_SCROLLWIN_LINEDOWN;
736                     if (vx > vs.x - sz.x)
737                         etype = 0;
738                 } else etype = 0;
739             } else {
740                 //  Is the mouse outsize the client height?
741                 if (cp.y < 0) {
742                     etype = wxEVT_SCROLLWIN_LINEUP;
743                     if (vy < step)
744                         etype = 0;
745                 } else if (cp.y > sz.y) {
746                     etype = wxEVT_SCROLLWIN_LINEDOWN;
747                     if (vy > vs.y - sz.y)
748                         etype = 0;
749                 } else etype = 0;
750             }
751         }
752         if (etype == 0)
753             return;  //  Pause scrolling
754         event.SetEventType(etype);
755     }
756     event.Skip();
757     if (draggingRows) {
758         //  Handle dragging rows
759         DragRows(cp.x, cp.y);
760     }
761     header->Refresh();  //  Adjust the header display
762 }
763
764 void
765 MyListCtrl::PostSelectionChangeNotification()
766 {
767     dataSource->OnSelectionChanged(this);
768         if (selectionChangeNotificationRequired && selectionChangeNotificationEnabled) {
769                 wxCommandEvent myEvent(MyListCtrlEvent, MyListCtrlEvent_tableSelectionChanged);
770                 wxPostEvent(this, myEvent);
771         selectionChangeNotificationRequired = false;
772         }
773 }
774
775 //  Find item on list control
776 //  Pos is the *client* coordinate in scroll (i.e. scrolled position)
777 bool
778 MyListCtrl::FindItemAtPosition(const wxPoint &pos, int *row, int *col)
779 {
780     int r, cx, i;
781     wxPoint p = scroll->CalcUnscrolledPosition(pos);
782     r = floor(p.y / rowHeight);
783     if (r < 0)
784         r = -1;
785     else if (r >= nrows)
786         r = nrows;
787     cx = 0;
788     if (p.x < 0)
789         i = -1;
790     else {
791         for (i = 0; i < ncols; i++) {
792             if (p.x >= cx && p.x < cx + colWidths[i])
793                 break;
794             cx += colWidths[i];
795         }
796     }
797     if (row != NULL)
798         *row = r;
799     if (col != NULL)
800         *col = i;
801     return (r >= 0 && r < nrows && i >= 0 && i < ncols);
802 }
803
804 //  The return rect is the *client* coordinate in scroll (i.e. scrolled position)
805 bool
806 MyListCtrl::GetItemRectForRowAndColumn(wxRect &rect, int row, int col)
807 {
808     int i, tx, ty, cx, cy;
809     if (col < 0 || col >= ncols || row < 0 || row >= nrows)
810         return false;
811     cy = rowHeight * row;
812     cx = 0;
813     for (i = 0; i < col; i++) {
814         cx += colWidths[i];
815     }
816     scroll->CalcScrolledPosition(cx, cy, &tx, &ty);
817     rect.x = tx;
818     rect.y = ty;
819     rect.width = colWidths[col];
820     rect.height = rowHeight;
821     return true;
822 }
823
824 //  Get the left-top position in scroll unit (= rowHeight)
825 void
826 MyListCtrl::GetScrollPosition(int *xpos, int *ypos)
827 {
828     *xpos = scroll->GetScrollPos(wxHORIZONTAL);
829     *ypos = scroll->GetScrollPos(wxVERTICAL);
830 }
831
832 //  Scroll so that (xpos, ypos) position is left-top
833 //  Return false if the position is outside the scrolling limit
834 bool
835 MyListCtrl::SetScrollPosition(int xpos, int ypos)
836 {
837     bool retval = true;
838     int xlim = scroll->GetScrollLines(wxHORIZONTAL) - scroll->GetScrollPageSize(wxHORIZONTAL);
839     int ylim = scroll->GetScrollLines(wxVERTICAL) - scroll->GetScrollPageSize(wxVERTICAL);
840 //    int xlim = (vsz.x - sz.x) / rowHeight;
841 //    int ylim = (vsz.y - sz.y) / rowHeight;
842     if (xpos > xlim) {
843         retval = false;
844         xpos = xlim;
845     }
846     if (xpos < 0) {
847         retval = false;
848         xpos = 0;
849     }
850     if (ypos > ylim) {
851         retval = false;
852         ypos = ylim;
853     }
854     if (ypos < 0) {
855         retval = false;
856         ypos = 0;
857     }
858     //  TextCtrl may be moved during the scroll, so amend the position
859     scroll->Scroll(xpos, ypos);
860     if (editText) {
861         wxRect r;
862         int delta = FromFrameDIP(scroll, 2);
863         if (GetItemRectForRowAndColumn(r, editRow, editColumn)) {
864             editText->SetPosition(wxPoint(r.x - delta, r.y - delta));
865         }
866     }
867     header->Refresh();
868     return retval;
869 }
870
871 bool
872 MyListCtrl::EnsureVisible(int row, int col)
873 {
874     wxRect r;
875     if (!GetItemRectForRowAndColumn(r, row, (col == -1 ? 0 : col)))
876         return false;
877     wxSize sz = scroll->GetClientSize();
878     int scx = -1, scy = -1;
879     int ux, uy;
880     scroll->CalcUnscrolledPosition(r.x, r.y, &ux, &uy);
881     if (col >= 0) {
882         if (r.x < 0) {
883             scx = floor(ux / rowHeight);
884         } else if (r.x + r.width > sz.x) {
885             scx = ceil((ux + r.width - sz.x) / rowHeight);
886             if (scx < 0)
887                 scx = 0;
888         }
889     }  // If col is negative, then do not scroll horizontally
890     if (r.y < 0) {
891         scy = floor(uy / rowHeight);
892     } else if (r.y + r.height > sz.y) {
893         scy = ceil((uy + r.height - sz.y) / rowHeight);
894         if (scy < 0)
895             scy = 0;
896     }
897     if (scx >= 0 || scy >= 0) {
898         scroll->Scroll(scx, scy);
899         header->Refresh();
900         return true;
901     } else return false;
902 }
903
904 void
905 MyListCtrl::StartEditText(int row, int col)
906 {
907     wxRect r;
908     int delta = FromFrameDIP(scroll, 2);
909     EnsureVisible(row, col);
910     if (!GetItemRectForRowAndColumn(r, row, col))
911         return;
912     if (editText == NULL) {
913         editText = new wxTextCtrl(scroll, -1, "", wxPoint(r.x - delta, r.y - delta), wxSize(r.width + delta * 2, r.height + delta * 2), wxTE_PROCESS_ENTER | wxTE_PROCESS_TAB | wxWANTS_CHARS);
914         editText->Bind(wxEVT_CHAR, &MyListCtrl::OnCharInText, this);
915         editText->Bind(wxEVT_CHAR_HOOK, &MyListCtrl::OnCharHookInText, this);
916     } else {
917         FinalizeEdit();
918         editText->SetPosition(wxPoint(r.x - delta, r.y - delta));
919         editText->SetSize(wxSize(r.width + delta * 2, r.height + delta * 2));
920     }
921     if (selection.size() != 1 || !IsRowSelected(row)) {
922         UnselectAllRows();
923         SelectRow(row);
924         PostSelectionChangeNotification();
925     }
926     wxString str = dataSource->GetItemText(this, row, col);
927     editText->SetValue(str);
928     editText->Show();
929     editText->SetFocus();
930     editText->SelectAll();
931     editRow = row;
932     editColumn = col;
933 }
934
935 void
936 MyListCtrl::FinalizeEdit()
937 {
938     if (editText != NULL) {
939         wxString sval = editText->GetValue();
940         if (dataSource)
941             dataSource->SetItemText(this, editRow, editColumn, sval);
942     }
943 }
944
945 void
946 MyListCtrl::EndEditText(bool setValueFlag)
947 {
948     if (!editText)
949         return;
950     if (setValueFlag)
951         FinalizeEdit();
952     editText->Hide();
953     editText->Destroy();
954     editText = NULL;
955     scroll->SetFocus();
956 }
957
958 void
959 MyListCtrl::EndEditTextAndRestart(bool setValueFlag, int newRow, int newCol)
960 {
961     EndEditText(setValueFlag);
962     StartEditText(newRow, newCol);
963 }
964
965 void
966 MyListCtrl::OnCharInText(wxKeyEvent &event)
967 {
968     int kc = event.GetKeyCode();
969     bool shiftDown = event.ShiftDown();
970     bool altDown = event.AltDown();
971     int row = editRow;
972     int col = editColumn;
973     if (kc == WXK_RETURN) {
974         if (altDown) {
975             EndEditText();
976         } else {
977             do {
978                 if (shiftDown) {
979                     if (row <= 0)
980                         row = -1;
981                     else
982                         row--;
983                 } else {
984                     if (row >= nrows - 1)
985                         row = -1;
986                     else
987                         row++;
988                 }
989             } while (row >= 0 && !dataSource->IsItemEditable(this, row, col));
990             if (row >= 0) {
991                 StartEditText(row, col);
992             } else {
993                 EndEditText();
994             }
995         }
996     } else if (kc == WXK_NUMPAD_ENTER) {
997         EndEditText();
998     } else if (kc == WXK_TAB) {
999         do {
1000             if (shiftDown) {
1001                 col--;
1002                 if (col < 0) {
1003                     col = ncols - 1;
1004                     row--;
1005                     if (row < 0)
1006                         row = -1;
1007                 }
1008             } else {
1009                 col++;
1010                 if (col >= ncols) {
1011                     col = 0;
1012                     row++;
1013                     if (row >= nrows)
1014                         row = -1;
1015                 }
1016             }
1017         } while (row >= 0 && !dataSource->IsItemEditable(this, row, col));
1018         if (row >= 0) {
1019             StartEditText(row, col);
1020         } else
1021             EndEditText();
1022     } else
1023         event.Skip();
1024 }
1025
1026 void
1027 MyListCtrl::OnCharHookInText(wxKeyEvent &event)
1028 {
1029 #if defined(__WXMAC__) || defined(__WXOSX__)
1030     //  On macOS, shift-TAB is consumed by wxWidgets even when wxTE_PROCESS_TAB and wxWANTS_CHARS
1031     //  are specified (why?); so, we intercept the TAB and shift-TAB here
1032     int kc = event.GetKeyCode();
1033     if (kc == WXK_TAB || kc == WXK_NUMPAD_TAB) {
1034         OnCharInText(event);
1035     } else event.Skip();
1036 #else
1037     event.Skip();
1038 #endif
1039 }
1040
1041 void
1042 MyListCtrl::OnCharInScroll(wxKeyEvent &event)
1043 {
1044     int kc = event.GetKeyCode();
1045     if (kc == WXK_DOWN || kc == WXK_UP) {
1046         int row;
1047         if (selection.size() > 0) {
1048             if (selection.size() > 1) {
1049                 if (lastMouseRow >= 0 && lastMouseRow < nrows)
1050                     row = lastMouseRow;
1051                 else if (kc == WXK_DOWN)
1052                     row = selection[selection.size() - 1];
1053                 else
1054                     row = selection[0];
1055             } else row = selection[0];
1056             if (kc == WXK_UP && row > 0)
1057                 row--;
1058             else if (kc == WXK_DOWN && row < nrows - 1)
1059                 row++;
1060             else return;  //  Ignore key
1061             UnselectAllRows();
1062             SelectRow(row);
1063             PostSelectionChangeNotification();
1064             Refresh();
1065             lastMouseRow = row;  //  Fake as if this row was clicked
1066         }
1067     } else event.Skip();
1068 }
1069
1070 void
1071 MyListCtrl::OnSetFocusInScroll(wxFocusEvent &event)
1072 {
1073     Refresh();
1074 }
1075
1076 void
1077 MyListCtrl::OnKillFocusInScroll(wxFocusEvent &event)
1078 {
1079     Refresh();
1080 }
1081
1082 void
1083 MyListCtrl::OnPopUpMenuSelected(wxCommandEvent &event)
1084 {
1085         if (dataSource != NULL)
1086                 dataSource->OnPopUpMenuSelected(this, lastPopUpRow, lastPopUpColumn, event.GetId() - 1);
1087 }