github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/client/GUI/lists/lists.go (about) 1 package lists 2 3 import ( 4 "fmt" 5 "sort" 6 7 dm "github.com/benoitkugler/goACVE/server/core/datamodel" 8 rd "github.com/benoitkugler/goACVE/server/core/rawdata" 9 10 "github.com/benoitkugler/goACVE/client/GUI/basic" 11 "github.com/therecipe/qt/core" 12 "github.com/therecipe/qt/gui" 13 "github.com/therecipe/qt/widgets" 14 ) 15 16 const debounceDelay = 200 // ms 17 18 // Liste est le widget top-level contenant une liste, 19 // une ligne de titre, et un bouton d'ajout. 20 // Attention à bien appeler `Init` ! 21 type Liste struct { 22 *widgets.QFrame 23 24 model model 25 View view 26 labelTitle *widgets.QLabel 27 searchField *widgets.QLineEdit // nil if OnSearch is nil 28 29 Headers []rd.Header 30 Title string 31 HighlightOnHover bool 32 SingleSelection bool 33 Placeholder string 34 MinHeight int 35 MinWidth int 36 MaxWidth int 37 38 // Callback d'ajout d'un élément, déclenché par le bouton nouveau. 39 OnAdd func() 40 41 // index est valide uniquement pour une Table (ambigu pour un Tree) 42 OnClick func(acces rd.Item, index int) 43 44 OnDoubleClick func(acces rd.Item, index int) 45 46 OnRightClick func(acces rd.Item, pos *core.QPoint) 47 48 // Callback déclenché par le tri. 49 OnSort func(field rd.Field, reverse bool) 50 51 // Callback déclenché sur recherche de l'utilisateur 52 OnSearch func(pattern string) 53 54 // Valide seulement pour une Table 55 OnDelete func(acces rd.Item, index int) 56 57 // Widgets supplémentaires en haut à droite 58 RightHeader *widgets.QHBoxLayout 59 } 60 61 type Table struct { 62 Liste 63 64 HideHeaders bool 65 ResizeToContents bool 66 } 67 68 type Tree struct { 69 Liste 70 71 FoldButton *widgets.QPushButton 72 } 73 74 type view interface { 75 widgets.QWidget_ITF 76 77 SetObjectName(string) 78 SetModel(model core.QAbstractItemModel_ITF) 79 Model() *core.QAbstractItemModel 80 SetSortingEnabled(bool) 81 SetAlternatingRowColors(bool) 82 SetItemDelegate(itf widgets.QAbstractItemDelegate_ITF) 83 84 SetMouseTracking(bool) 85 SetProperty(string, core.QVariant_ITF) bool 86 87 SetSelectionBehavior(behavior widgets.QAbstractItemView__SelectionBehavior) 88 SetSelectionMode(mode widgets.QAbstractItemView__SelectionMode) 89 90 SetSizeAdjustPolicy(policy widgets.QAbstractScrollArea__SizeAdjustPolicy) 91 SetSizePolicy2(widgets.QSizePolicy__Policy, widgets.QSizePolicy__Policy) 92 SetMinimumHeight(int) 93 SetMinimumWidth(int) 94 SetMaximumWidth(int) 95 SetColumnWidth(int, int) 96 97 ConnectPaintEvent(f func(event *gui.QPaintEvent)) 98 PaintEventDefault(event gui.QPaintEvent_ITF) 99 Viewport() *widgets.QWidget 100 Rect() *core.QRect 101 ScrollTo(index core.QModelIndex_ITF, hint widgets.QAbstractItemView__ScrollHint) 102 103 ConnectActivated(f func(index *core.QModelIndex)) 104 ConnectPressed(f func(index *core.QModelIndex)) 105 ConnectDoubleClicked(f func(index *core.QModelIndex)) 106 107 SetContextMenuPolicy(policy core.Qt__ContextMenuPolicy) 108 ConnectCustomContextMenuRequested(f func(point *core.QPoint)) 109 IndexAt(point core.QPoint_ITF) *core.QModelIndex 110 111 ClearSelection() 112 SelectionModel() *core.QItemSelectionModel 113 CurrentIndex() *core.QModelIndex 114 SelectedIndexes() []*core.QModelIndex 115 116 ConnectKeyPressEvent(f func(event *gui.QKeyEvent)) 117 KeyPressEventDefault(event gui.QKeyEvent_ITF) 118 } 119 120 type model interface { 121 core.QAbstractItemModel_ITF 122 123 Index(int, int, core.QModelIndex_ITF) *core.QModelIndex 124 125 //remove(section int) 126 dataAt(index *core.QModelIndex) (rd.Item, bool) 127 128 getHeaders() []rd.Header 129 BeginResetModel() 130 EndResetModel() 131 ConnectColumnCount(f func(index *core.QModelIndex) int) 132 133 ConnectSort(f func(section int, reverse core.Qt__SortOrder)) 134 135 ConnectModelReset(func()) 136 ConnectRowsRemoved(func(parent *core.QModelIndex, first int, last int)) 137 } 138 139 // DefaultSort tri les données présentes et remplace les données du model 140 func DefaultSort(liste Table, field rd.Field, reverse bool) { 141 data := liste.Model().GetData() 142 dm.SortBy(data, field, reverse) 143 liste.Model().SetData(data) 144 } 145 146 func (l *Liste) init(_view view, _model model) { 147 l.model = _model 148 l.View = _view 149 150 l.model.ConnectColumnCount(func(index *core.QModelIndex) int { 151 return len(l.model.getHeaders()) 152 }) 153 154 l.View.SetSortingEnabled(true) 155 if l.OnSort != nil { // on transmet le field et l'ordre 156 l.model.ConnectSort(func(section int, reverse core.Qt__SortOrder) { 157 l.OnSort(l.model.getHeaders()[section].Field, reverse == 0) 158 }) 159 } 160 161 l.QFrame = basic.Frame() 162 l.QFrame.SetLayout(widgets.NewQVBoxLayout()) 163 l.QFrame.Layout().SetSpacing(1) 164 l.QFrame.Layout().SetContentsMargins(0, 0, 0, 0) 165 166 l.View.SetObjectName("liste") 167 l.View.SetModel(l.model) 168 l.View.SetAlternatingRowColors(true) 169 l.View.SetSizeAdjustPolicy(widgets.QAbstractScrollArea__AdjustToContentsOnFirstShow) // overided in table 170 l.View.SetSizePolicy2(widgets.QSizePolicy__Expanding, widgets.QSizePolicy__Expanding) 171 l.View.SetMinimumHeight(l.MinHeight) 172 l.View.SetMinimumWidth(l.MinWidth) 173 if l.MaxWidth > 0 { 174 l.View.SetMaximumWidth(l.MaxWidth) 175 } 176 177 l.View.ConnectPaintEvent(func(event *gui.QPaintEvent) { 178 l.View.PaintEventDefault(event) 179 drawPlaceholder(l.View, l.Placeholder) 180 }) 181 182 if l.HighlightOnHover { 183 l.View.SetMouseTracking(true) 184 l.View.SetProperty("highlight", core.NewQVariant9(true)) 185 } 186 187 l.View.SetSelectionBehavior(widgets.QAbstractItemView__SelectRows) 188 if l.SingleSelection { 189 l.View.SetSelectionMode(widgets.QAbstractItemView__SingleSelection) 190 } 191 192 if l.Title != "" || l.OnSearch != nil || l.RightHeader != nil { 193 l.QFrame.SetObjectName("cadre") 194 195 headerF := basic.Frame() 196 header := widgets.NewQHBoxLayout2(headerF) 197 header.SetContentsMargins(0, 0, 0, 3) 198 header.SetSpacing(1) 199 l.QFrame.Layout().SetContentsMargins(5, 8, 5, 5) 200 if l.Title != "" { 201 l.labelTitle = basic.Label(l.Title) 202 header.AddWidget(l.labelTitle, 0, 0) 203 } 204 205 l.setupOnSearch(header) 206 207 if l.RightHeader != nil { 208 header.AddStretch(2) 209 header.AddLayout(l.RightHeader, 0) 210 } 211 l.QFrame.Layout().AddWidget(headerF) 212 } 213 214 l.QFrame.Layout().AddWidget(l.View) 215 216 if l.OnAdd != nil { 217 newButton := basic.Button("Ajouter") 218 newButton.SetObjectName(basic.ONAdd) 219 newButton.ConnectClicked(func(_ bool) { l.OnAdd() }) 220 l.QFrame.Layout().AddWidget(newButton) 221 } 222 223 if l.OnClick != nil { 224 l.View.ConnectPressed(func(current *core.QModelIndex) { 225 if item, ok := l.model.dataAt(current); ok { 226 l.OnClick(item, current.Row()) 227 } 228 }) 229 } 230 231 if l.OnDoubleClick != nil { 232 l.View.ConnectDoubleClicked(func(index *core.QModelIndex) { 233 if item, ok := l.model.dataAt(index); ok { 234 l.OnDoubleClick(item, index.Row()) 235 } 236 }) 237 } 238 239 if l.OnRightClick != nil { 240 l.View.SetContextMenuPolicy(core.Qt__CustomContextMenu) 241 l.View.ConnectCustomContextMenuRequested(func(point *core.QPoint) { 242 index := l.View.IndexAt(point) 243 if item, ok := l.model.dataAt(index); ok { 244 l.OnRightClick(item, point) 245 } 246 }) 247 } 248 249 l.ConnectDataChanged(l.updateTitle) 250 l.updateTitle() 251 } 252 253 func debounce(field *widgets.QLineEdit, callback func(text string)) { 254 timer := core.NewQTimer(nil) 255 timer.SetSingleShot(true) 256 257 var finalText string 258 259 // quand l'utilisateur arrête de tapper on déclenche 260 // l'action avec le texte accumulé 261 timer.ConnectTimeout(func() { 262 callback(finalText) 263 }) 264 265 // quand l'utilisateur tape, on stocke le texte et 266 // on remet à zéro le Timer 267 field.ConnectTextEdited(func(text string) { 268 finalText = text 269 timer.Start(debounceDelay) 270 }) 271 } 272 273 func (l *Liste) setupOnSearch(header *widgets.QHBoxLayout) { 274 if l.OnSearch == nil { 275 return 276 } 277 278 l.searchField = widgets.NewQLineEdit(nil) 279 l.searchField.SetMaximumWidth(400) 280 l.searchField.SetPlaceholderText("Rechercher...") 281 282 // debounce pour éviter des ralentissements sur les grosses listes 283 debounce(l.searchField, l.OnSearch) 284 285 cancel := widgets.NewQToolButton(nil) 286 cancel.SetIcon(basic.Icons.Delete) 287 cancel.ConnectClicked(func(_ bool) { 288 l.OnSearch("") 289 l.SetSearch("") 290 l.searchField.SetFocus2() 291 }) 292 header.AddWidget(l.searchField, 1, 0) 293 header.AddWidget(cancel, 0, core.Qt__AlignLeft) 294 } 295 296 // SetSearch met à jour le champ de recherche, SANS 297 // déclencher la fonction de recherche 298 func (l *Liste) SetSearch(text string) { 299 if l.searchField == nil { 300 return 301 } 302 l.searchField.SetText(text) 303 } 304 305 func (l *Liste) ConnectDataChanged(f func()) { 306 l.model.ConnectModelReset(f) 307 l.model.ConnectRowsRemoved(func(_ *core.QModelIndex, _ int, _ int) { f() }) 308 } 309 310 func (l *Liste) updateTitle() { 311 if l.labelTitle == nil { 312 return 313 } 314 nb := l.View.Model().RowCount(core.NewQModelIndex()) 315 l.labelTitle.SetText(fmt.Sprintf("<b>%s</b> <i>(%d)</i>", l.Title, nb)) 316 } 317 318 func (l *Liste) Selection() rd.Ids { 319 indexes := l.View.SelectedIndexes() 320 ids := make(rd.Set, len(indexes)) 321 for _, index := range indexes { 322 if item, ok := l.model.dataAt(index); ok { 323 ids.Add(item.Id.Int64()) 324 } 325 } 326 out := ids.Keys() 327 sort.Slice(out, func(i, j int) bool { // déterminisme 328 return out[i] < out[j] 329 }) 330 return out 331 } 332 333 func (l *Liste) ShowContextMenu(menu *widgets.QMenu, pos *core.QPoint) { 334 menu.Exec2(l.View.QWidget_PTR().MapToGlobal(pos), nil) 335 } 336 337 func drawPlaceholder(view view, placeholder string) { 338 if view.Model().RowCount(core.NewQModelIndex()) == 0 { 339 painter := gui.NewQPainter2(view.Viewport()) 340 painter.SetFont(basic.ItalicFont) 341 painter.DrawText4(view.Rect().Adjusted(0, 0, -5, -5), int(core.Qt__AlignCenter)|int(core.Qt__TextWordWrap), placeholder, nil) 342 painter.DestroyQPainter() 343 } 344 } 345 346 func (t *Table) Init() { 347 view := widgets.NewQTableView(nil) 348 model := newTableModel() 349 model.headers = t.Headers 350 t.init(view, model) 351 352 view.VerticalHeader().SetVisible(t.OnDelete != nil) 353 view.VerticalHeader().ConnectSectionClicked(func(logicalIndex int) { 354 sourceIndex := t.model.Index(logicalIndex, 0, core.NewQModelIndex()) 355 if item, ok := t.model.dataAt(sourceIndex); ok { 356 t.OnDelete(item, sourceIndex.Row()) 357 } 358 }) 359 360 view.HorizontalHeader().SetStretchLastSection(true) 361 view.HorizontalHeader().SetVisible(!t.HideHeaders) 362 if t.ResizeToContents { 363 view.HorizontalHeader().SetSectionResizeMode(widgets.QHeaderView__ResizeToContents) 364 view.SetSizeAdjustPolicy(widgets.QAbstractScrollArea__AdjustToContents) 365 view.VerticalHeader().SetSectionResizeMode(widgets.QHeaderView__ResizeToContents) 366 } 367 } 368 369 func (t *Tree) Init() { 370 view := widgets.NewQTreeView(nil) 371 view.SetUniformRowHeights(true) 372 model := NewTreeModel(nil) 373 model.headers = t.Headers 374 t.SingleSelection = true 375 376 if t.RightHeader == nil { 377 t.RightHeader = widgets.NewQHBoxLayout() 378 } 379 t.FoldButton = basic.Button("Déplier") 380 t.FoldButton.SetCheckable(true) 381 t.RightHeader.AddWidget(t.FoldButton, 0, 0) 382 view.Header().SetStretchLastSection(true) 383 t.init(view, model) 384 } 385 386 func (t *Tree) Fold(fold bool) { 387 if fold { 388 t.View.(*widgets.QTreeView).ExpandAll() 389 t.FoldButton.SetText("Replier") 390 } else { 391 t.View.(*widgets.QTreeView).CollapseAll() 392 t.FoldButton.SetText("Déplier") 393 } 394 } 395 396 func selectIndex(view view, index *core.QModelIndex) { 397 view.SelectionModel().Select(index, core.QItemSelectionModel__Rows|core.QItemSelectionModel__Select) 398 view.ScrollTo(index, widgets.QAbstractItemView__EnsureVisible) 399 } 400 401 func (t *Table) SelectRow(id rd.IId) { 402 for i, v := range t.Model().GetData() { 403 if v.Id == id { 404 index := t.model.Index(i, 0, core.NewQModelIndex()) 405 selectIndex(t.View, index) 406 } 407 } 408 } 409 410 func (t *Tree) SelectRow(id rd.IId) { 411 t.View.ClearSelection() 412 model := t.Model() 413 for rowParent, v := range model.GetData() { 414 parentIndex := model.index(rowParent, 0, core.NewQModelIndex()) 415 if v.Id == id { // selection du parent 416 selectIndex(t.View, parentIndex) 417 return 418 } else { // parcourt des enfants 419 for rowEnfant, e := range v.Childs { 420 if e.Id == id { 421 enfantIndex := model.index(rowEnfant, 0, parentIndex) 422 selectIndex(t.View, enfantIndex) 423 t.View.(*widgets.QTreeView).Expand(parentIndex) 424 return 425 } 426 } 427 } 428 } 429 } 430 431 func (t *Table) HorizontalHeader() *widgets.QHeaderView { 432 return t.View.(*widgets.QTableView).HorizontalHeader() 433 } 434 435 func (t *Tree) HorizontalHeader() *widgets.QHeaderView { 436 return t.View.(*widgets.QTreeView).Header() 437 } 438 439 func (l *Liste) CurrentData() (rd.Item, bool) { 440 return l.model.dataAt(l.View.CurrentIndex()) 441 } 442 443 func (t *Table) Model() *TableModel { 444 return t.model.(*TableModel) 445 } 446 447 func (t *Tree) Model() *TreeModel { 448 return t.model.(*TreeModel) 449 } 450 451 // ------------------------------ Models ------------------------------ 452 453 func _headerData(section int, orientation core.Qt__Orientation, role int, headers []rd.Header) *core.QVariant { 454 switch orientation { 455 case core.Qt__Vertical: 456 switch role { 457 case int(core.Qt__DecorationRole): 458 return basic.Icons.Delete.ToVariant() 459 case int(core.Qt__ToolTipRole): 460 return core.NewQVariant15("Supprimer") 461 } 462 case core.Qt__Horizontal: 463 switch role { 464 case int(core.Qt__DisplayRole): 465 return core.NewQVariant15(headers[section].Label) 466 case int(core.Qt__TextAlignmentRole): 467 return core.NewQVariant7(int64(core.Qt__AlignCenter)) 468 case int(core.Qt__FontRole): 469 return basic.BoldFont.ToVariant() 470 } 471 } 472 return core.NewQVariant() 473 } 474 475 func safeColor(color rd.Color) *core.QVariant { 476 if color == nil { 477 return core.NewQVariant() 478 } 479 s := color.AHex() 480 if s == "" { 481 return core.NewQVariant() 482 } 483 return gui.NewQColor6(s).ToVariant() 484 } 485 486 func _data(index *core.QModelIndex, role int, headers []rd.Header, item rd.Item) *core.QVariant { 487 field := headers[index.Column()].Field 488 489 switch role { 490 case int(core.Qt__DisplayRole), int(core.Qt__ToolTipRole): 491 data := item.Fields.Data(field) 492 return core.NewQVariant15(data.String()) 493 case int(core.Qt__TextAlignmentRole): 494 return core.NewQVariant7(int64(core.Qt__AlignCenter)) 495 case int(core.Qt__FontRole): 496 if item.Bolds[field] { 497 return basic.BoldFont.ToVariant() 498 } 499 case int(core.Qt__BackgroundRole): 500 color := item.BackgroundColor(field) 501 return safeColor(color) 502 case int(core.Qt__ForegroundRole): 503 color := item.TextColor(field) 504 return safeColor(color) 505 case int(core.Qt__UserRole): 506 return core.NewQVariant5(int(field)) 507 } 508 509 return core.NewQVariant() 510 } 511 512 // ------------------------------------ Table ------------------------------------ 513 514 type TableModel struct { 515 *core.QAbstractTableModel 516 517 headers []rd.Header 518 modelData rd.Table 519 } 520 521 func newTableModel() *TableModel { 522 cm := TableModel{QAbstractTableModel: core.NewQAbstractTableModel(nil)} 523 cm.ConnectHeaderData(cm.headerData) 524 cm.ConnectRowCount(cm.rowCount) 525 cm.ConnectData(cm.data) 526 return &cm 527 } 528 529 func (m *TableModel) getHeaders() []rd.Header { 530 return m.headers 531 } 532 533 func (m *TableModel) rowCount(_ *core.QModelIndex) int { 534 return len(m.modelData) 535 } 536 537 func (m *TableModel) headerData(section int, orientation core.Qt__Orientation, role int) *core.QVariant { 538 return _headerData(section, orientation, role, m.headers) 539 } 540 541 func (m *TableModel) data(index *core.QModelIndex, role int) *core.QVariant { 542 return _data(index, role, m.headers, m.modelData[index.Row()]) 543 } 544 545 func (m *TableModel) remove(section int) { 546 m.BeginRemoveRows(core.NewQModelIndex(), section, section) 547 m.modelData = append(m.modelData[:section], m.modelData[section+1:]...) 548 m.EndRemoveRows() 549 } 550 551 func (m *TableModel) SetData(data rd.Table) { 552 m.BeginResetModel() 553 m.modelData = data 554 m.EndResetModel() 555 } 556 557 func (m *TableModel) GetData() rd.Table { 558 return m.modelData 559 } 560 561 func (m *TableModel) dataAt(index *core.QModelIndex) (rd.Item, bool) { 562 if !index.IsValid() { 563 return rd.Item{}, false 564 } 565 return m.modelData[index.Row()], true 566 }