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  }