mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-06 23:38:11 +08:00
remove options widget + launch on file selection (#839)
* remove options widget + launch on file selection * extend drop area width
This commit is contained in:
+16
-55
@@ -14,11 +14,8 @@ from PySide2.QtWidgets import *
|
|||||||
|
|
||||||
import server.gui.cellxgene_rc
|
import server.gui.cellxgene_rc
|
||||||
from server.gui.browser import CefWidget, CefApplication
|
from server.gui.browser import CefWidget, CefApplication
|
||||||
from server.gui.options_parser import parse_opt_string
|
|
||||||
from server.cli.launch import parse_engine_args
|
|
||||||
from server.gui.workers import Worker, SiteReadyWorker
|
from server.gui.workers import Worker, SiteReadyWorker
|
||||||
from server.gui.utils import WINDOWS, LINUX, MAC, FileLoadSignals, Emitter, WorkerSignals, FileChanged
|
from server.gui.utils import WINDOWS, LINUX, MAC, FileLoadSignals, Emitter, WorkerSignals, FileChanged
|
||||||
from server.utils.errors import OptionsError
|
|
||||||
from server.utils.utils import find_available_port
|
from server.utils.utils import find_available_port
|
||||||
|
|
||||||
if WINDOWS or LINUX:
|
if WINDOWS or LINUX:
|
||||||
@@ -30,6 +27,7 @@ if WINDOWS or LINUX:
|
|||||||
# TODO remember this or calculate it?
|
# TODO remember this or calculate it?
|
||||||
WIDTH = 1300
|
WIDTH = 1300
|
||||||
HEIGHT = 800
|
HEIGHT = 800
|
||||||
|
MAX_CONTENT_WIDTH = 700
|
||||||
GUI_PORT = find_available_port("localhost")
|
GUI_PORT = find_available_port("localhost")
|
||||||
BROWSER_INDEX = 0
|
BROWSER_INDEX = 0
|
||||||
LOAD_INDEX = 1
|
LOAD_INDEX = 1
|
||||||
@@ -149,9 +147,8 @@ class LoadWidget(QFrame):
|
|||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
super(LoadWidget, self).__init__(parent=parent)
|
super(LoadWidget, self).__init__(parent=parent)
|
||||||
# Init layout
|
# Init layout
|
||||||
self.MAX_CONTENT_WIDTH = 500
|
|
||||||
load_ui_layout = QVBoxLayout()
|
load_ui_layout = QVBoxLayout()
|
||||||
h_margin = (WIDTH - self.MAX_CONTENT_WIDTH) // 2
|
h_margin = (WIDTH - MAX_CONTENT_WIDTH) // 2
|
||||||
if h_margin < 10:
|
if h_margin < 10:
|
||||||
h_margin = 10
|
h_margin = 10
|
||||||
load_ui_layout.setContentsMargins(h_margin, 20, h_margin, 20)
|
load_ui_layout.setContentsMargins(h_margin, 20, h_margin, 20)
|
||||||
@@ -178,17 +175,14 @@ class LoadWidget(QFrame):
|
|||||||
self.file_area = FileArea()
|
self.file_area = FileArea()
|
||||||
self.file_name.signals.changed.connect(self.updatePath)
|
self.file_name.signals.changed.connect(self.updatePath)
|
||||||
|
|
||||||
self.launch_widget = QPushButton("Launch cellxgene")
|
self.launch_widget = QLabel("Select a file to launch cellxgene")
|
||||||
self.launch_widget.setEnabled(False)
|
# self.launch_widget.setEnabled(False)
|
||||||
self.launch_widget.clicked.connect(self.onLoad)
|
# self.launch_widget.clicked.connect(self.onLoad)
|
||||||
|
|
||||||
self.progress = QProgressBar()
|
self.progress = QProgressBar()
|
||||||
self.progress.setTextVisible(False)
|
self.progress.setTextVisible(False)
|
||||||
|
|
||||||
self.advanced_options = CLIOptionsArea(self)
|
|
||||||
|
|
||||||
file_layout.addWidget(self.file_area)
|
file_layout.addWidget(self.file_area)
|
||||||
file_layout.addWidget(self.advanced_options)
|
|
||||||
self.loading_layout = QStackedLayout()
|
self.loading_layout = QStackedLayout()
|
||||||
self.loading_layout.addWidget(self.launch_widget)
|
self.loading_layout.addWidget(self.launch_widget)
|
||||||
self.loading_layout.addWidget(self.progress)
|
self.loading_layout.addWidget(self.progress)
|
||||||
@@ -198,7 +192,7 @@ class LoadWidget(QFrame):
|
|||||||
# Error section
|
# Error section
|
||||||
self.error_label = QLabel("")
|
self.error_label = QLabel("")
|
||||||
self.error_label.setWordWrap(True)
|
self.error_label.setWordWrap(True)
|
||||||
self.error_label.setFixedWidth(self.MAX_CONTENT_WIDTH)
|
self.error_label.setFixedWidth(MAX_CONTENT_WIDTH)
|
||||||
message_layout.addWidget(self.error_label)
|
message_layout.addWidget(self.error_label)
|
||||||
|
|
||||||
# Options Form
|
# Options Form
|
||||||
@@ -225,14 +219,13 @@ class LoadWidget(QFrame):
|
|||||||
self.file_area.label.setText("File: " + file_name)
|
self.file_area.label.setText("File: " + file_name)
|
||||||
else:
|
else:
|
||||||
self.file_area.label.setText("")
|
self.file_area.label.setText("")
|
||||||
self.launch_widget.setEnabled(bool(file_name))
|
# self.launch_widget.setEnabled(bool(file_name))
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.loading_layout.setCurrentIndex(0)
|
self.loading_layout.setCurrentIndex(0)
|
||||||
self.timer.stop()
|
self.timer.stop()
|
||||||
self.error_label.setText("")
|
self.error_label.setText("")
|
||||||
self.file_name.updateValue(None)
|
self.file_name.updateValue(None)
|
||||||
self.advanced_options.reset()
|
|
||||||
|
|
||||||
def updateProgress(self):
|
def updateProgress(self):
|
||||||
curr_val = self.progress.value()
|
curr_val = self.progress.value()
|
||||||
@@ -240,23 +233,15 @@ class LoadWidget(QFrame):
|
|||||||
self.progress.setValue(next_val)
|
self.progress.setValue(next_val)
|
||||||
|
|
||||||
def resetProgress(self):
|
def resetProgress(self):
|
||||||
print("here i am")
|
|
||||||
self.progress.setValue(0)
|
self.progress.setValue(0)
|
||||||
self.loading_layout.setCurrentIndex(0)
|
self.loading_layout.setCurrentIndex(0)
|
||||||
self.timer.stop()
|
self.timer.stop()
|
||||||
|
|
||||||
def createScanpyEngine(self, file_name):
|
def createScanpyEngine(self, file_name):
|
||||||
try:
|
title = splitext(basename(file_name))[0]
|
||||||
self.advanced_options.parse()
|
|
||||||
except OptionsError as e:
|
|
||||||
self.signals.error.emit(f"Options Error: {e}")
|
|
||||||
return
|
|
||||||
title = self.advanced_options.title
|
|
||||||
if not title:
|
|
||||||
title = splitext(basename(file_name))[0]
|
|
||||||
self.window().setupServer()
|
self.window().setupServer()
|
||||||
worker = Worker(self.window().parent_conn, self.window().child_conn, file_name, host="127.0.0.1",
|
worker = Worker(self.window().parent_conn, self.window().child_conn, file_name, host="127.0.0.1",
|
||||||
port=GUI_PORT, title=title, engine_options=self.advanced_options.engine_options)
|
port=GUI_PORT, title=title, engine_options={})
|
||||||
self.window().load_emitter.signals.ready.connect(self.onDataReady)
|
self.window().load_emitter.signals.ready.connect(self.onDataReady)
|
||||||
self.window().load_emitter.signals.engine_error.connect(self.onServerError)
|
self.window().load_emitter.signals.engine_error.connect(self.onServerError)
|
||||||
self.window().load_emitter.signals.server_error.connect(self.onServerError)
|
self.window().load_emitter.signals.server_error.connect(self.onServerError)
|
||||||
@@ -299,37 +284,11 @@ class LoadWidget(QFrame):
|
|||||||
self.resetProgress()
|
self.resetProgress()
|
||||||
self.window().stacked_layout.setCurrentIndex(LOAD_INDEX)
|
self.window().stacked_layout.setCurrentIndex(LOAD_INDEX)
|
||||||
self.error_label.setText(f"Error: {err}")
|
self.error_label.setText(f"Error: {err}")
|
||||||
self.error_label.resize(self.MAX_CONTENT_WIDTH, self.error_label.height())
|
self.error_label.resize(MAX_CONTENT_WIDTH, self.error_label.height())
|
||||||
self.window().repaint()
|
self.window().repaint()
|
||||||
|
|
||||||
onServerError = partialmethod(onError, server_error=True)
|
onServerError = partialmethod(onError, server_error=True)
|
||||||
|
|
||||||
class CLIOptionsArea(QFrame):
|
|
||||||
def __init__(self, parent):
|
|
||||||
super(CLIOptionsArea, self).__init__(parent)
|
|
||||||
self.engine_options = {}
|
|
||||||
self.setFixedWidth(500)
|
|
||||||
self.title = None
|
|
||||||
self.form_layout = QFormLayout()
|
|
||||||
self.form_layout.setContentsMargins(0,0,0,0)
|
|
||||||
self.cli_label = QLabel("CLI Options:")
|
|
||||||
self.cli_label.setToolTip("Additional options can be passed as cli parameters. See cellxgene --help for more info")
|
|
||||||
self.cli_widget = QLineEdit()
|
|
||||||
self.form_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
||||||
self.form_layout.addRow(self.cli_label, self.cli_widget)
|
|
||||||
self.setLayout(self.form_layout)
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
self.cli_widget.setText("")
|
|
||||||
self.engine_options = {}
|
|
||||||
self.title = None
|
|
||||||
|
|
||||||
def parse(self):
|
|
||||||
opts = parse_opt_string(self.cli_widget.text())
|
|
||||||
self.title = opts.pop("title", None)
|
|
||||||
self.engine_options = parse_engine_args(**opts)
|
|
||||||
|
|
||||||
|
|
||||||
class FilePath(QObject):
|
class FilePath(QObject):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(FilePath, self).__init__()
|
super(FilePath, self).__init__()
|
||||||
@@ -346,17 +305,17 @@ class FileArea(QFrame):
|
|||||||
super(FileArea, self).__init__()
|
super(FileArea, self).__init__()
|
||||||
self.setFrameShape(QFrame.Box)
|
self.setFrameShape(QFrame.Box)
|
||||||
self.setMinimumHeight(100)
|
self.setMinimumHeight(100)
|
||||||
self.setFixedWidth(500)
|
self.setFixedWidth(MAX_CONTENT_WIDTH)
|
||||||
self.setAcceptDrops(True)
|
self.setAcceptDrops(True)
|
||||||
self.instructions = QLabel(self)
|
self.instructions = QLabel(self)
|
||||||
self.instructions.setText("Drag & Drop a h5ad file to load or open")
|
self.instructions.setText("Drag & Drop a h5ad file to load or open")
|
||||||
self.instructions.setGeometry(10, 10, 500, self.instructions.height())
|
self.instructions.setGeometry(10, 10, MAX_CONTENT_WIDTH, self.instructions.height())
|
||||||
self.loadButton = QPushButton("Open...", parent=self)
|
self.loadButton = QPushButton("Open...", parent=self)
|
||||||
x_pos = (500 - self.loadButton.width()) / 2
|
x_pos = (MAX_CONTENT_WIDTH - self.loadButton.width()) / 2
|
||||||
self.loadButton.setGeometry(x_pos, 50, self.loadButton.width(), self.loadButton.height())
|
self.loadButton.setGeometry(x_pos, 50, self.loadButton.width(), self.loadButton.height())
|
||||||
self.loadButton.clicked.connect(self.fileBrowse)
|
self.loadButton.clicked.connect(self.fileBrowse)
|
||||||
self.label = QLabel(self)
|
self.label = QLabel(self)
|
||||||
self.label.setGeometry(10, 75, 500, self.label.height())
|
self.label.setGeometry(10, 75, MAX_CONTENT_WIDTH, self.label.height())
|
||||||
|
|
||||||
def fileBrowse(self):
|
def fileBrowse(self):
|
||||||
options = QFileDialog.Options()
|
options = QFileDialog.Options()
|
||||||
@@ -365,6 +324,7 @@ class FileArea(QFrame):
|
|||||||
"Open H5AD File", "", "H5AD Files (*.h5ad)", options=options)
|
"Open H5AD File", "", "H5AD Files (*.h5ad)", options=options)
|
||||||
if file_name:
|
if file_name:
|
||||||
self.parent().file_name.updateValue(file_name)
|
self.parent().file_name.updateValue(file_name)
|
||||||
|
self.parent().onLoad()
|
||||||
|
|
||||||
def dragEnterEvent(self, e):
|
def dragEnterEvent(self, e):
|
||||||
if e.mimeData().hasUrls:
|
if e.mimeData().hasUrls:
|
||||||
@@ -391,6 +351,7 @@ class FileArea(QFrame):
|
|||||||
for url in e.mimeData().urls():
|
for url in e.mimeData().urls():
|
||||||
file_name = str(url.toLocalFile())
|
file_name = str(url.toLocalFile())
|
||||||
self.parent().file_name.updateValue(file_name)
|
self.parent().file_name.updateValue(file_name)
|
||||||
|
self.parent().onLoad()
|
||||||
else:
|
else:
|
||||||
e.ignore()
|
e.ignore()
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import shlex
|
|
||||||
|
|
||||||
import click
|
|
||||||
|
|
||||||
from server.cli.launch import common_args
|
|
||||||
from server.utils.errors import OptionsError
|
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@common_args
|
|
||||||
def cli(**kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def parse_opt_string(opts):
|
|
||||||
context = click.Context(cli)
|
|
||||||
parser = click.OptionParser(context)
|
|
||||||
for command in context.command.params:
|
|
||||||
command.add_to_parser(parser, context)
|
|
||||||
|
|
||||||
try:
|
|
||||||
opts, args, param_order = parser.parse_args(shlex.split(opts))
|
|
||||||
for param in cli.params:
|
|
||||||
value, args = param.handle_parse_result(context, opts, args)
|
|
||||||
except click.ClickException as ce:
|
|
||||||
raise OptionsError(ce.message) from ce
|
|
||||||
return dict(context.params)
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import unittest
|
|
||||||
|
|
||||||
from server.gui.options_parser import parse_opt_string
|
|
||||||
from server.utils.errors import OptionsError
|
|
||||||
|
|
||||||
default_opts = {'title': None, 'layout': (), 'obs_names': None, 'var_names': None, 'max_category_items': 1000,
|
|
||||||
'diffexp_lfc_cutoff': 0.01}
|
|
||||||
|
|
||||||
|
|
||||||
class UtilsTest(unittest.TestCase):
|
|
||||||
|
|
||||||
def test_empty_opts(self):
|
|
||||||
result = parse_opt_string("")
|
|
||||||
self.assertEqual(result, default_opts)
|
|
||||||
|
|
||||||
def test_one_opts(self):
|
|
||||||
result = parse_opt_string("--title abcde")
|
|
||||||
expected_opts = dict(default_opts)
|
|
||||||
expected_opts["title"] = "abcde"
|
|
||||||
self.assertEqual(result, expected_opts)
|
|
||||||
|
|
||||||
def test_malformed_opts(self):
|
|
||||||
with self.assertRaises(OptionsError):
|
|
||||||
parse_opt_string("--ewwwww lkafsjkl")
|
|
||||||
|
|
||||||
def test_complex_opts(self):
|
|
||||||
result = parse_opt_string(
|
|
||||||
"--title abcde --obs-names zzzzz --var-names 'xxx yyy' --max-category-items 234907 "
|
|
||||||
"--diffexp-lfc-cutoff 0.00003847382")
|
|
||||||
expected_opts = {'title': 'abcde', 'layout': (), 'obs_names': 'zzzzz', 'var_names': 'xxx yyy',
|
|
||||||
'max_category_items': 234907, 'diffexp_lfc_cutoff': 3.847382e-05}
|
|
||||||
self.assertEqual(result, expected_opts)
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
class OptionsError(Exception):
|
|
||||||
"""
|
|
||||||
Raised when user specified cli options specified fail parsing
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, message):
|
|
||||||
self.message = message
|
|
||||||
Reference in New Issue
Block a user