Repository: biolab/orange3-text

Issue:
Corpus Viewer: retain settings
<!--
This is an issue template. Please fill in the relevant details in the
sections below.
-->

##### Text version
<!-- From menu _Options→Add-ons→Orange3-Text_ or code `orangecontrib.text.version.full_version` -->
0.2.4-dev

##### Orange version
<!-- From menu _Help→About→Version_ or code `Orange.version.full_version` -->
3.5.dev

##### Expected behavior
Corpus Viewer retains the selection 
- in 'Display features'. Settings retained regardless of the change on input.
- in query word.
- on the selected documents.


##### Actual behavior
Corpus Viewer retains no selections.


##### Steps to reproduce the behavior
Corpus (bookexcerpts) - Corpus Viewer. Type in 'doctor' and select the first 3 documents. Save the workflow and reload it. Selections are not saved.
For 'Display features', use Election Tweets and set to display only Text. Upon new input, all the features are displayed (should be only Text). [if domains don't match, everything reset completely]


##### Additional info (worksheets, data, screenshots, ...)





Discussion:

Resolving patch:
diff --git a/orangecontrib/text/widgets/owcorpusviewer.py b/orangecontrib/text/widgets/owcorpusviewer.py
--- a/orangecontrib/text/widgets/owcorpusviewer.py
+++ b/orangecontrib/text/widgets/owcorpusviewer.py
@@ -3,7 +3,7 @@
 import sre_constants
 from itertools import chain
 
-from AnyQt.QtCore import Qt, QUrl
+from AnyQt.QtCore import Qt, QUrl, QItemSelection, QItemSelectionModel, QItemSelectionRange
 from AnyQt.QtGui import QStandardItemModel, QStandardItem
 from AnyQt.QtWidgets import (QListView, QSizePolicy, QTableView,
                              QAbstractItemView, QHeaderView, QSplitter,
@@ -12,8 +12,7 @@
 from Orange.data import Table
 from Orange.data.domain import filter_visible
 from Orange.widgets import gui, widget
-from Orange.widgets.settings import Setting, ContextSetting
-from Orange.widgets.utils.webview import HAVE_WEBKIT
+from Orange.widgets.settings import Setting, ContextSetting, PerfectDomainContextHandler
 from Orange.widgets.widget import OWWidget, Msg
 from orangecontrib.text.corpus import Corpus
 
@@ -33,8 +32,17 @@ class OWCorpusViewer(OWWidget):
     inputs = [(IO.DATA, Table, 'set_data')]
     outputs = [(IO.MATCHED, Corpus, widget.Default), (IO.UNMATCHED, Corpus)]
 
-    search_indices = ContextSetting([0])   # features included in search
-    display_indices = ContextSetting([0])  # features for display
+    settingsHandler = PerfectDomainContextHandler(
+        match_values = PerfectDomainContextHandler.MATCH_VALUES_ALL
+    )
+
+    search_indices = ContextSetting([], exclude_metas=False)   # features included in search
+    display_indices = ContextSetting([], exclude_metas=False)  # features for display
+    display_features = ContextSetting([], exclude_metas=False)
+    regexp_filter = ContextSetting("")
+
+    selection = Setting([0], schema_only=True)
+
     show_tokens = Setting(False)
     autocommit = Setting(True)
 
@@ -50,7 +58,7 @@ def __init__(self):
         self.output_mask = []           # Output corpus indices
         self.doc_webview = None         # WebView for showing content
         self.search_features = []       # two copies are needed since Display allows drag & drop
-        self.display_features = []
+        self.display_list_indices = [0]
 
         # Info attributes
         self.update_info()
@@ -67,12 +75,12 @@ def __init__(self):
         self.search_listbox = gui.listBox(
             self.controlArea, self, 'search_indices', 'search_features',
             selectionMode=QListView.ExtendedSelection,
-            box='Search features', callback=self.regenerate_docs,)
+            box='Search features', callback=self.search_features_changed)
 
         # Display features
         display_box = gui.widgetBox(self.controlArea, 'Display features')
         self.display_listbox = gui.listBox(
-            display_box, self, 'display_indices', 'display_features',
+            display_box, self, 'display_list_indices', 'display_features',
             selectionMode=QListView.ExtendedSelection,
             callback=self.show_docs, enableDragDrop=True)
         self.show_tokens_checkbox = gui.checkBox(display_box, self, 'show_tokens',
@@ -82,7 +90,7 @@ def __init__(self):
         gui.auto_commit(self.controlArea, self, 'autocommit', 'Send data', 'Auto send is on')
 
         # Search
-        self.filter_input = gui.lineEdit(self.mainArea, self, '',
+        self.filter_input = gui.lineEdit(self.mainArea, self, 'regexp_filter',
                                          orientation=Qt.Horizontal,
                                          sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,
                                                                 QSizePolicy.Fixed),
@@ -118,13 +126,30 @@ def copy_to_clipboard(self):
         QApplication.clipboard().setText(text)
 
     def set_data(self, data=None):
+        self.closeContext()
         self.reset_widget()
         self.corpus = data
+        self.search_features = []
         if data is not None:
             if not isinstance(data, Corpus):
                 self.corpus = Corpus.from_table(data.domain, data)
-            self.load_features()
+            domain = self.corpus.domain
+
+            self.search_features = list(filter_visible(chain(domain.variables, domain.metas)))
+            self.display_features = list(filter_visible(chain(domain.variables, domain.metas)))
+            self.search_indices = list(range(len(self.search_features)))
+            self.display_indices = list(range(len(self.display_features)))
+            self.openContext(self.corpus)
+            self.display_list_indices = self.display_indices
             self.regenerate_docs()
+            self.list_docs()
+            self.update_info()
+            self.set_selection()
+            self.show_docs()
+            # Enable/disable tokens checkbox
+            if not self.corpus.has_tokens():
+                self.show_tokens_checkbox.setCheckState(False)
+            self.show_tokens_checkbox.setEnabled(self.corpus.has_tokens())
         self.commit()
 
     def reset_widget(self):
@@ -132,6 +157,7 @@ def reset_widget(self):
         self.corpus = None
         self.corpus_docs = None
         self.output_mask = []
+        self.display_features = []
         # Widgets
         self.search_listbox.clear()
         self.display_listbox.clear()
@@ -139,7 +165,6 @@ def reset_widget(self):
         self.update_info()
         # Models/vars
         self.search_features.clear()
-        self.display_features.clear()
         self.search_indices.clear()
         self.display_indices.clear()
         self.doc_list_model.clear()
@@ -148,25 +173,11 @@ def reset_widget(self):
         # WebView
         self.doc_webview.setHtml('')
 
-    def load_features(self):
-        self.search_indices = []
-        self.display_indices = []
-        if self.corpus is not None:
-            domain = self.corpus.domain
-            self.search_features = list(filter_visible(chain(domain.variables, domain.metas)))
-            self.display_features = list(filter_visible(chain(domain.variables, domain.metas)))
-            # FIXME: Select features based on ContextSetting
-            self.search_indices = list(range(len(self.search_features)))
-            self.display_indices = list(range(len(self.display_features)))
-
-            # Enable/disable tokens checkbox
-            if not self.corpus.has_tokens():
-                self.show_tokens_checkbox.setCheckState(False)
-            self.show_tokens_checkbox.setEnabled(self.corpus.has_tokens())
-
     def list_docs(self):
         """ List documents into the left scrolling area """
-        search_keyword = self.filter_input.text().strip('|')
+        if self.corpus_docs is None:
+            return
+        search_keyword = self.regexp_filter.strip('|')
         try:
             reg = re.compile(search_keyword, re.IGNORECASE)
         except sre_constants.error:
@@ -187,11 +198,26 @@ def is_match(x):
                 self.doc_list_model.appendRow(item)
                 self.output_mask.append(i)
 
+    def reset_selection(self):
         if self.doc_list_model.rowCount() > 0:
-            self.doc_list.selectRow(0)          # Select the first document
+            self.doc_list.selectRow(0)  # Select the first document
         else:
             self.doc_webview.setHtml('')
-        self.commit()
+
+    def set_selection(self):
+        view = self.doc_list
+        if len(self.selection):
+            selection = QItemSelection()
+
+            for row in self.selection:
+                selection.append(
+                    QItemSelectionRange(
+                        view.model().index(row, 0),
+                        view.model().index(row, 0)
+                    )
+                )
+            view.selectionModel().select(
+                selection, QItemSelectionModel.ClearAndSelect)
 
     def show_docs(self):
         """ Show the selected documents in the right area """
@@ -242,6 +268,7 @@ def show_docs(self):
         </body>
         </html>
         '''
+        self.display_indices = self.display_list_indices
         if self.corpus is None:
             return
 
@@ -256,6 +283,9 @@ def show_docs(self):
                                   if i in self.search_indices]
 
         html = '<table>'
+        selection = [i.row() for i in self.doc_list.selectionModel().selectedRows()]
+        if selection != []:
+            self.selection = selection
         for doc_count, index in enumerate(self.doc_list.selectionModel().selectedRows()):
             if doc_count > 0:   # add split
                 html += '<tr class="line separator"><td/><td/></tr>' \
@@ -291,6 +321,10 @@ def load_js(self):
         for script in ('jquery-3.1.1.min.js', 'jquery.mark.min.js', 'highlighter.js', ):
             self.doc_webview.evalJS(open(os.path.join(resources, script), encoding='utf-8').read())
 
+    def search_features_changed(self):
+        self.regenerate_docs()
+        self.refresh_search()
+
     def regenerate_docs(self):
         self.corpus_docs = None
         self.Warning.no_feats_search.clear()
@@ -299,15 +333,16 @@ def regenerate_docs(self):
             if len(feats) == 0:
                 self.Warning.no_feats_search()
             self.corpus_docs = self.corpus.documents_from_features(feats)
-            self.refresh_search()
 
     def refresh_search(self):
         if self.corpus is not None:
             self.list_docs()
+            self.reset_selection()
             self.update_info()
+            self.commit()
 
     def highlight_docs(self):
-        search_keyword = self.filter_input.text().\
+        search_keyword = self.regexp_filter.\
             strip('|').replace('\\', '\\\\')    # escape one \ to  two for mark.js
         if search_keyword:
             self.doc_webview.evalJS('mark("{}");'.format(search_keyword))


Test patch: