mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-26 01:48:12 +08:00
GUI UI Elements (#816)
This commit is contained in:
@@ -130,3 +130,10 @@ uninstall :
|
||||
pip uninstall -y cellxgene || :
|
||||
|
||||
.PHONY : install install-dev install-release-test install-release uninstall
|
||||
|
||||
# GUI
|
||||
|
||||
build-assets :
|
||||
pyside2-rcc server/gui/cellxgene.qrc -o server/gui/cellxgene_rc.py
|
||||
|
||||
.PHONY : build-assets
|
||||
|
||||
+67
-53
@@ -1,4 +1,5 @@
|
||||
import errno
|
||||
import functools
|
||||
import logging
|
||||
from os import devnull
|
||||
from os.path import splitext, basename, getsize
|
||||
@@ -13,22 +14,56 @@ from server.app.util.errors import ScanpyFileError
|
||||
from server.app.util.utils import custom_format_warning
|
||||
from server.utils.utils import find_available_port, is_port_available
|
||||
|
||||
|
||||
# anything bigger than this will generate a special message
|
||||
BIG_FILE_SIZE_THRESHOLD = 100 * 2**20 # 100MB
|
||||
BIG_FILE_SIZE_THRESHOLD = 100 * 2 ** 20 # 100MB
|
||||
|
||||
|
||||
def common_args(func):
|
||||
"""
|
||||
Decorator to contain CLI args that will be common to both CLI and GUI: title and engine args.
|
||||
"""
|
||||
@click.option("--title", "-t", help="Title to display (if omitted will use file name).")
|
||||
@click.option(
|
||||
"--layout",
|
||||
"-l",
|
||||
default=[],
|
||||
multiple=True,
|
||||
show_default=True,
|
||||
help="Layout name, eg, 'umap'."
|
||||
)
|
||||
@click.option("--obs-names", default=None, metavar="", help="Name of annotation field to use for observations.")
|
||||
@click.option("--var-names", default=None, metavar="", help="Name of annotation to use for variables.")
|
||||
@click.option(
|
||||
"--max-category-items",
|
||||
default=1000,
|
||||
metavar="",
|
||||
show_default=True,
|
||||
help="Categories with more distinct values than this will not be displayed.",
|
||||
)
|
||||
@click.option(
|
||||
"--diffexp-lfc-cutoff",
|
||||
default=0.01,
|
||||
show_default=True,
|
||||
help="Relative expression cutoff used when selecting top N differentially expressed genes",
|
||||
)
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def parse_engine_args(layout, obs_names, var_names, max_category_items, diffexp_lfc_cutoff):
|
||||
return {
|
||||
"layout": layout,
|
||||
"max_category_items": max_category_items,
|
||||
"diffexp_lfc_cutoff": diffexp_lfc_cutoff,
|
||||
"obs_names": obs_names,
|
||||
"var_names": var_names,
|
||||
}
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument("data", metavar="<data file>", type=click.Path(exists=True, file_okay=True, dir_okay=False))
|
||||
@click.option(
|
||||
"--layout",
|
||||
"-l",
|
||||
default=[],
|
||||
multiple=True,
|
||||
show_default=True,
|
||||
help="Layout name, eg, 'umap'."
|
||||
)
|
||||
@click.option("--title", "-t", help="Title to display (if omitted will use file name).", metavar="")
|
||||
@click.option(
|
||||
"--verbose",
|
||||
"-v",
|
||||
@@ -49,22 +84,7 @@ BIG_FILE_SIZE_THRESHOLD = 100 * 2**20 # 100MB
|
||||
)
|
||||
@click.option("--port", "-p", help="Port to run server on, if not specified cellxgene will find an available port.",
|
||||
metavar="", show_default=True)
|
||||
@click.option("--obs-names", default=None, metavar="", help="Name of annotation field to use for observations.")
|
||||
@click.option("--var-names", default=None, metavar="", help="Name of annotation to use for variables.")
|
||||
@click.option("--host", default="127.0.0.1", help="Host IP address")
|
||||
@click.option(
|
||||
"--max-category-items",
|
||||
default=1000,
|
||||
metavar="",
|
||||
show_default=True,
|
||||
help="Categories with more distinct values than this will not be displayed.",
|
||||
)
|
||||
@click.option(
|
||||
"--diffexp-lfc-cutoff",
|
||||
default=0.01,
|
||||
show_default=True,
|
||||
help="Relative expression cutoff used when selecting top N differentially expressed genes",
|
||||
)
|
||||
@click.option(
|
||||
"--scripts",
|
||||
default=[],
|
||||
@@ -72,20 +92,21 @@ BIG_FILE_SIZE_THRESHOLD = 100 * 2**20 # 100MB
|
||||
help="Additional script files to include in html page",
|
||||
show_default=True,
|
||||
)
|
||||
@common_args
|
||||
def launch(
|
||||
data,
|
||||
layout,
|
||||
title,
|
||||
verbose,
|
||||
debug,
|
||||
obs_names,
|
||||
var_names,
|
||||
open_browser,
|
||||
port,
|
||||
host,
|
||||
layout,
|
||||
obs_names,
|
||||
var_names,
|
||||
max_category_items,
|
||||
diffexp_lfc_cutoff,
|
||||
scripts,
|
||||
title,
|
||||
scripts
|
||||
):
|
||||
"""Launch the cellxgene data viewer.
|
||||
This web app lets you explore single-cell expression data.
|
||||
@@ -98,6 +119,7 @@ def launch(
|
||||
|
||||
> cellxgene launch <your data file> --title <your title>"""
|
||||
|
||||
e_args = parse_engine_args(layout, obs_names, var_names, max_category_items, diffexp_lfc_cutoff)
|
||||
# Startup message
|
||||
click.echo("[cellxgene] Starting the CLI...")
|
||||
|
||||
@@ -112,22 +134,22 @@ def launch(
|
||||
else:
|
||||
warnings.formatwarning = custom_format_warning
|
||||
|
||||
if scripts:
|
||||
click.echo(r"""
|
||||
/ / /\ \ \__ _ _ __ _ __ (_)_ __ __ _
|
||||
\ \/ \/ / _` | '__| '_ \| | '_ \ / _` |
|
||||
\ /\ / (_| | | | | | | | | | | (_| |
|
||||
\/ \/ \__,_|_| |_| |_|_|_| |_|\__, |
|
||||
|___/
|
||||
The --scripts flag is intended for developers to include google analytics etc. You could be opening yourself to a
|
||||
security risk by including the --scripts flag. Make sure you trust the scripts that you are including.
|
||||
""")
|
||||
scripts_pretty = ", ".join(scripts)
|
||||
click.confirm(f"Are you sure you want to inject these scripts: {scripts_pretty}?", abort=True)
|
||||
|
||||
if not verbose:
|
||||
sys.tracebacklimit = 0
|
||||
|
||||
if scripts:
|
||||
click.echo(r"""
|
||||
/ / /\ \ \__ _ _ __ _ __ (_)_ __ __ _
|
||||
\ \/ \/ / _` | '__| '_ \| | '_ \ / _` |
|
||||
\ /\ / (_| | | | | | | | | | | (_| |
|
||||
\/ \/ \__,_|_| |_| |_|_|_| |_|\__, |
|
||||
|___/
|
||||
The --scripts flag is intended for developers to include google analytics etc. You could be opening yourself to a
|
||||
security risk by including the --scripts flag. Make sure you trust the scripts that you are including.
|
||||
""")
|
||||
scripts_pretty = ", ".join(scripts)
|
||||
click.confirm(f"Are you sure you want to inject these scripts: {scripts_pretty}?", abort=True)
|
||||
|
||||
if not title:
|
||||
file_parts = splitext(basename(data))
|
||||
title = file_parts[0]
|
||||
@@ -170,16 +192,8 @@ security risk by including the --scripts flag. Make sure you trust the scripts t
|
||||
mpl.use("TkAgg")
|
||||
from server.app.scanpy_engine.scanpy_engine import ScanpyEngine
|
||||
|
||||
args = {
|
||||
"layout": layout,
|
||||
"max_category_items": max_category_items,
|
||||
"diffexp_lfc_cutoff": diffexp_lfc_cutoff,
|
||||
"obs_names": obs_names,
|
||||
"var_names": var_names,
|
||||
}
|
||||
|
||||
try:
|
||||
server.attach_data(ScanpyEngine(data, args), title=title)
|
||||
server.attach_data(ScanpyEngine(data, e_args), title=title)
|
||||
except ScanpyFileError as e:
|
||||
raise click.ClickException(f"{e}")
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE RCC><RCC version="1.0">
|
||||
<qresource>
|
||||
<file alias="logo.png">images/cellxgene_logo.png</file>
|
||||
<file alias="collapsed.svg">images/properties_contract.svg</file>
|
||||
<file alias="expanded.svg">images/properties_expand.svg</file>
|
||||
<file alias="icon.png">images/properties_expand.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -0,0 +1,440 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Resource object code
|
||||
#
|
||||
# Created: Wed Jun 19 15:02:04 2019
|
||||
# by: The Resource Compiler for PySide2 (Qt v5.12.3)
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PySide2 import QtCore
|
||||
|
||||
qt_resource_data = b"\
|
||||
\x00\x00\x0d\xde\
|
||||
\x89\
|
||||
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
|
||||
\x00\x01;\x00\x00\x00j\x08\x06\x00\x00\x00\xc3y\xf6!\
|
||||
\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\
|
||||
\x01\x00\x9a\x9c\x18\x00\x00\x00\x01sRGB\x00\xae\xce\
|
||||
\x1c\xe9\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\x0b\xfc\
|
||||
a\x05\x00\x00\x0dsIDATx\x01\xed\xdd\x7fn\
|
||||
\x1b\xc7\x15\x07\xf07C\xdbQ\x93\x00f\xfe\x88e7\
|
||||
\x05L\x9d J/PJ9@\xe4^@\x12\x90H\
|
||||
(\x0aT\xce\x09,\x9d\xc0\xf2\x1fE \xb9\x80\xd9\x0b\
|
||||
\xd4\xf2\x01,2'\xb0|\x023@\x0bG\xea\x1fQ\
|
||||
\xff\x09\x04\xd1;\xaf\xef-\xc9X\xa48\xb3?\xb8\xfc\
|
||||
e~?\x80b\x85\xbb\xab%g\xdf\xbe\x9d\x9d\x99\x1d\
|
||||
\x12\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||
\x00\x00\xc0\x9c24\x22\xa7\xab\x87L3l\xf1xk\
|
||||
de\x03\x00\xe3g\x09\x00`\x0e \xd9\x01\xc0\x5c@\
|
||||
\xb2\x03\x80\xb9\x80d\x07\x00s\x01\xc9\x0e\x00\xe6\xc2\x0d\
|
||||
\x02\x18\xe0m\xf5i\xd5Z\xae\xfb\x96KW{\xed\xee\
|
||||
\xf1\xd6f\xef6?T\xac-\xbd!\xbf\xa6\xf4r/\
|
||||
\x11\xc0\x04\xccD\xb2\xbb\xf3\xf2;\x1a\x85\xb3\xaf\x9f\x12\
|
||||
\x00\xcc\x07\xdc\xc6\x02\xc0\x5c@\xb2\x03\x80\xb9\x80d\x07\
|
||||
\x00s\x01\xc9\x0e\x00\xe6\x02\x92\x1d\x00\xcc\x05$;\x00\
|
||||
\x98\x0b31\xf4\x04CD\x00`X\xa8\xd9\x01\xc0\x5c\
|
||||
\xc0\x13\x14c\xf0\xa6\xfa\xac\xfcI\xa9\xb5\xee\x98\x96\xad\
|
||||
\xa1\x0a3W\x88\xcc\xb91t.\xbf\xcb\xbf\xa6\x11E\
|
||||
\xd1\x8b{\x8d\xbf4i\x08\xfa\xd4\x831\xbc&?\xf7\
|
||||
\xe5\xefW\x88\xb8,\x7f\xbb\xa9\xcb\x9c\xa3\xd7\xcc\xe6\xe8\
|
||||
^\xe3\xbb\x06\xc1o\xbaef-}\xa9\xc7E\xcbk\
|
||||
\xd019\xad\x1e\xac9k\xca\x83\xfeF\xc9\xb5N\xee\
|
||||
4\xfez\x92f\x7f\x1a\x0b\xb7JQ\xb5\xe4\x5c\xb5\xff\
|
||||
8\xe9~\x1d\xd1kr\xb6\x91\xf68\xbd]=\xdc\xf0\
|
||||
.tQ\xe3jL\xf5\xc6\x07-wc0b\xfeQ\
|
||||
\x02\xa46L\xfc%\xc5^dm\xe3\x8b\x97\xdf\x1e\xd1\
|
||||
\x04a\xf2N\x8f\x22&\xef\x8c\x1f\x9f2v\x97\x8cY\
|
||||
O\xb3\xbe\x04\x9e\x9e`\x9bY\x82NO\x9eO\xed\xbb\
|
||||
\x1d&~(\x05^N\xb1I\xd3\x92\xa9}~\xfc\xdd\
|
||||
^h\xa5\x0f\xfdq\xb1\xff|}\xb0v\x83\xe9q\xfb\
|
||||
\xc4\xf4\x93\xb2\xda\xd5\xb2\x92x~Eq\x82\xb8\xce\x11\
|
||||
\xef\xdd;\xde\xde\x0d\xfd\x9d<\xc7\x89$v\x16_~\
|
||||
\xfb\xcf\xd0J\xa1\xf3L\xf6\xf5\xfd\xdd\xe3\xed\xfd\xf6\xb1\
|
||||
t\xcf\x92?+\xef\x7f~\xbc\xfd=e\xf0\xdf\xd5\xa7\
|
||||
\x8f\x8a\x8e\xbdQ\xc1m\xec\x88\x9c}}\xb8S\xb2\xa5\
|
||||
Wi\x13\x9db\xa6\xaa&\x0b\x0d\xa04\xebkr\xf9\
|
||||
\xd8^\xbe\x92\x93m7e\xb0\xa9\x8a\xae/'\xc9\x1b\
|
||||
\xdd\x9e\xa6\x88&\x04\xca)\xcb\xb6g\xabO\x1f\xdf`\
|
||||
\xf3<\xe9\xe4WZVg\xab\x87\xcf\x0cS\xe2\xba>\
|
||||
\x9al>\xb1\xad7Y\x8f\x13\xb1\xab\xe9\xbe\xf3\x96\x8b\
|
||||
\xeeK\xe3\xb0}\xd12\x95\xa4\xf5\x1d\x99\x87\xb2~\x9d\
|
||||
R\xd0\xd8\xd1\x0b\xc0,\xc5\xdeDncC\xcf\xba\x0e\
|
||||
\xea\x8c\x98\xb5gcO\xf5j\xc7\xbcK9i@H\
|
||||
\xc2\xa3\xd0\x15\xf0\xac\xfa\xf7e\xb2\xa5z\x86@\xeb\xa7\
|
||||
\xb5\xb0\xba\x04\xdd\xca\xb0\xb7\xcfE\xd0\x93\x92\xb9\xb5{\
|
||||
V=\x5c\xb9\xd3\xd8JuK\xd8\xd5\xaeQ^\xd6%\
|
||||
14\xee\xf4\xd56\xfb\x9dvj\x22\x94\x81\x94\xf1F\
|
||||
\xde{\xa0\xd3\xea?\xd6\xc9\xbaZ\xde\xdb\x1c\xdd\xf7\xc7\
|
||||
\xb6\xb5,\x09oe\xa9\xb1y\x9ee[\xa9E\xads\
|
||||
\xc6$\xad\x17\xdc\x9fW\x0f\x1ej\x8d\xd0\xb7N\xbb\xbc\
|
||||
\xad&\xc5\x0a\xe53\x91\xd8C\xcd\xae`oW\x7f\xd8\
|
||||
\x90\x90\xd9\xa5!i\xc2\x93`\xa8\x0e\xdc\x87\x04\x1b\xdb\
|
||||
\x1b\xcf\x87Ht]\x1at\xcfi\xc24!\xc8I\xa6\
|
||||
'W\x99-\xd5%\xe1-\xa7\xdd\xf6\xfd\x89g*\x9a\
|
||||
\x18B\xb5\xe2\xa2\x8eM\xfa\xf7\xf6\xb4\xaa\x89\x8e\x86\xb7\
|
||||
\xfc\xa9\xbdLU\xdb\xefS\xa1\x1c\x0c\x19\x7f\x19^)\
|
||||
o\x1a\x8e\xc6\xde\xf10\xb5\xf9\xac\x90\xec\x0a\x14\x07\x02\
|
||||
\xd9<A9P\xa9T\x1a\xf8\xb7\xe2v@2\x15*\
|
||||
\xc6\xf2\xe9\xea\xc1.MH\xbb\x86\xda\x93\x10R'\xbc\
|
||||
A'\x9e^$\xb4=n\xd0\xfaE\x1e\x9b4\xda\xed\
|
||||
d\xc5\xd0[L\xdf\xc5o\x04\xca\xbe}\x15\x1c{K\
|
||||
\xd2\x0c\x93\xa9\x96=\x0c$\xbb\x22Y\xaa\x86\x02A\xda\
|
||||
}\xce\xb51\x9b\x1c?\xd0\x1f\xc7\x1cl|\xd6[\x8a\
|
||||
\xfe\xa0\x8bk\x0b\x09\xed\x80\xb2]\xc3Q\xb4\xe9\x5c\xb4\
|
||||
\x92f?\xf2\xcev\xc6y\x85\xbdJ{1\xe32\xe9\
|
||||
\x95\x98\xf0\xbc5\x0c\xf9\xac_\xbc\xdc\xbe\xd6\xeb\xd7\xae\
|
||||
\xd5\xf5\xad\xdb\xe7\xea\xf1\xd1\xb2\x8b\x7fgnR\x0ei\
|
||||
\xf6w\xf58\xc9\xcf\xa6\xdc\xb3\x06o\xdf}\x17\xbf$\
|
||||
\xda\x99\x94-\x1e\xe4\x9d[{\xad\xec\xe3v\xb6\xa46\
|
||||
h\xf9\x0c\xd2\xd1\xf60\xed\xbe\x98\xcc\xdf\xc6\x15{\x18\
|
||||
zR\xa0\xc4\x9a\x03\xb7V\xee\xd5{\x86(\x1c\xbd]\
|
||||
=\xd0\x1e*\xefvrBW\xe5\x9f\xc6o\xffo\xdc\
|
||||
F\xb8\x13\x9d\x8f\xee\xd6\xb7\x1f\xf4\xbd\x98\xb4\x9f\xf2\x82\
|
||||
mim\xa8F\x13\xa0=\x99\xf2\xfe\xa8\xef\xfdu\x13\
|
||||
\xde\xb56\xbcP\xa2[\xacoo\x0c\xdaG\x89K\xeb\
|
||||
\x1c,6jF\x1c\xc9\xf1\xe9iCj\xc8\xbejr\
|
||||
\x5c\xebr\x92W(\x83\xe4Z$\xef\xc9q\xda\xed{\
|
||||
\xb1\xf6\xf3\xca\xe1\xbe$\x8b\x9d\x81[\xb4/~\x95,\
|
||||
\xed\x5c\xce\x91\xf4\xeeo\xd5\xfa^>\x92\xda\xfcm)\
|
||||
\xbf5\xff\x96\xe6\xcb\xfeW\xda\xb5\xba\x10>Z\xcc\x18\
|
||||
{rH\xca\xbf\xb3\x97\x1b\xf2\xab\xb7\x8d\xb0(3[\
|
||||
\xb3\xd3\xce\x85,?\xa3\xf6\xef\xb8\x16b*\xbe\xe5\xcc\
|
||||
\xee\xc5\xa0\xb1Xz\xa2k\x8d\xc2\xbf\x1d\xf7^a\x13\
|
||||
\xae\xac\xce\xb9\x81C\x07\xdaC#\xfc\xb5\x14\x09\xba?\
|
||||
\xd1\x04\xe9\xfbKS\xc3\xd3Z\xc0\xe0DG'\xbeD\
|
||||
\xf7\x8bl#\x89\xaeJ!lw\x07%\x11}\xcd\x19\
|
||||
J\xac\x09]\x95\x14\x0b\x9aX\x17=CU>\xe2\x9b\
|
||||
\xbb\x14 5\xae5J\xaf9 \xd1\xc5$N\x9e\x84\
|
||||
6\xd4\xf1\xa0\xfd\xafI[\xde7\xa1m\xc2\xb1\xe7\xad\
|
||||
\xb5\x1a\x09\xf2\xb1\xc4\x1enc\x0br\xc3r5\xb8\x82\
|
||||
1\x0d\xdf\xa2\x88\xf8\x85o\x99\x9c\xd8\xb7\xbb\xbf\xc7\xb7\
|
||||
\xb0\x01\x9aPCW}\xc7\xf4\xa3o\x99$\xbb*M\
|
||||
X(\xe1i\x8dF\x13\x9d\xf4L\x0eLt\xbf\xf2\xcd\
|
||||
\x15\xdf\xdf\xbd\xa0w\xe1\xf6?M>\x8do3%\xb4\
|
||||
\x90\xa4X`\xc3\xdeD\xf3Y\xdc\xe3\x1a\xbau\xbe^\
|
||||
\xe3\xca\xa9\x99ee\x8d=\xb9`xo7u\x8ch\
|
||||
8\xf6\xf8\xb5\x7f[[\xd4g\x0a\xc2mlA\xe4\x16\
|
||||
c\xd9\x04n\x93\xd8\xb9\x93\xc0\xc2\x9a\xd4\x1e\x1a\x83\x16\
|
||||
9\xb2\xbf\xd5\xfa\x8cu\xcb\xa1[Xc\xccIh\xfc\
|
||||
\x925\xe66\xf9Uh\x0a\xf8niu\xa8\xc2\xc7\xd4\
|
||||
\xd2\xb2\xe8M\x5c\x9dD\x17\x1a\x96\x91TnL\xce{\
|
||||
\x22\xe6!5\xa0`ME\xda\xa9\xce\xf3\x8e3\xb3L\
|
||||
\xa9{\xaa\x0be\xdfU\xa41\xc0\xbbX\x9f\x92\x18&\
|
||||
\xf6\xf4B\x96uhMVHv\x05)\x91\xb9\xcf\xc1\
|
||||
\xe5%\xef\x81\x94+b\x83\xd2`S\x09\x8f\xf72\x8f\
|
||||
$)\xe4\xeeq\x1cG\xc0\xa5\xe1Ix\x95k+\xa6\
|
||||
Ht\xed\xd5\xa8\x1c.6\x7f\xad;\x0fi\x96(\x87\
|
||||
\xda\x07\xe5v\xea\x19\xd9\x12\xe5\x11\xaa]\x8d\x96\xad\x84\
|
||||
\x96j;\xa31\xa5\x1d\xcai\x81.\xf4s\x8d4\xf6\
|
||||
p\x1b;&\x11EC\x1fH&\x1ei\xa0w\x02n\
|
||||
*xni\xdfK\x99\xe8:\xebV\xc2\x8bM\xa1'\
|
||||
\xd9\xe4\x12\xd2\xe8\x1863\xff\x99\x90\xec`j]\xb8\
|
||||
[\xfb\xbe\xce\x1by}s\x1aj\xa1\x83\xf1\x07\x97\xec\
|
||||
\xf8\x03\xf8LHv0\xb5>\xb5\x97\xcf|\xb5$\xe9\
|
||||
\xb4x\x9e\xb6\xdd\xcb\x14\x5cs\x83\xd9\x846\xbb\x82H\
|
||||
\x8f\xeaO6\xd0\xa0&mvC_\x19\x13OZ\xe6\
|
||||
\xa6\xac\xd3\xa4\x9c.haj\x92\x82>\x00\xef\x88B\
|
||||
\xc3,R?_)\xbd\x9f\xe7&\xd4\xb1Sp\xadE\
|
||||
n\xf9\x9al\x02\xb7\xce:\xf06g\xfb\x94td5\
|
||||
i\x12\x92\xf6;T\xec1\x8f#\xf6\x90\xec\x0a\x92\x94\
|
||||
\x88$\x11\xde'\x0a\x8f\x90O\xde\x89\x0eI\x08\xf5*\
|
||||
\x9a\x17\x8b\xf5\xad\x874\xe34\xd1\xc5\x0f\xdf_\xc5:\
|
||||
\xe5\x11\x9f\xf4\x0d\x84M\x99\xf0\x5c3\xd4\x93X\xe0p\
|
||||
\x8eX\xd2\x85O\x12\xd6\x93{\xc7\x83\xc7\xbfM\xafp\
|
||||
\x19\xb2t\xf2,&L\xc20i\xb8\x8d-\x08\x9b(\
|
||||
\x98\xc8\x22\xeb\xbe\xf2-{\xbbrP\xd3y\xc9\x06\xfd\
|
||||
\x5c\x9dr'8|\x85\xe2\x1e\xb1\xf5\xb4\x8f\xdeL\xea\
|
||||
\xf1\xb0$\xbeD\xe78ZY<\xde~@\xd7\x1f?\
|
||||
\xea&\xbc\x8a\xefoF\xae\x14.7\xa9A\x16Y\x1e\
|
||||
r\x1c\xc2\x8f}\xc9q\xa2Y\xe3\x06\x0f\x8d\xea\xcaR\
|
||||
\x86\x93\x8a=$\xbb\x82,\xb8\x85\xa3\xd0rc\xfcc\
|
||||
\xaf\xacI\xf7\xf4\x82\x0eQ\x09=m!\xcaif\xc7\
|
||||
h\xcf\x83\xd7\xaa\xa7\x9d7o\x5cB\x89\xae[s\x8b\
|
||||
\x9f\x92\xc8\x98\xf0\xfe\xd0\xd8:I*\xb7\xd0\x03\xe9Y\
|
||||
{\x22?r\xb7j\xa1\xe5\xfa\xd8W\x9a\xb2o\xcf\x19\
|
||||
w\xf0f\x8c\x13\x00x\xb5\xcb?\xf8\x9cp\xf9\x13\xdb\
|
||||
zL\x09\xba\x03\xc3'\x11{Hv\x05\xd1\x91\xefr\
|
||||
B5|\xcb}\x01~\x1a\xbff*\xbe\xed\xe2)\xb3\
|
||||
\xaf\xfe\x7f`\xf4\xbd\x8a'`\x94\xa41\xe8\xc4\xd7@\
|
||||
\xd3\x07\xd4;s\x91-\xc7\x13)\xea|kS M\
|
||||
\xa2\xeb\xca\x93\xf0\xa4\xfd/i2\x84G:\x8f[\xff\
|
||||
\xab:\x83\x8a\xcdX\x13K\x8a\x85\xf6\xfb\x89\xe7,|\
|
||||
4\xa8\x96\xa3\xaf\xe9\xb2x\xf2W2\xf14\x5c\xd30\
|
||||
\xd1jR\x19\xea\xf1\x93\xe4\xfc<\x14{:\xd9,M\
|
||||
(\xf6f\xb6\xcdnT\x13z\x0e#\xe2h\xcf\x9aR\
|
||||
\xd5\xb7\xbc3K\xeb\x86\x84\xc5\x89\x8e\xed\x92+\xcdZ\
|
||||
b\x97\xbe\xeb\x9d\x0fm\xc1\xdd\xdao\x99\xd6Nh,\
|
||||
\x97\x06\x9d\x9c \x1br\x0b\xdcp\xdcnX\xb6:\xd6\
|
||||
\xcc\xb4\x96\xb9\xbf\xa3\xc4\xba\xfd\xb3\xea\xe1\xeb\xac\x13f\
|
||||
\x16)K\xa2\xeb\xd2\x84w\xbar\xd0\xff\xac\xb0\xb7\x0d\
|
||||
\x8f9:J\x1a\xf4*\xed\xae\x8f\xe5\xf8\xecpg\xe2\
|
||||
\x05\xe9\xb8\xa8\xea@\xee\xd0`q\x9f\xa4XP\x1a\x0f\
|
||||
R\x1b\xd2\xd9\x81O\xba\xc7\xc9\xc4O\x88\xb4$\x19\xf4\
|
||||
(O\xc3D\xab\x1a{\x97\xe6r=<)\x82Y\x93\
|
||||
\xf7\xba\x962\xf6\x1e\x8f3\xf6P\xb3+P\xe76\xb3\
|
||||
\x91\xb0ZE\x03B\x82z#i\xf2M\x9d\x9a\xa7?\
|
||||
\xb8\xb5\xd6\xc0\xcc\xa9\x1a\x82\xb56\xa9\xfb\x89\xf7e\xa8\
|
||||
\xea\xd9_9\xcb0\x8e\xa2\xc5\xfb\xe5\xbe^\xd7\x84D\
|
||||
\xd7\xe5\xab\xe1\x19\xba\xfe\xb0|\xcac\xd3\xd9\xbe]f\
|
||||
\xc1\x87\xf9\x13\xe8\xfe\xa4\xfc\x9f$\xad\xa7\xc7\xe4\xeaq\
|
||||
\x22\xf2>\x0e6\xf1\x89V5\xf6\x1c\xbb\x22c\xef3\
|
||||
\x89\xbd\x7f\x8d+\xf6\x90\xec\x0a&W\xf4\xcd\xbcs\xa0\
|
||||
\xf5\x90\x13\x9e]4\xf0\x09\x82\xc5\xc6\xf6Q\xf0\xe9\x82\
|
||||
\xec\xfb\x9a\x988\xa1qk\xe5\xca{I\x95\xe8\xba4\
|
||||
\xe1\xf5&1\xde\xbb\xdb\x18<\xa5\xb8\x1e\x9b\x84\xb6\xbb\
|
||||
B\xdd\xd5\x9e\xf1\x14s\xc7\xa5\xc6T\x9et\xc7\x92&\
|
||||
\xf1Y\x8d=$\xbb\x82\xc5S\x02\xb1[\x19*\xe1\xc9\
|
||||
\x09o\xb8\xf5 t\xc2'>N\x95a_Y\x92\xcb\
|
||||
(\xc4\x13x\xb6'\xaf\xcc\xf5^n\xf2\xcd\x07\xed\x89\
|
||||
/yo1\xf0-_\xdd\xc4:\xce\x84\xa7\xc98M\
|
||||
\x0d/\x91|>-\x9biyv\xb9\xa0\xd8{#q\
|
||||
\xfe\xe7q\xc5\x1e\x92\xdd\x08t\x13^\xca\xdb\xa6\x1e\xf1\
|
||||
\xec\xb5\x12\xd4i\xbe\x874\x0e:\x9d\x116gb\xd5\
|
||||
\x93\xf0W\xbe\xf9\xd54|\xe1\x8e\xbc\x87\xdab}k\
|
||||
)\xcf{\xd1\xdb+\xd9\xf6\xab\xc5\x84\xaf3Tq\xb9\
|
||||
jM2m\x99i\x0d{\xc8IM\xb5\x86\xd7N\xe6\
|
||||
\xd9\x8fSw\xf6d\xfd|\xd3p\x9c\xba:\xb1\xb7\x94\
|
||||
;\xf6\xa4L%\xf6\xfe\x98\xf6\xfbv\x8b0\x91\x0e\x8a\
|
||||
\xac\x93i\x8ec\xf2\xcd\xa2u\x02sE\xda#6J\
|
||||
&\x9e%\xb7\x1aZ_\x93\x5cd\xf9\xc9\x17\xc7\xdbG\
|
||||
\x94Ag\xc6\x94\xa5\xb4\xfb\xd1\x93\xe7\x9d\xf4\xaa\x19\x8e\
|
||||
\xf6\xa7\xe9\xe4\x19'=\xc1\xb4\xb1_\xae\xf5\xbbV'\
|
||||
-\x1d\xd0\xe0\xae\xe5\xa4=\xdf\x17|k\x7f\xc1\x5c>\
|
||||
4y\xbf^\xacC\x93\xb9\xfcSK{\x9ct\x98\x87\
|
||||
\xf6~\xea\xfe\x97\xea\xd3\xf9\x0cp'~\xf2\xc4\xdeQ\
|
||||
\xea\x99~\x0a4\xdc\x11\x0c\xc0\x97d\xf7\xd2\xb6\x96\x05\
|
||||
\xbaX\x96\xbat\xe5\xfdt9\xaei\x9d=\xff\x95n\
|
||||
5\x8a\xbc=\x89\xc7e\xc9~$\x00oG\xcc\x9f\x95\
|
||||
\x8c\xf9E\xda\xab\xfe\xa7\x03C\xe75\xc1\x85t\xcbK\
|
||||
\x8f\x8b\x96\x15G\xfc\xd3\xd5c\x22\xb1\xac\x1d\x03\xdeG\
|
||||
\xd7$)mf}\x22\x22\x14\x0f\x8e\xdc\xc9\xac\x1e'\
|
||||
-KK\xb6lJ\xe6\xfe\xb4\xc5\x1e\x92\x9dG\xd1\xc9\
|
||||
\x0e\xa6\x8b~\xabY\xda[(\x1d\xd8\x1b\xea\x99\xd5\xa6\
|
||||
\x84I\xd4T \x1b\xb4\xd9\xc1\xdc\xd1D\xc7\xf6\xe6\xab\
|
||||
4\xa3\xf8\x93\x06}\x8bs$\xba\xd9\x80\x89\x00`\xae\
|
||||
\xb4\xbf`\xdc\xc6\xe3\xd5tP\xef\xd9\xea\xe1Z\xcb\xd8\
|
||||
\xbd\xcb\xa8\xd4\xd3\x94\xa0\xdf\xb9\xd0\xfe&7\x0e\x7fm\
|
||||
%Q\xa66V\x98\x1c$;\x98+\xfd_\xf2,\xc9\
|
||||
j\xf9\x06\xbb\xe77\xac\xd3\xdb\xd5\xa6\xbe&\x9d\x11\xe5\
|
||||
\xf6\x93-\xc9-\x19\xbe\xb1\x900}\x90\xec`nt\
|
||||
\xbe\xb8:PS3\x15\xfdo\xfa\xc6f\xdeC\x87\xcf\
|
||||
\xec\x18Y\xb2C\x03?L\x1bv\xb6\x5c\x5c+ux\
|
||||
\x003L\x1ftP\xc0\xdc\xf8}c{\x7f\xd8\x91\xff\
|
||||
:VLnq\xbfG\xa2\x9b=\xa8}\xc1\xdc\x89\x1f\
|
||||
<\x97\xb6;kL\xea)\x860\x18{\xf6!\xd9\xc1\
|
||||
\xdc\xea\xcc\xb6Q\xb5\xd6~\xa3S9\xc9\xc9Py?\
|
||||
u\x167\xe5\xb5\xf3\x88\xe8Gc\xa2\x93\x0b\xb7p4\
|
||||
\xbd\xdff\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||
\x00\x00\x00\x00\x00\x00\x01\xff\x07\xb4<2M\x93'\x06\
|
||||
\xcb\x00\x00\x00\x00IEND\xaeB`\x82\
|
||||
\x00\x00\x03X\
|
||||
<\
|
||||
?xml version=\x221.\
|
||||
0\x22 encoding=\x22utf\
|
||||
-8\x22?>\x0a<!-- Gener\
|
||||
ator: Adobe Illu\
|
||||
strator 23.0.1, \
|
||||
SVG Export Plug-\
|
||||
In . SVG Version\
|
||||
: 6.00 Build 0) \
|
||||
-->\x0a<svg versio\
|
||||
n=\x221.1\x22 id=\x22Laye\
|
||||
r_1\x22 xmlns=\x22http\
|
||||
://www.w3.org/20\
|
||||
00/svg\x22 xmlns:xl\
|
||||
ink=\x22http://www.\
|
||||
w3.org/1999/xlin\
|
||||
k\x22 x=\x220px\x22 y=\x220p\
|
||||
x\x22\x0a\x09 viewBox=\x220 \
|
||||
0 100 100\x22 style\
|
||||
=\x22enable-backgro\
|
||||
und:new 0 0 100 \
|
||||
100\x22 xml:space=\x22\
|
||||
preserve\x22>\x0a<styl\
|
||||
e type=\x22text/css\
|
||||
\x22>\x0a path{fill\
|
||||
:rgb(150, 146, 1\
|
||||
44)}\x0a polygon\
|
||||
{fill:rgb(150, 1\
|
||||
46, 144)}\x0a ci\
|
||||
rcle{fill:rgb(15\
|
||||
0, 146, 144)}\x0a \
|
||||
rect{fill:rgb(\
|
||||
150, 146, 144)}\x0a\
|
||||
</style><path d=\
|
||||
\x22M51.3,75.9c-1.9\
|
||||
,0-3.8-0.8-5-2.4\
|
||||
L18.1,38.4c-2.2-\
|
||||
2.8-1.8-6.9,1-9.\
|
||||
1c2.8-2.2,6.9-1.\
|
||||
8,9.1,1l28.2,35.\
|
||||
1c2.2,2.8,1.8,6.\
|
||||
9-1,9.1\x0a\x09C54.2,7\
|
||||
5.5,52.7,75.9,51\
|
||||
.3,75.9z\x22/>\x0a<pat\
|
||||
h d=\x22M51.3,75.9c\
|
||||
-1.4,0-2.9-0.5-4\
|
||||
-1.4c-2.8-2.2-3.\
|
||||
2-6.3-1-9.1l28.2\
|
||||
-35.1c2.2-2.8,6.\
|
||||
3-3.2,9.1-1c2.8,\
|
||||
2.2,3.2,6.3,1,9.\
|
||||
1L56.4,73.5\x0a\x09C55\
|
||||
.1,75.1,53.2,75.\
|
||||
9,51.3,75.9z\x22/>\x0a\
|
||||
</svg>\x0a\
|
||||
\x00\x00\x03Y\
|
||||
<\
|
||||
?xml version=\x221.\
|
||||
0\x22 encoding=\x22utf\
|
||||
-8\x22?>\x0a<!-- Gener\
|
||||
ator: Adobe Illu\
|
||||
strator 23.0.1, \
|
||||
SVG Export Plug-\
|
||||
In . SVG Version\
|
||||
: 6.00 Build 0) \
|
||||
-->\x0a<svg versio\
|
||||
n=\x221.1\x22 id=\x22Laye\
|
||||
r_1\x22 xmlns=\x22http\
|
||||
://www.w3.org/20\
|
||||
00/svg\x22 xmlns:xl\
|
||||
ink=\x22http://www.\
|
||||
w3.org/1999/xlin\
|
||||
k\x22 x=\x220px\x22 y=\x220p\
|
||||
x\x22\x0a\x09 viewBox=\x220 \
|
||||
0 100 100\x22 style\
|
||||
=\x22enable-backgro\
|
||||
und:new 0 0 100 \
|
||||
100;\x22 xml:space=\
|
||||
\x22preserve\x22>\x0a<sty\
|
||||
le type=\x22text/cs\
|
||||
s\x22>\x0a path{fil\
|
||||
l:rgb(150, 146, \
|
||||
144)}\x0a polygo\
|
||||
n{fill:rgb(150, \
|
||||
146, 144)}\x0a c\
|
||||
ircle{fill:rgb(1\
|
||||
50, 146, 144)}\x0a \
|
||||
rect{fill:rgb\
|
||||
(150, 146, 144)}\
|
||||
\x0a</style><path d\
|
||||
=\x22M31.8,56.4c-1.\
|
||||
9,0-3.8-0.8-5-2.\
|
||||
4c-2.2-2.8-1.8-6\
|
||||
.9,1-9.1l35.1-28\
|
||||
.2c2.8-2.2,6.9-1\
|
||||
.8,9.1,1c2.2,2.8\
|
||||
,1.8,6.9-1,9.1L3\
|
||||
5.8,54.9\x0a\x09C34.6,\
|
||||
55.9,33.2,56.4,3\
|
||||
1.8,56.4z\x22/>\x0a<pa\
|
||||
th d=\x22M66.9,84.6\
|
||||
c-1.4,0-2.9-0.5-\
|
||||
4-1.4L27.7,54.9c\
|
||||
-2.8-2.2-3.2-6.3\
|
||||
-1-9.1c2.2-2.8,6\
|
||||
.3-3.2,9.1-1l35.\
|
||||
1,28.2c2.8,2.2,3\
|
||||
.2,6.3,1,9.1\x0a\x09C7\
|
||||
0.6,83.8,68.8,84\
|
||||
.6,66.9,84.6z\x22/>\
|
||||
\x0a</svg>\x0a\
|
||||
\x00\x00\x03X\
|
||||
<\
|
||||
?xml version=\x221.\
|
||||
0\x22 encoding=\x22utf\
|
||||
-8\x22?>\x0a<!-- Gener\
|
||||
ator: Adobe Illu\
|
||||
strator 23.0.1, \
|
||||
SVG Export Plug-\
|
||||
In . SVG Version\
|
||||
: 6.00 Build 0) \
|
||||
-->\x0a<svg versio\
|
||||
n=\x221.1\x22 id=\x22Laye\
|
||||
r_1\x22 xmlns=\x22http\
|
||||
://www.w3.org/20\
|
||||
00/svg\x22 xmlns:xl\
|
||||
ink=\x22http://www.\
|
||||
w3.org/1999/xlin\
|
||||
k\x22 x=\x220px\x22 y=\x220p\
|
||||
x\x22\x0a\x09 viewBox=\x220 \
|
||||
0 100 100\x22 style\
|
||||
=\x22enable-backgro\
|
||||
und:new 0 0 100 \
|
||||
100\x22 xml:space=\x22\
|
||||
preserve\x22>\x0a<styl\
|
||||
e type=\x22text/css\
|
||||
\x22>\x0a path{fill\
|
||||
:rgb(150, 146, 1\
|
||||
44)}\x0a polygon\
|
||||
{fill:rgb(150, 1\
|
||||
46, 144)}\x0a ci\
|
||||
rcle{fill:rgb(15\
|
||||
0, 146, 144)}\x0a \
|
||||
rect{fill:rgb(\
|
||||
150, 146, 144)}\x0a\
|
||||
</style><path d=\
|
||||
\x22M51.3,75.9c-1.9\
|
||||
,0-3.8-0.8-5-2.4\
|
||||
L18.1,38.4c-2.2-\
|
||||
2.8-1.8-6.9,1-9.\
|
||||
1c2.8-2.2,6.9-1.\
|
||||
8,9.1,1l28.2,35.\
|
||||
1c2.2,2.8,1.8,6.\
|
||||
9-1,9.1\x0a\x09C54.2,7\
|
||||
5.5,52.7,75.9,51\
|
||||
.3,75.9z\x22/>\x0a<pat\
|
||||
h d=\x22M51.3,75.9c\
|
||||
-1.4,0-2.9-0.5-4\
|
||||
-1.4c-2.8-2.2-3.\
|
||||
2-6.3-1-9.1l28.2\
|
||||
-35.1c2.2-2.8,6.\
|
||||
3-3.2,9.1-1c2.8,\
|
||||
2.2,3.2,6.3,1,9.\
|
||||
1L56.4,73.5\x0a\x09C55\
|
||||
.1,75.1,53.2,75.\
|
||||
9,51.3,75.9z\x22/>\x0a\
|
||||
</svg>\x0a\
|
||||
"
|
||||
|
||||
qt_resource_name = b"\
|
||||
\x00\x08\
|
||||
\x05\xe2Y'\
|
||||
\x00l\
|
||||
\x00o\x00g\x00o\x00.\x00p\x00n\x00g\
|
||||
\x00\x08\
|
||||
\x0aaZ\xa7\
|
||||
\x00i\
|
||||
\x00c\x00o\x00n\x00.\x00p\x00n\x00g\
|
||||
\x00\x0d\
|
||||
\x0dq\x0b\x87\
|
||||
\x00c\
|
||||
\x00o\x00l\x00l\x00a\x00p\x00s\x00e\x00d\x00.\x00s\x00v\x00g\
|
||||
\x00\x0c\
|
||||
\x07)\x8aG\
|
||||
\x00e\
|
||||
\x00x\x00p\x00a\x00n\x00d\x00e\x00d\x00.\x00s\x00v\x00g\
|
||||
"
|
||||
|
||||
qt_resource_struct = b"\
|
||||
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00\x01\
|
||||
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
|
||||
\x00\x00\x00L\x00\x00\x00\x00\x00\x01\x00\x00\x14\x9b\
|
||||
\x00\x00\x00\x16\x00\x00\x00\x00\x00\x01\x00\x00\x0d\xe2\
|
||||
\x00\x00\x00,\x00\x00\x00\x00\x00\x01\x00\x00\x11>\
|
||||
"
|
||||
|
||||
def qInitResources():
|
||||
QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data)
|
||||
|
||||
def qCleanupResources():
|
||||
QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data)
|
||||
|
||||
qInitResources()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 312 B |
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
path{fill:rgb(150, 146, 144)}
|
||||
polygon{fill:rgb(150, 146, 144)}
|
||||
circle{fill:rgb(150, 146, 144)}
|
||||
rect{fill:rgb(150, 146, 144)}
|
||||
</style><path d="M31.8,56.4c-1.9,0-3.8-0.8-5-2.4c-2.2-2.8-1.8-6.9,1-9.1l35.1-28.2c2.8-2.2,6.9-1.8,9.1,1c2.2,2.8,1.8,6.9-1,9.1L35.8,54.9
|
||||
C34.6,55.9,33.2,56.4,31.8,56.4z"/>
|
||||
<path d="M66.9,84.6c-1.4,0-2.9-0.5-4-1.4L27.7,54.9c-2.8-2.2-3.2-6.3-1-9.1c2.2-2.8,6.3-3.2,9.1-1l35.1,28.2c2.8,2.2,3.2,6.3,1,9.1
|
||||
C70.6,83.8,68.8,84.6,66.9,84.6z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 857 B |
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 100 100" style="enable-background:new 0 0 100 100" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
path{fill:rgb(150, 146, 144)}
|
||||
polygon{fill:rgb(150, 146, 144)}
|
||||
circle{fill:rgb(150, 146, 144)}
|
||||
rect{fill:rgb(150, 146, 144)}
|
||||
</style><path d="M51.3,75.9c-1.9,0-3.8-0.8-5-2.4L18.1,38.4c-2.2-2.8-1.8-6.9,1-9.1c2.8-2.2,6.9-1.8,9.1,1l28.2,35.1c2.2,2.8,1.8,6.9-1,9.1
|
||||
C54.2,75.5,52.7,75.9,51.3,75.9z"/>
|
||||
<path d="M51.3,75.9c-1.4,0-2.9-0.5-4-1.4c-2.8-2.2-3.2-6.3-1-9.1l28.2-35.1c2.2-2.8,6.3-3.2,9.1-1c2.8,2.2,3.2,6.3,1,9.1L56.4,73.5
|
||||
C55.1,75.1,53.2,75.9,51.3,75.9z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 856 B |
+191
-37
@@ -8,13 +8,17 @@ import threading
|
||||
|
||||
from cefpython3 import cefpython as cef
|
||||
import PySide2
|
||||
from PySide2.QtGui import *
|
||||
from PySide2.QtCore import *
|
||||
from PySide2.QtWidgets import *
|
||||
|
||||
import server.gui.cellxgene_rc
|
||||
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.utils import WINDOWS, LINUX, MAC, FileLoadSignals, Emitter, WorkerSignals
|
||||
from server.utils.constants import MODES
|
||||
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
|
||||
|
||||
if WINDOWS or LINUX:
|
||||
@@ -24,17 +28,19 @@ if WINDOWS or LINUX:
|
||||
|
||||
# Configuration
|
||||
# TODO remember this or calculate it?
|
||||
WIDTH = 1024
|
||||
HEIGHT = 768
|
||||
WIDTH = 1300
|
||||
HEIGHT = 800
|
||||
GUI_PORT = find_available_port("localhost")
|
||||
BROWSER_INDEX = 0
|
||||
LOAD_INDEX = 1
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
super(MainWindow, self).__init__(None)
|
||||
self.cef_widget = None
|
||||
self.data_widget = None
|
||||
self.stacked_layout = None
|
||||
self.parent_conn, self.child_conn = None, None
|
||||
self.load_emitter = None
|
||||
self.emitter_thread = None
|
||||
@@ -49,6 +55,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def showBrowser(self):
|
||||
self.stacked_layout.setCurrentIndex(BROWSER_INDEX)
|
||||
|
||||
def restartOnError(self):
|
||||
self.window().shutdownServer()
|
||||
# close emitter on error/finished
|
||||
@@ -120,8 +127,12 @@ class MainWindow(QMainWindow):
|
||||
file_menu.addAction(load_action)
|
||||
|
||||
def showLoad(self):
|
||||
self.clearMessages()
|
||||
self.stacked_layout.setCurrentIndex(LOAD_INDEX)
|
||||
|
||||
def clearMessages(self):
|
||||
self.data_widget.reset()
|
||||
|
||||
def closeEvent(self, event):
|
||||
# Close browser (force=True) and free CEF reference
|
||||
if self.cef_widget.browser:
|
||||
@@ -147,57 +158,105 @@ class LoadWidget(QFrame):
|
||||
logo_layout = QHBoxLayout()
|
||||
logo_layout.setContentsMargins(0, 0, 0, 20)
|
||||
|
||||
load_layout = QGridLayout()
|
||||
load_layout.setContentsMargins(0, 0, 0, 0)
|
||||
load_layout.setSpacing(0)
|
||||
file_layout = QVBoxLayout()
|
||||
|
||||
message_layout = QHBoxLayout()
|
||||
message_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.serverError = False
|
||||
self.title = ""
|
||||
self.label = QLabel("cellxgene")
|
||||
self.file_name = FilePath()
|
||||
|
||||
self.label = QLabel()
|
||||
logo = QPixmap(":/logo.png")
|
||||
self.label.setPixmap(logo)
|
||||
self.label.setContentsMargins(100, 0, 100, 0)
|
||||
logo_layout.addWidget(self.label)
|
||||
|
||||
# UI section
|
||||
# TODO add load spinner
|
||||
# TODO add cancel button to send back to browser (if available)
|
||||
self.embedding_label = QLabel("embedding: ")
|
||||
load_layout.addWidget(self.embedding_label, 0, 0)
|
||||
self.file_label = QLabel("file: ")
|
||||
load_layout.addWidget(self.file_label, 0, 1)
|
||||
self.embeddings = QComboBox(self)
|
||||
self.embeddings.currentIndexChanged.connect(self.updateEmbedding)
|
||||
self.embeddings.addItems(MODES)
|
||||
self.embedding_selection = [MODES[0]]
|
||||
load_layout.addWidget(self.embeddings, 1, 0)
|
||||
|
||||
self.load = QPushButton("Open...")
|
||||
self.load.clicked.connect(self.onLoad)
|
||||
load_layout.addWidget(self.load, 1, 1)
|
||||
self.file_area = FileArea()
|
||||
self.file_name.signals.changed.connect(self.updatePath)
|
||||
|
||||
self.launch_widget = QPushButton("Launch cellxgene")
|
||||
self.launch_widget.setEnabled(False)
|
||||
self.launch_widget.clicked.connect(self.onLoad)
|
||||
|
||||
self.progress = QProgressBar()
|
||||
self.progress.setTextVisible(False)
|
||||
|
||||
self.advanced_options = CLIOptionsArea(self)
|
||||
|
||||
file_layout.addWidget(self.file_area)
|
||||
file_layout.addWidget(self.advanced_options)
|
||||
self.loading_layout = QStackedLayout()
|
||||
self.loading_layout.addWidget(self.launch_widget)
|
||||
self.loading_layout.addWidget(self.progress)
|
||||
file_layout.addLayout(self.loading_layout)
|
||||
file_layout.setStretch(0, 10)
|
||||
|
||||
# Error section
|
||||
self.error_label = QLabel("")
|
||||
self.error_label.setWordWrap(True)
|
||||
self.error_label.setFixedWidth(self.MAX_CONTENT_WIDTH)
|
||||
message_layout.addWidget(self.error_label, alignment=Qt.AlignTop)
|
||||
message_layout.addWidget(self.error_label)
|
||||
|
||||
# Options Form
|
||||
|
||||
# Layout
|
||||
for l in [logo_layout, load_layout, message_layout ]:
|
||||
for l in [logo_layout, file_layout, message_layout]:
|
||||
load_ui_layout.addLayout(l)
|
||||
|
||||
load_ui_layout.setStretch(2, 10)
|
||||
#TODO remove magic number
|
||||
load_ui_layout.setStretch(1, 10)
|
||||
self.setLayout(load_ui_layout)
|
||||
|
||||
self.timer = QTimer()
|
||||
self.timer.setInterval(100)
|
||||
self.timer.timeout.connect(self.updateProgress)
|
||||
|
||||
self.signals = FileLoadSignals()
|
||||
self.signals.selectedFile.connect(self.createScanpyEngine)
|
||||
self.signals.error.connect(self.onError)
|
||||
|
||||
def updateEmbedding(self, idx):
|
||||
self.embedding_selection = [MODES[idx]]
|
||||
def updatePath(self):
|
||||
file_name = self.file_name.value
|
||||
if file_name:
|
||||
self.file_area.label.setText("File: " + file_name)
|
||||
else:
|
||||
self.file_area.label.setText("")
|
||||
self.launch_widget.setEnabled(bool(file_name))
|
||||
|
||||
def reset(self):
|
||||
self.loading_layout.setCurrentIndex(0)
|
||||
self.timer.stop()
|
||||
self.error_label.setText("")
|
||||
self.file_name.updateValue(None)
|
||||
self.advanced_options.reset()
|
||||
|
||||
def updateProgress(self):
|
||||
curr_val = self.progress.value()
|
||||
next_val = (curr_val + 1) % 100
|
||||
self.progress.setValue(next_val)
|
||||
|
||||
def resetProgress(self):
|
||||
print("here i am")
|
||||
self.progress.setValue(0)
|
||||
self.loading_layout.setCurrentIndex(0)
|
||||
self.timer.stop()
|
||||
|
||||
def createScanpyEngine(self, file_name):
|
||||
try:
|
||||
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()
|
||||
worker = Worker(self.window().parent_conn, self.window().child_conn, file_name, self.title, host="127.0.0.1", port=GUI_PORT,
|
||||
layout=self.embedding_selection)
|
||||
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)
|
||||
self.window().load_emitter.signals.ready.connect(self.onDataReady)
|
||||
self.window().load_emitter.signals.engine_error.connect(self.onServerError)
|
||||
self.window().load_emitter.signals.server_error.connect(self.onServerError)
|
||||
@@ -208,15 +267,14 @@ class LoadWidget(QFrame):
|
||||
self.window().child_conn.close()
|
||||
|
||||
def onLoad(self):
|
||||
options = QFileDialog.Options()
|
||||
# options |= QFileDialog.DontUseNativeDialog
|
||||
file_name, _ = QFileDialog.getOpenFileName(self,
|
||||
"Open H5AD File", "", "H5AD Files (*.h5ad)", options=options)
|
||||
self.title = splitext(basename(file_name))[0]
|
||||
if file_name:
|
||||
self.signals.selectedFile.emit(file_name)
|
||||
if self.file_name.value:
|
||||
# Reset error on reload
|
||||
self.serverError = False
|
||||
self.loading_layout.setCurrentIndex(1)
|
||||
self.timer.start()
|
||||
self.signals.selectedFile.emit(self.file_name.value)
|
||||
else:
|
||||
self.signals.error.emit("Please select a file before launching.")
|
||||
|
||||
def onDataReady(self):
|
||||
self.site_ready_worker = SiteReadyWorker(self.window().url)
|
||||
@@ -228,6 +286,7 @@ class LoadWidget(QFrame):
|
||||
|
||||
def onServerReady(self):
|
||||
if not self.serverError:
|
||||
self.resetProgress()
|
||||
self.window().cef_widget.browser.Navigate(self.window().url)
|
||||
self.window().showBrowser()
|
||||
|
||||
@@ -236,13 +295,105 @@ class LoadWidget(QFrame):
|
||||
if server_error:
|
||||
self.serverError = True
|
||||
# Report error and switch to load screen
|
||||
self.window().shutdownServer()
|
||||
self.window().shutdownServer()
|
||||
self.resetProgress()
|
||||
self.window().stacked_layout.setCurrentIndex(LOAD_INDEX)
|
||||
self.error_label.setText(f"Error: {err}")
|
||||
self.error_label.resize(self.MAX_CONTENT_WIDTH, self.error_label.height())
|
||||
self.window().repaint()
|
||||
|
||||
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):
|
||||
def __init__(self):
|
||||
super(FilePath, self).__init__()
|
||||
self.value = ""
|
||||
self.signals = FileChanged()
|
||||
|
||||
def updateValue(self, path=None):
|
||||
self.value = path
|
||||
self.signals.changed.emit(self.value != path)
|
||||
|
||||
|
||||
class FileArea(QFrame):
|
||||
def __init__(self):
|
||||
super(FileArea, self).__init__()
|
||||
self.setFrameShape(QFrame.Box)
|
||||
self.setMinimumHeight(100)
|
||||
self.setFixedWidth(500)
|
||||
self.setAcceptDrops(True)
|
||||
self.instructions = QLabel(self)
|
||||
self.instructions.setText("Drag & Drop a h5ad file to load or open")
|
||||
self.instructions.setGeometry(10, 10, 500, self.instructions.height())
|
||||
self.loadButton = QPushButton("Open...", parent=self)
|
||||
x_pos = (500 - self.loadButton.width()) / 2
|
||||
self.loadButton.setGeometry(x_pos, 50, self.loadButton.width(), self.loadButton.height())
|
||||
self.loadButton.clicked.connect(self.fileBrowse)
|
||||
self.label = QLabel(self)
|
||||
self.label.setGeometry(10, 75, 500, self.label.height())
|
||||
|
||||
def fileBrowse(self):
|
||||
options = QFileDialog.Options()
|
||||
# options |= QFileDialog.DontUseNativeDialog
|
||||
file_name, _ = QFileDialog.getOpenFileName(self,
|
||||
"Open H5AD File", "", "H5AD Files (*.h5ad)", options=options)
|
||||
if file_name:
|
||||
self.parent().file_name.updateValue(file_name)
|
||||
|
||||
def dragEnterEvent(self, e):
|
||||
if e.mimeData().hasUrls:
|
||||
e.accept()
|
||||
else:
|
||||
e.ignore()
|
||||
|
||||
def dragMoveEvent(self, e):
|
||||
if e.mimeData().hasUrls:
|
||||
e.accept()
|
||||
else:
|
||||
e.ignore()
|
||||
|
||||
def dropEvent(self, e):
|
||||
"""
|
||||
Drop files directly onto the widget
|
||||
File locations are stored in fname
|
||||
:param e:
|
||||
:return:
|
||||
"""
|
||||
if e.mimeData().hasUrls:
|
||||
e.setDropAction(Qt.CopyAction)
|
||||
e.accept()
|
||||
for url in e.mimeData().urls():
|
||||
file_name = str(url.toLocalFile())
|
||||
self.parent().file_name.updateValue(file_name)
|
||||
else:
|
||||
e.ignore()
|
||||
|
||||
|
||||
def main():
|
||||
# This generates an error.log file on error
|
||||
@@ -256,6 +407,9 @@ def main():
|
||||
cef.Initialize(settings)
|
||||
app = CefApplication(sys.argv)
|
||||
main_window = MainWindow()
|
||||
main_window.setWindowTitle("cellxgene")
|
||||
main_window.setUnifiedTitleAndToolBarOnMac(True)
|
||||
main_window.setWindowIcon(QIcon(":icon.png"))
|
||||
main_window.show()
|
||||
main_window.activateWindow()
|
||||
main_window.raise_()
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
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)
|
||||
@@ -41,6 +41,11 @@ class SiteReadySignals(QObject):
|
||||
|
||||
class FileLoadSignals(QObject):
|
||||
selectedFile = Signal(str)
|
||||
error = Signal(str)
|
||||
|
||||
|
||||
class FileChanged(QObject):
|
||||
changed = Signal(bool)
|
||||
|
||||
|
||||
class Emitter:
|
||||
|
||||
@@ -21,13 +21,13 @@ class EmittingProcess(Process):
|
||||
|
||||
|
||||
class Worker(EmittingProcess):
|
||||
def __init__(self, parent_conn, child_conn, data_file, title, host, port, layout=["umap"], *args, **kwargs):
|
||||
def __init__(self, parent_conn, child_conn, data_file, host, port, title, engine_options, *args, **kwargs):
|
||||
super(Worker, self).__init__(parent_conn, child_conn)
|
||||
self.data_file = data_file
|
||||
self.layout = layout
|
||||
self.title = title
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.title = title
|
||||
self.engine_options = engine_options
|
||||
|
||||
def run(self):
|
||||
super(Worker, self).run()
|
||||
@@ -47,12 +47,12 @@ class Worker(EmittingProcess):
|
||||
# load data
|
||||
try:
|
||||
args = {
|
||||
"layout": self.layout,
|
||||
"max_category_items": 100,
|
||||
"diffexp_lfc_cutoff": 0.01,
|
||||
"obs_names": None,
|
||||
"var_names": None,
|
||||
}
|
||||
args.update(self.engine_options)
|
||||
data = ScanpyEngine(self.data_file, args)
|
||||
server.attach_data(data, self.title)
|
||||
self.emit("ready")
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
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)
|
||||
@@ -0,0 +1,7 @@
|
||||
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