Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FIX] File Widget: Fix recent urls save/restore #6259

Merged
merged 3 commits into from
Jan 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions Orange/widgets/data/owfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
open_filename_dialog, stored_recent_paths_prepend
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.widget import Output, Msg
from Orange.widgets.utils.combobox import TextEditCombo


# Backward compatibility: class RecentPath used to be defined in this module,
# and it is used in saved (pickled) settings. It must be imported into the
Expand Down Expand Up @@ -232,13 +234,12 @@ def package(w):
rb_button = gui.appendRadioButton(vbox, "URL:", addToLayout=False)
layout.addWidget(rb_button, 3, 0, Qt.AlignVCenter)

self.url_combo = url_combo = QComboBox()
self.url_combo = url_combo = TextEditCombo()
url_model = NamedURLModel(self.sheet_names)
url_model.wrap(self.recent_urls)
url_combo.setLineEdit(LineEditSelectOnFocus())
url_combo.setModel(url_model)
url_combo.setSizePolicy(Policy.Ignored, Policy.Fixed)
url_combo.setEditable(True)
url_combo.setInsertPolicy(url_combo.InsertAtTop)
url_edit = url_combo.lineEdit()
margins = url_edit.textMargins()
Expand Down Expand Up @@ -343,14 +344,19 @@ def select_reader(self, n):
self.load_data()

def _url_set(self):
index = self.url_combo.currentIndex()
url = self.url_combo.currentText()
pos = self.recent_urls.index(url)
url = url.strip()

if not urlparse(url).scheme:
url = 'http://' + url
self.url_combo.setItemText(pos, url)
self.recent_urls[pos] = url
self.url_combo.setItemText(index, url)

if index != 0:
model = self.url_combo.model()
root = self.url_combo.rootModelIndex()
model.moveRow(root, index, root, 0)
assert self.url_combo.currentIndex() == 0

self.source = self.URL
self.load_data()
Expand Down
19 changes: 19 additions & 0 deletions Orange/widgets/data/tests/test_owfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from AnyQt.QtCore import QMimeData, QPoint, Qt, QUrl, QPointF
from AnyQt.QtGui import QDragEnterEvent, QDropEvent
from AnyQt.QtTest import QTest
from AnyQt.QtWidgets import QComboBox

import Orange
Expand Down Expand Up @@ -713,6 +714,24 @@ def read():
self.assertTrue(self.widget.Warning.load_warning.is_shown())
self.assertIn(WARNING_MSG, str(self.widget.Warning.load_warning))

def test_recent_url_serialization(self):
with patch.object(self.widget, "load_data", lambda: None):
self.widget.url_combo.insertItem(0, "https://example.com/test.tab")
self.widget.url_combo.insertItem(1, "https://example.com/test1.tab")
self.widget.source = OWFile.URL
s = self.widget.settingsHandler.pack_data(self.widget)
self.assertEqual(s["recent_urls"],
["https://example.com/test.tab",
"https://example.com/test1.tab"])
self.widget.url_combo.lineEdit().clear()
QTest.keyClicks(self.widget.url_combo, "https://example.com/test1.tab")
QTest.keyClick(self.widget.url_combo, Qt.Key_Enter)
# must move the entered url to first position
s = self.widget.settingsHandler.pack_data(self.widget)
self.assertEqual(s["recent_urls"],
["https://example.com/test1.tab",
"https://example.com/test.tab"])


class TestOWFileDropHandler(unittest.TestCase):
def test_canDropUrl(self):
Expand Down
122 changes: 117 additions & 5 deletions Orange/widgets/utils/combobox.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
from AnyQt.QtCore import Qt
from AnyQt.QtGui import QBrush, QColor, QPalette, QPen, QFont, QFontMetrics
from AnyQt.QtWidgets import QStylePainter, QStyleOptionComboBox, QStyle
from AnyQt.QtCore import Qt, Signal
from AnyQt.QtGui import (
QBrush, QColor, QPalette, QPen, QFont, QFontMetrics, QFocusEvent
)
from AnyQt.QtWidgets import (
QStylePainter, QStyleOptionComboBox, QStyle, QApplication, QLineEdit
)

from orangewidget.utils.combobox import ComboBoxSearch, ComboBox
from orangewidget.utils.combobox import (
ComboBoxSearch, ComboBox, qcombobox_emit_activated
)

__all__ = [
"ComboBoxSearch", "ComboBox", "ItemStyledComboBox"
"ComboBoxSearch", "ComboBox", "ItemStyledComboBox", "TextEditCombo"
]


Expand Down Expand Up @@ -71,3 +77,109 @@ def initStyleOption(self, option: 'QStyleOptionComboBox') -> None:
if self.currentIndex() == -1:
option.currentText = self.__placeholderText
option.palette.setCurrentColorGroup(QPalette.Disabled)


class TextEditCombo(ComboBox):
#: This signal is emitted whenever the contents of the combo box are
#: changed and the widget loses focus *OR* via item activation (activated
#: signal)
editingFinished = Signal()

def __init__(self, *args, **kwargs):
kwargs.setdefault("editable", True)
# `activated=...` kwarg needs to be connected after `__on_activated`
activated = kwargs.pop("activated", None)
self.__edited = False
super().__init__(*args, **kwargs)
self.activated.connect(self.__on_activated)
if activated is not None:
self.activated.connect(activated)
ledit = self.lineEdit()
if ledit is not None:
ledit.textEdited.connect(self.__markEdited)

def setLineEdit(self, edit: QLineEdit) -> None:
super().setLineEdit(edit)
edit.textEdited.connect(self.__markEdited)

def __markEdited(self):
self.__edited = True

def __on_activated(self):
self.__edited = False # mark clean on any activation
self.editingFinished.emit()

def focusOutEvent(self, event: QFocusEvent) -> None:
super().focusOutEvent(event)
popup = QApplication.activePopupWidget()
if self.isEditable() and self.__edited and \
(event.reason() != Qt.PopupFocusReason or
not (popup is not None
and popup.parent() in (self, self.lineEdit()))):
def monitor():
# monitor if editingFinished was emitted from
# __on_editingFinished to avoid double emit.
nonlocal emitted
emitted = True
emitted = False
self.editingFinished.connect(monitor)
self.__edited = False
self.__on_editingFinished()
self.editingFinished.disconnect(monitor)

if not emitted:
self.editingFinished.emit()

def __on_editingFinished(self):
le = self.lineEdit()
policy = self.insertPolicy()
text = le.text()
if not text:
return
index = self.findText(text, Qt.MatchFixedString)
if index != -1:
self.setCurrentIndex(index)
qcombobox_emit_activated(self, index)
return
if policy == ComboBox.NoInsert:
return
elif policy == ComboBox.InsertAtTop:
index = 0
elif policy == ComboBox.InsertAtBottom:
index = self.count()
elif policy == ComboBox.InsertAfterCurrent:
index = self.currentIndex() + 1
elif policy == ComboBox.InsertBeforeCurrent:
index = max(self.currentIndex(), 0)
elif policy == ComboBox.InsertAlphabetically:
for index in range(self.count()):
if self.itemText(index).lower() >= text.lower():
break
elif policy == ComboBox.InsertAtCurrent:
self.setItemText(self.currentIndex(), text)
qcombobox_emit_activated(self, self.currentIndex())
return

if index > -1:
self.insertItem(index, text)
self.setCurrentIndex(index)
qcombobox_emit_activated(self, self.currentIndex())

def text(self):
# type: () -> str
"""
Return the current text.
"""
return self.itemText(self.currentIndex())

def setText(self, text):
# type: (str) -> None
"""
Set `text` as the current text (adding it to the model if necessary).
"""
idx = self.findData(text, Qt.EditRole, Qt.MatchExactly)
if idx != -1:
self.setCurrentIndex(idx)
else:
self.addItem(text)
self.setCurrentIndex(self.count() - 1)
71 changes: 68 additions & 3 deletions Orange/widgets/utils/tests/test_combobox.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from AnyQt.QtCore import Qt
from AnyQt.QtGui import QFont, QColor
from AnyQt.QtCore import Qt, QEvent
from AnyQt.QtGui import QFont, QColor, QFocusEvent
from AnyQt.QtWidgets import QApplication
from AnyQt.QtTest import QTest, QSignalSpy

from orangewidget.tests.base import GuiTest
from Orange.widgets.utils.combobox import ItemStyledComboBox
from orangewidget.tests.utils import simulate
from orangewidget.utils.itemmodels import PyListModel
from Orange.widgets.utils.combobox import ItemStyledComboBox, TextEditCombo


class TestItemStyledComboBox(GuiTest):
Expand All @@ -19,3 +23,64 @@ def test_combobox(self):
Qt.FontRole: QFont("Windings")
})
cb.grab()


class TestTextEditCombo(GuiTest):
def test_texteditcombo(self):
cb = TextEditCombo()
model = PyListModel()
cb.setModel(model)

def enter_text(text: str):
cb.lineEdit().selectAll()
spy_act = QSignalSpy(cb.activated[int])
spy_edit = QSignalSpy(cb.editingFinished)
QTest.keyClick(cb.lineEdit(), Qt.Key_Delete)
QTest.keyClicks(cb.lineEdit(), text)
QApplication.sendEvent(
cb, QFocusEvent(QEvent.FocusOut, Qt.TabFocusReason)
)
self.assertEqual(len(spy_edit), 1)
if cb.insertPolicy() != TextEditCombo.NoInsert:
self.assertEqual(list(spy_act), [[cb.currentIndex()]])

cb.setInsertPolicy(TextEditCombo.NoInsert)
enter_text("!!")
self.assertEqual(list(model), [])
cb.setInsertPolicy(TextEditCombo.InsertAtTop)
enter_text("BB")
enter_text("AA")
self.assertEqual(list(model), ["AA", "BB"])
cb.setInsertPolicy(TextEditCombo.InsertAtBottom)
enter_text("CC")
self.assertEqual(list(model), ["AA", "BB", "CC"])
cb.setInsertPolicy(TextEditCombo.InsertBeforeCurrent)
cb.setCurrentIndex(1)
enter_text("AB")
self.assertEqual(list(model), ["AA", "AB", "BB", "CC"])
cb.setInsertPolicy(TextEditCombo.InsertAfterCurrent)
cb.setCurrentIndex(2)
enter_text("BC")
self.assertEqual(list(model), ["AA", "AB", "BB", "BC", "CC"])
cb.setInsertPolicy(TextEditCombo.InsertAtCurrent)
cb.setCurrentIndex(2)
enter_text("BBA")
self.assertEqual(list(model), ["AA", "AB", "BBA", "BC", "CC"])
cb.setInsertPolicy(TextEditCombo.InsertAlphabetically)
enter_text("BCA")
self.assertEqual(list(model), ["AA", "AB", "BBA", "BC", "BCA", "CC"])

def test_activate_editing_finished_emit_ordering(self):
def activated():
sigs.append("activated")

def finished():
sigs.append("finished")

sigs = []
cb = TextEditCombo(
activated=activated, editingFinished=finished
)
cb.insertItem(0, "AA")
simulate.combobox_activate_index(cb, 0)
self.assertEqual(sigs, ["finished", "activated"])
22 changes: 1 addition & 21 deletions Orange/widgets/utils/textimport.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@

from Orange.widgets.utils import encodings
from Orange.widgets.utils.overlay import OverlayWidget
from Orange.widgets.utils.combobox import TextEditCombo


__all__ = ["ColumnType", "RowSpec", "CSVOptionsWidget", "CSVImportWidget"]
Expand Down Expand Up @@ -233,27 +234,6 @@ def minimumSizeHint(self):
return super(LineEdit, self).sizeHint()


class TextEditCombo(QComboBox):
def text(self):
# type: () -> str
"""
Return the current text.
"""
return self.itemText(self.currentIndex())

def setText(self, text):
# type: (str) -> None
"""
Set `text` as the current text (adding it to the model if necessary).
"""
idx = self.findData(text, Qt.EditRole, Qt.MatchExactly)
if idx != -1:
self.setCurrentIndex(idx)
else:
self.addItem(text)
self.setCurrentIndex(self.count() - 1)


class CSVOptionsWidget(QWidget):
"""
A widget presenting common CSV options.
Expand Down