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

[ENH] Open draged files on CSV Import, Load Model and Distance File widgets #6747

Merged
merged 4 commits into from
Mar 1, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
76 changes: 63 additions & 13 deletions Orange/widgets/data/owcsvimport.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
----------------------

"""
from __future__ import annotations
import sys
import types
import os
Expand Down Expand Up @@ -32,7 +33,8 @@
)

from AnyQt.QtCore import (
Qt, QFileInfo, QTimer, QSettings, QObject, QSize, QMimeDatabase, QMimeType
Qt, QFileInfo, QTimer, QSettings, QObject, QSize, QMimeDatabase, QMimeType,
QUrl
)
from AnyQt.QtGui import (
QStandardItem, QStandardItemModel, QPalette, QColor, QIcon
Expand All @@ -50,12 +52,15 @@
from pandas import CategoricalDtype
from pandas.api import types as pdtypes

from orangecanvas.utils import assocf
from orangecanvas.utils.qobjref import qobjref_weak
from orangewidget.utils import enum_as_int

import Orange.data
from Orange.misc.collections import natural_sorted

from Orange.widgets import widget, gui, settings
from Orange.widgets.utils.filedialogs import OWUrlDropBase
from Orange.widgets.utils.localization import pl
from Orange.widgets.utils.concurrent import PyOwned
from Orange.widgets.utils import (
Expand Down Expand Up @@ -610,7 +615,7 @@
return Options(dialect=dialect, encoding=encoding, rowspec=rowspec)


class OWCSVFileImport(widget.OWWidget):
class OWCSVFileImport(OWUrlDropBase):
name = "CSV File Import"
description = "Import a data table from a CSV formatted file."
icon = "icons/CSVFile.svg"
Expand Down Expand Up @@ -967,6 +972,24 @@
"""Activate the Import Options dialog for the current item."""
item = self.current_item()
assert item is not None
path = item.path()
options = item.options()

def onfinished(dlg: CSVImportDialog):
if dlg.result() != QDialog.Accepted:
return

Check warning on line 980 in Orange/widgets/data/owcsvimport.py

View check run for this annotation

Codecov / codecov/patch

Orange/widgets/data/owcsvimport.py#L980

Added line #L980 was not covered by tests
newoptions = dlg.options()
item.setData(newoptions, ImportItem.OptionsRole)
# update local recent paths list
self._note_recent(path, newoptions)
if newoptions != options:
self._invalidate()

self._activate_import_dialog_for_item(item, finished=onfinished)

def _activate_import_dialog_for_item(
self, item: ImportItem, finished: Callable[[CSVImportDialog], None]
):
dlg = CSVImportDialog(
self, windowTitle="Import Options", sizeGripEnabled=True,
)
Expand All @@ -984,18 +1007,14 @@
if isinstance(options, Options):
dlg.setOptions(options)

def update():
newoptions = dlg.options()
item.setData(newoptions, ImportItem.OptionsRole)
# update local recent paths list
self._note_recent(path, newoptions)
if newoptions != options:
self._invalidate()
dlg.accepted.connect(update)
dialog_ref = qobjref_weak(dlg)

def store_size():
def onfinished():
dlg = dialog_ref()
settings.setValue("size", dlg.size())
dlg.finished.connect(store_size)
finished(dlg)

dlg.finished.connect(onfinished)
dlg.show()

def set_selected_file(self, filename, options=None):
Expand Down Expand Up @@ -1035,7 +1054,6 @@
else:
item = ImportItem.fromPath(filename)

# item.setData(VarPath(filename), ImportItem.VarPathRole)
item.setData(True, ImportItem.IsSessionItemRole)
model.insertRow(0, item)

Expand Down Expand Up @@ -1127,6 +1145,7 @@
"""
Cancel current pending or executing task.
"""
self.__committimer.stop()
if self.__watcher is not None:
self.__cancel_task()
self.__clear_running_state()
Expand Down Expand Up @@ -1311,6 +1330,37 @@
idx = -1
self.recent_combo.setCurrentIndex(idx)

def canDropUrl(self, url: QUrl) -> bool:
if url.isLocalFile():
return _mime_type_for_path(url.toLocalFile()).inherits("text/plain")
else:
return False

def handleDroppedUrl(self, url: QUrl) -> None:
# search recent items for path
path = url.toLocalFile()
hist = self.itemsFromSettings()
res = assocf(hist, lambda p: samepath(p, path))
if res is not None:
_, options = res

Check warning on line 1345 in Orange/widgets/data/owcsvimport.py

View check run for this annotation

Codecov / codecov/patch

Orange/widgets/data/owcsvimport.py#L1345

Added line #L1345 was not covered by tests
else:
mt = _mime_type_for_path(path)
options = default_options_for_mime_type(path, mt.name())
self.activate_import_for_file(path, options)

def activate_import_for_file(self, path: str, options: Options | None = None):
self.cancel() # Cancel current task if any
item = ImportItem() # dummy temp item
item.setPath(path)
item.setOptions(options)

def finished(dlg: CSVImportDialog):
if dlg.result() != QDialog.Accepted:
return
self.set_selected_file(path, dlg.options())

self._activate_import_dialog_for_item(item, finished)

@classmethod
def migrate_settings(cls, settings, version):
if not version or version < 2:
Expand Down
32 changes: 13 additions & 19 deletions Orange/widgets/data/owfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from Orange.widgets.utils.itemmodels import PyListModel
from Orange.widgets.utils.filedialogs import RecentPathsWComboMixin, \
open_filename_dialog, stored_recent_paths_prepend
from Orange.widgets.utils.filedialogs import OWUrlDropBase
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.widget import Output, Msg
from Orange.widgets.utils.combobox import TextEditCombo
Expand Down Expand Up @@ -82,7 +83,7 @@ def focusInEvent(self, event):
QTimer.singleShot(0, self.selectAll)


class OWFile(widget.OWWidget, RecentPathsWComboMixin):
class OWFile(OWUrlDropBase, RecentPathsWComboMixin):
name = "File"
id = "orange.widgets.data.file"
description = "Read data from an input file or network " \
Expand Down Expand Up @@ -311,8 +312,7 @@ def package(w):

QTimer.singleShot(0, self.load_data)

@staticmethod
def sizeHint():
def sizeHint(self):
return QSize(600, 550)

def select_file(self, n):
Expand Down Expand Up @@ -632,24 +632,18 @@ def get_ext_name(filename):

self.report_data("Data", self.data)

@staticmethod
def dragEnterEvent(event):
"""Accept drops of valid file urls"""
urls = event.mimeData().urls()
if urls:
try:
FileFormat.get_reader(urls[0].toLocalFile())
event.acceptProposedAction()
except MissingReaderException:
pass

def dropEvent(self, event):
"""Handle file drops"""
urls = event.mimeData().urls()
if urls:
self.add_path(urls[0].toLocalFile()) # add first file
def canDropUrl(self, url: QUrl) -> bool:
return OWFileDropHandler().canDropUrl(url)

def handleDroppedUrl(self, url: QUrl) -> None:
if url.isLocalFile():
self.add_path(url.toLocalFile()) # add first file
self.source = self.LOCAL_FILE
self.load_data()
else:
self.url_combo.insertItem(0, url.toString())
self.url_combo.setCurrentIndex(0)
self._url_set()

def workflowEnvChanged(self, key, value, oldvalue):
"""
Expand Down
41 changes: 38 additions & 3 deletions Orange/widgets/data/tests/test_owcsvimport.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# pylint: disable=no-self-use,protected-access,invalid-name,arguments-differ
# pylint: disable=protected-access,invalid-name,arguments-differ
import tempfile
import unittest
from unittest import mock
Expand All @@ -14,7 +14,7 @@
import pandas as pd
from numpy.testing import assert_array_equal

from AnyQt.QtCore import QSettings, Qt
from AnyQt.QtCore import QSettings, Qt, QUrl
from AnyQt.QtGui import QIcon
from AnyQt.QtWidgets import QFileDialog
from AnyQt.QtTest import QSignalSpy
Expand All @@ -28,7 +28,7 @@
from Orange.widgets.tests.base import WidgetTest, GuiTest
from Orange.widgets.data import owcsvimport
from Orange.widgets.data.owcsvimport import (
OWCSVFileImport, pandas_to_table, ColumnType, RowSpec,
OWCSVFileImport, pandas_to_table, ColumnType, RowSpec, ImportItem,
)
from Orange.widgets.utils.pathutils import PathItem, samepath
from Orange.widgets.utils.settings import QSettings_writeArray
Expand Down Expand Up @@ -374,6 +374,41 @@ def test_browse_for_missing_prefixed_parent(self):
self.assertEqual(item[0], cur.varPath())
self.assertEqual(item[1].as_dict(), cur.options().as_dict())

def test_activate_import_dialog(self):
path = self.data_regions_path
item = ImportItem.fromPath(path)
self.widget.import_items_model.appendRow(ImportItem.fromPath(path))
opts = item.options()
self.assertIsNone(opts)
with mock.patch.object(owcsvimport.CSVImportDialog, "show"):
self.widget.import_options_button.click()
dlg = self.widget.findChild(owcsvimport.CSVImportDialog)
dlg.accept()
item_ = self.widget.current_item()
self.assertEqual(item.path(), item_.path())
self.assertIsNotNone(item_.options())

def test_drop_file(self):
self.assertFalse(self.widget.canDropUrl(QUrl("https://aa-bb.com")))
url = QUrl.fromLocalFile(self.data_regions_path)
self.assertTrue(self.widget.canDropUrl(url))

with mock.patch.object(owcsvimport.CSVImportDialog, "show"):
self.widget.handleDroppedUrl(url)
dlg = self.widget.findChild(owcsvimport.CSVImportDialog)
dlg.reject()
item = self.widget.current_item()
self.assertIsNone(item, "Rejecting the dialog should not record the recent file")

with mock.patch.object(owcsvimport.CSVImportDialog, "show"):
self.widget.handleDroppedUrl(url)
dlg = self.widget.findChild(owcsvimport.CSVImportDialog)
dlg.accept()
item = self.widget.current_item()
self.assertEqual(item.path(), url.toLocalFile())
out = self.get_output(self.widget.Outputs.data)
self.assertEqual(len(out.domain), 3)

def test_long_data(self):
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "test.csv")
Expand Down
13 changes: 8 additions & 5 deletions Orange/widgets/data/tests/test_owfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,13 @@ class TestOWFile(WidgetTest):
event_data = None

def setUp(self):
super().setUp()
self.widget = self.create_widget(OWFile) # type: OWFile
dataset_dirs.append(dirname(__file__))

def tearDown(self):
dataset_dirs.pop()
super().tearDown()

def test_describe_call_get_nans(self):
table = Table("iris")
Expand All @@ -93,11 +95,6 @@ def test_dragEnterEvent_accepts_urls(self):
self.widget.dragEnterEvent(event)
self.assertTrue(event.isAccepted())

def test_dragEnterEvent_skips_osx_file_references(self):
event = self._drag_enter_event(QUrl.fromLocalFile('/.file/id=12345'))
self.widget.dragEnterEvent(event)
self.assertFalse(event.isAccepted())

def test_dragEnterEvent_skips_usupported_files(self):
event = self._drag_enter_event(QUrl.fromLocalFile('file.unsupported'))
self.widget.dragEnterEvent(event)
Expand All @@ -122,6 +119,12 @@ def test_dropEvent_selects_file(self):
self.assertTrue(path.samefile(self.widget.last_path(), TITANIC_PATH))
self.widget.load_data.assert_called_with()

event = self._drop_event(QUrl("https://example.com/aa.csv"))
self.widget.load_data.reset_mock()
self.widget.dropEvent(event)
self.assertEqual(self.widget.source, OWFile.URL)
self.widget.load_data.assert_called_with()

def _drop_event(self, url):
# make sure data does not get garbage collected before it used
self.event_data = data = QMimeData()
Expand Down
17 changes: 14 additions & 3 deletions Orange/widgets/model/owloadmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@
from typing import Any, Dict

from AnyQt.QtWidgets import QSizePolicy, QStyle, QFileDialog
from AnyQt.QtCore import QTimer
from AnyQt.QtCore import QTimer, QUrl

from orangewidget.workflow.drophandler import SingleFileDropHandler

from Orange.base import Model
from Orange.widgets import widget, gui
from Orange.widgets.model import owsavemodel
from Orange.widgets.utils.filedialogs import RecentPathsWComboMixin, RecentPath, \
stored_recent_paths_prepend
stored_recent_paths_prepend, OWUrlDropBase
from Orange.widgets.utils import stdpaths
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.widget import Msg, Output


class OWLoadModel(widget.OWWidget, RecentPathsWComboMixin):
class OWLoadModel(OWUrlDropBase, RecentPathsWComboMixin):
name = "Load Model"
description = "Load a model from an input file."
priority = 3050
Expand Down Expand Up @@ -91,6 +91,17 @@ def open_file(self):
else:
self.Outputs.model.send(model)

def canDropUrl(self, url: QUrl) -> bool:
if url.isLocalFile():
return OWLoadModelDropHandler().canDropFile(url.toLocalFile())
else:
return False

def handleDroppedUrl(self, url: QUrl) -> None:
if url.isLocalFile():
self.add_path(url.toLocalFile())
self.open_file()


class OWLoadModelDropHandler(SingleFileDropHandler):
WIDGET = OWLoadModel
Expand Down