33from typing import Union , Dict , List
44
55import numpy as np
6+ from sklearn .exceptions import UndefinedMetricWarning
67
78from AnyQt .QtWidgets import QHeaderView , QStyledItemDelegate , QMenu , \
8- QApplication
9- from AnyQt .QtGui import QStandardItemModel , QStandardItem , QClipboard
9+ QApplication , QToolButton
10+ from AnyQt .QtGui import QStandardItemModel , QStandardItem , QClipboard , QColor
1011from AnyQt .QtCore import Qt , QSize , QObject , pyqtSignal as Signal , \
1112 QSortFilterProxyModel
12- from sklearn .exceptions import UndefinedMetricWarning
13+
14+ from orangewidget .gui import OrangeUserRole
1315
1416from Orange .data import Domain , Variable
1517from Orange .evaluation import scoring
@@ -128,7 +130,84 @@ def is_bad(x):
128130 return left < right
129131
130132
131- DEFAULT_HINTS = {"Model_" : True , "Train" : False , "Test" : False }
133+ DEFAULT_HINTS = {"Model_" : True , "Train_" : False , "Test_" : False }
134+
135+
136+ class PersistentMenu (QMenu ):
137+ def mouseReleaseEvent (self , e ):
138+ action = self .activeAction ()
139+ if action :
140+ action .setEnabled (False )
141+ super ().mouseReleaseEvent (e )
142+ action .setEnabled (True )
143+ action .trigger ()
144+ else :
145+ super ().mouseReleaseEvent (e )
146+
147+
148+ class SelectableColumnsHeader (QHeaderView ):
149+ SelectMenuRole = next (OrangeUserRole )
150+ ShownHintRole = next (OrangeUserRole )
151+ sectionVisibleChanged = Signal (int , bool )
152+
153+ def __init__ (self , shown_columns_hints , * args , ** kwargs ):
154+ super ().__init__ (Qt .Horizontal , * args , ** kwargs )
155+ self .show_column_hints = shown_columns_hints
156+ self .button = QToolButton (self )
157+ self .button .setArrowType (Qt .DownArrow )
158+ self .button .setFixedSize (24 , 12 )
159+ col = self .button .palette ().color (self .button .backgroundRole ())
160+ self .button .setStyleSheet (
161+ f"border: none; background-color: { col .name (QColor .NameFormat .HexRgb )} " )
162+ self .setContextMenuPolicy (Qt .CustomContextMenu )
163+ self .customContextMenuRequested .connect (self .show_column_chooser )
164+ self .button .clicked .connect (self ._on_button_clicked )
165+
166+ def showEvent (self , e ):
167+ self ._set_pos ()
168+ self .button .show ()
169+ super ().showEvent (e )
170+
171+ def resizeEvent (self , e ):
172+ self ._set_pos ()
173+ super ().resizeEvent (e )
174+
175+ def _set_pos (self ):
176+ w , h = self .button .width (), self .button .height ()
177+ vw , vh = self .viewport ().width (), self .viewport ().height ()
178+ self .button .setGeometry (vw - w , (vh - h ) // 2 , w , h )
179+
180+ def __data (self , section , role ):
181+ return self .model ().headerData (section , Qt .Horizontal , role )
182+
183+ def show_column_chooser (self , pos ):
184+ # pylint: disable=unsubscriptable-object, unsupported-assignment-operation
185+ menu = PersistentMenu ()
186+ for section in range (self .count ()):
187+ name , enabled = self .__data (section , self .SelectMenuRole )
188+ hint_id = self .__data (section , self .ShownHintRole )
189+ action = menu .addAction (name )
190+ action .setDisabled (not enabled )
191+ action .setCheckable (True )
192+ action .setChecked (self .show_column_hints [hint_id ])
193+
194+ @action .triggered .connect # pylint: disable=cell-var-from-loop
195+ def update (checked , q = hint_id , section = section ):
196+ self .show_column_hints [q ] = checked
197+ self .setSectionHidden (section , not checked )
198+ self .sectionVisibleChanged .emit (section , checked )
199+ self .resizeSections (self .ResizeToContents )
200+
201+ pos .setY (self .viewport ().height ())
202+ menu .exec (self .mapToGlobal (pos ))
203+
204+ def _on_button_clicked (self ):
205+ self .show_column_chooser (self .button .pos ())
206+
207+ def update_shown_columns (self ):
208+ for section in range (self .count ()):
209+ hint_id = self .__data (section , self .ShownHintRole )
210+ self .setSectionHidden (section , not self .show_column_hints [hint_id ])
132211
133212
134213class ScoreTable (OWComponent , QObject ):
@@ -138,6 +217,7 @@ class ScoreTable(OWComponent, QObject):
138217 # backwards compatibility
139218 @property
140219 def shown_scores (self ):
220+ # pylint: disable=unsubscriptable-object
141221 column_names = {
142222 self .model .horizontalHeaderItem (col ).data (Qt .DisplayRole )
143223 for col in range (1 , self .model .columnCount ())}
@@ -166,65 +246,45 @@ def __init__(self, master):
166246 header .setSectionResizeMode (QHeaderView .ResizeToContents )
167247 header .setDefaultAlignment (Qt .AlignCenter )
168248 header .setStretchLastSection (False )
169- header .setContextMenuPolicy (Qt .CustomContextMenu )
170- header .customContextMenuRequested .connect (self .show_column_chooser )
171249
172250 for score in Score .registry .values ():
173251 self .show_score_hints .setdefault (score .__name__ , score .default_visible )
174252
175253 self .model = QStandardItemModel (master )
176- self .model .setHorizontalHeaderLabels (["Method" ])
254+ header = SelectableColumnsHeader (self .show_score_hints )
255+ header .setSectionsClickable (True )
256+ self .view .setHorizontalHeader (header )
177257 self .sorted_model = ScoreModel ()
178258 self .sorted_model .setSourceModel (self .model )
179259 self .view .setModel (self .sorted_model )
180260 self .view .setItemDelegate (self .ItemDelegate ())
181-
182- def show_column_chooser (self , pos ):
183- menu = QMenu ()
184- header = self .view .horizontalHeader ()
185- for col in range (1 , self .model .columnCount ()):
186- item = self .model .horizontalHeaderItem (col )
187- qualname = item .data (Qt .UserRole )
188- if col < 3 :
189- option = item .data (Qt .DisplayRole )
190- else :
191- score = Score .registry [qualname ]
192- option = score .long_name
193- if score .name != score .long_name :
194- option += f" ({ score .name } )"
195- action = menu .addAction (option )
196- action .setCheckable (True )
197- action .setChecked (self .show_score_hints [qualname ])
198-
199- @action .triggered .connect
200- def update (checked , q = qualname ):
201- self .show_score_hints [q ] = checked
202- self ._update_shown_columns ()
203-
204- menu .exec (header .mapToGlobal (pos ))
205-
206- def _update_shown_columns (self ):
207- self .view .resizeColumnsToContents ()
208- header = self .view .horizontalHeader ()
209- for section in range (1 , header .count ()):
210- qualname = self .model .horizontalHeaderItem (section ).data (Qt .UserRole )
211- header .setSectionHidden (section , not self .show_score_hints [qualname ])
212- self .shownScoresChanged .emit ()
261+ header .sectionVisibleChanged .connect (self .shownScoresChanged .emit )
262+ self .sorted_model .dataChanged .connect (self .view .resizeColumnsToContents )
213263
214264 def update_header (self , scorers : List [Score ]):
215265 self .model .setColumnCount (3 + len (scorers ))
216- for i , name , id_ in ((0 , "Model" , "Model_" ),
217- (1 , "Train time [s]" , "Train" ),
218- (2 , "Test time [s]" , "Test" )):
266+ SelectMenuRole = SelectableColumnsHeader .SelectMenuRole
267+ ShownHintRole = SelectableColumnsHeader .ShownHintRole
268+ for i , name , long_name , id_ , in ((0 , "Model" , "Model" , "Model_" ),
269+ (1 , "Train" , "Train time [s]" , "Train_" ),
270+ (2 , "Test" , "Test time [s]" , "Test_" )):
219271 item = QStandardItem (name )
220- item .setData (id_ , Qt .UserRole )
272+ item .setData ((long_name , i != 0 ), SelectMenuRole )
273+ item .setData (id_ , ShownHintRole )
274+ item .setToolTip (long_name )
221275 self .model .setHorizontalHeaderItem (i , item )
222276 for col , score in enumerate (scorers , start = 3 ):
223277 item = QStandardItem (score .name )
224- item .setData (score .__name__ , Qt .UserRole )
278+ name = score .long_name
279+ if name != score .name :
280+ name += f" ({ score .name } )"
281+ item .setData ((name , True ), SelectMenuRole )
282+ item .setData (score .__name__ , ShownHintRole )
225283 item .setToolTip (score .long_name )
226284 self .model .setHorizontalHeaderItem (col , item )
227- self ._update_shown_columns ()
285+
286+ self .view .horizontalHeader ().update_shown_columns ()
287+ self .view .resizeColumnsToContents ()
228288
229289 def copy_selection_to_clipboard (self ):
230290 mime = table_selection_to_mime_data (self .view )
0 commit comments