11import concurrent .futures
22from dataclasses import dataclass
3- from typing import Optional , Union , Sequence , TypedDict , Tuple
3+ from typing import Optional , Union , Sequence , List , TypedDict , Tuple , Dict
44
55from scipy .sparse import issparse
66
1010from AnyQt .QtCore import Slot
1111
1212import Orange .data
13+ from Orange .data import Variable
1314from Orange .data .table import Table
1415from Orange .data .sql .table import SqlTable
1516
1920from Orange .widgets .utils .itemdelegates import TableDataDelegate
2021from Orange .widgets .utils .tableview import table_selection_to_mime_data
2122from Orange .widgets .utils .widgetpreview import WidgetPreview
22- from Orange .widgets .widget import OWWidget , Input , Output
23+ from Orange .widgets .widget import OWWidget , Input , Output , Msg
2324from Orange .widgets .utils .annotated_data import (create_annotated_table ,
2425 ANNOTATED_DATA_SIGNAL_NAME )
2526from Orange .widgets .utils .itemmodels import TableModel
2627from Orange .widgets .utils .state_summary import format_summary_details
28+ from Orange .widgets .utils import disconnected
2729from Orange .widgets .data .utils .tableview import RichTableView
2830from Orange .widgets .data .utils import tablesummary as tsummary
2931
@@ -48,6 +50,9 @@ class _Selection(TypedDict):
4850 columns : Tuple [int ]
4951
5052
53+ _Sorting = List [Tuple [str , int ]]
54+
55+
5156class OWTable (OWWidget ):
5257 name = "Data Table"
5358 description = "View the dataset in a spreadsheet."
@@ -62,6 +67,15 @@ class Outputs:
6267 selected_data = Output ("Selected Data" , Table , default = True )
6368 annotated_data = Output (ANNOTATED_DATA_SIGNAL_NAME , Table )
6469
70+ class Warning (OWWidget .Warning ):
71+ missing_sort_columns = Msg (
72+ "Cannot restore sorting.\n "
73+ "Missing columns in input table: {}"
74+ )
75+ non_sortable_input = Msg (
76+ "Cannot restore sorting.\n "
77+ "Input table cannot be sorted due to implementation constraints."
78+ )
6579 buttons_area_orientation = Qt .Vertical
6680
6781 show_distributions = Setting (False )
@@ -73,12 +87,16 @@ class Outputs:
7387 stored_selection : _Selection = Setting (
7488 {"rows" : [], "columns" : []}, schema_only = True
7589 )
90+ stored_sort : _Sorting = Setting (
91+ [], schema_only = True
92+ )
7693 settings_version = 1
7794
7895 def __init__ (self ):
7996 super ().__init__ ()
8097 self .input : Optional [InputData ] = None
81- self .__pending_selection = self .stored_selection
98+ self .__pending_selection : Optional [_Selection ] = self .stored_selection
99+ self .__pending_sort : Optional [_Sorting ] = self .stored_sort
82100 self .dist_color = QColor (220 , 220 , 220 , 255 )
83101
84102 info_box = gui .vBox (self .controlArea , "Info" )
@@ -124,7 +142,9 @@ def __init__(self):
124142 header .setSectionsClickable (True )
125143 header .setSortIndicatorShown (True )
126144 header .setSortIndicator (- 1 , Qt .AscendingOrder )
127- header .sortIndicatorChanged .connect (self .update_selection )
145+ header .sortIndicatorChanged .connect (
146+ self ._on_sort_indicator_changed , Qt .UniqueConnection
147+ )
128148
129149 self .view = view
130150 self .mainArea .layout ().addWidget (self .view )
@@ -154,6 +174,8 @@ def set_dataset(self, data: Optional[Table]):
154174
155175 def handleNewSignals (self ):
156176 super ().handleNewSignals ()
177+ self .Warning .non_sortable_input .clear ()
178+ self .Warning .missing_sort_columns .clear ()
157179 data : Optional [Table ] = self .input .table if self .input else None
158180 slot = self .input
159181 if slot is not None and isinstance (slot .summary .len , concurrent .futures .Future ):
@@ -164,6 +186,9 @@ def update(_):
164186
165187 self ._update_input_summary ()
166188
189+ if data is not None and self .__pending_sort is not None :
190+ self .__restore_sort ()
191+
167192 if data is not None and self .__pending_selection is not None :
168193 selection = self .__pending_selection
169194 self .__pending_selection = None
@@ -296,11 +321,87 @@ def _on_select_rows_changed(self):
296321 def restore_order (self ):
297322 """Restore the original data order of the current view."""
298323 self .view .sortByColumn (- 1 , Qt .AscendingOrder )
324+ self .stored_sort = []
325+ self .Warning .missing_sort_columns .clear ()
299326
300327 @Slot ()
301328 def _update_info (self ):
302329 self ._update_input_summary ()
303330
331+ def _on_sort_indicator_changed (self , index : int , order : Qt .SortOrder ) -> None :
332+ if index == - 1 :
333+ self .stored_sort = []
334+ elif self .input is not None :
335+ model = self .input .model
336+ var = model .headerData (index , Qt .Horizontal , TableModel .VariableRole )
337+ order = - 1 if order == Qt .DescendingOrder else 1
338+ # Drop any previously applied sort on this column
339+ self .stored_sort = [(n , d ) for n , d in self .stored_sort
340+ if n != var .name ]
341+ self .stored_sort .append ((var .name , order ))
342+ self .update_selection ()
343+ self .Warning .missing_sort_columns .clear ()
344+
345+ def set_sort_columns (self , sorting : List [Tuple [str , int ]]):
346+ """
347+ Set the model sorting parameters.
348+
349+ Parameters
350+ ----------
351+ sorting: List[Tuple[str, int]]
352+ For each (name: str, inc: int) tuple where `name` is the column
353+ name and `inc` is 1 for increasing order and -1 for decreasing
354+ order, the model is sorted by that column.
355+ """
356+ if self .input is None :
357+ return # pragma: no cover
358+ self .stored_sort = []
359+ # Map header names/titles to column indices
360+ columns = {var .name : i for
361+ var , i in self .__header_variable_indices ().items ()}
362+ # Suppress the _on_sort_indicator_changed -> commit calls
363+ with disconnected (self .view .horizontalHeader ().sortIndicatorChanged ,
364+ self ._on_sort_indicator_changed , Qt .UniqueConnection ):
365+ for name , order in sorting :
366+ if name in columns :
367+ self .view .sortByColumn (
368+ columns [name ],
369+ Qt .AscendingOrder if order == 1 else Qt .DescendingOrder
370+ )
371+ self .stored_sort .append ((name , order ))
372+
373+ def __restore_sort (self ) -> None :
374+ assert self .input is not None
375+ sort = self .__pending_sort
376+ self .__pending_sort = None
377+ if sort is None :
378+ return # pragma: no cover
379+ if not self .view .isSortingEnabled () and sort :
380+ self .Warning .non_sortable_input ()
381+ self .Warning .missing_sort_columns .clear ()
382+ return
383+ # Map header names/titles to column indices
384+ vars_ = self .__header_variable_indices ()
385+ columns = {var .name : i for var , i in vars_ .items ()}
386+ missing_columns = []
387+ sort_ = []
388+ for name , order in sort :
389+ if name in columns :
390+ sort_ .append ((name , order ))
391+ else :
392+ missing_columns .append (name )
393+ self .set_sort_columns (sort_ )
394+ if missing_columns :
395+ self .Warning .missing_sort_columns (", " .join (missing_columns ))
396+
397+ def __header_variable_indices (self ) -> Dict [Variable , int ]:
398+ model = self .view .model ()
399+ if model is None :
400+ return {} # pragma: no cover
401+ vars_ = [model .headerData (i , Qt .Horizontal , TableModel .VariableRole )
402+ for i in range (model .columnCount ())]
403+ return {v : i for i , v in enumerate (vars_ ) if isinstance (v , Variable )}
404+
304405 def update_selection (self , * _ ):
305406 self .commit .deferred ()
306407
@@ -360,11 +461,16 @@ def select_vars(role):
360461 metas = select_vars (TableModel .Meta )
361462 domain = Orange .data .Domain (attrs , class_vars , metas )
362463
363- # Send all data by default
364- if not rowsel :
365- selected_data = table
366- else :
464+ sortsection = self .view .horizontalHeader ().sortIndicatorSection ()
465+ if rowsel :
367466 selected_data = table .from_table (domain , table , rowsel )
467+ elif sortsection != - 1 :
468+ # Send sorted data
469+ permutation = model .mapToSourceRows (...)
470+ selected_data = table .from_table (table .domain , table , permutation )
471+ else :
472+ # Send all data by default
473+ selected_data = table
368474
369475 self .Outputs .selected_data .send (selected_data )
370476 self .Outputs .annotated_data .send (create_annotated_table (table , rowsel ))
0 commit comments