From b7f77488707d29adad9435a76e57738fe3d698fc Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Mon, 16 Oct 2023 17:46:24 -0700 Subject: [PATCH 01/29] Adding the bulletin dialog box class --- sleap/gui/dialogs/bulletin.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 sleap/gui/dialogs/bulletin.py diff --git a/sleap/gui/dialogs/bulletin.py b/sleap/gui/dialogs/bulletin.py new file mode 100644 index 000000000..2030c1feb --- /dev/null +++ b/sleap/gui/dialogs/bulletin.py @@ -0,0 +1,30 @@ +""" +GUI for displaying the new announcement. +""" + +from typing import List +from qtpy import QtWidgets + + +class BulletinDialog(QtWidgets.QDialog): + """ + Dialog window to display the announcement. + """ + + app: "MainWindow" + + def __init__(self, *args, **kwargs): + super(BulletinDialog, self).__init__(*args, **kwargs) + + self.setWindowTitle("Announcement") + self.info_msg() + + def info_msg(self): + """Display information about changes.""" + msg = QtWidgets.QMessageBox() + information = self.app.state["announcement"] + msg.setText( + self.app.state["announcement"] + ) + msg.exec_() + From 8706ccb129fc03c1c7f6c604c256515679284877 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Mon, 16 Oct 2023 23:17:37 -0700 Subject: [PATCH 02/29] Add new variable to enable bulletin dialogue --- sleap/gui/app.py | 6 ++++++ sleap/gui/dialogs/bulletin.py | 25 +++++++++++-------------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/sleap/gui/app.py b/sleap/gui/app.py index 1d6b9d976..3af68f623 100644 --- a/sleap/gui/app.py +++ b/sleap/gui/app.py @@ -65,6 +65,7 @@ from sleap.gui.dialogs.formbuilder import FormBuilderModalDialog from sleap.gui.dialogs.metrics import MetricsTableDialog from sleap.gui.dialogs.shortcuts import ShortcutDialog +from sleap.gui.dialogs.bulletin import BulletinDialog from sleap.gui.overlays.instance import InstanceOverlay from sleap.gui.overlays.tracks import TrackListOverlay, TrackTrailOverlay from sleap.gui.shortcuts import Shortcuts @@ -172,6 +173,11 @@ def __init__( self.release_checker = ReleaseChecker() self.announcement_checker = AnnouncementChecker(app=self) + self.new_announcement = self.announcement_checker.new_announcement + + if self.new_announcement: + self.display_bulletin = BulletinDialog(app=self) + if self.state["share usage data"]: ping_analytics() diff --git a/sleap/gui/dialogs/bulletin.py b/sleap/gui/dialogs/bulletin.py index 2030c1feb..c91d8cd8d 100644 --- a/sleap/gui/dialogs/bulletin.py +++ b/sleap/gui/dialogs/bulletin.py @@ -6,25 +6,22 @@ from qtpy import QtWidgets -class BulletinDialog(QtWidgets.QDialog): - """ - Dialog window to display the announcement. - """ +class BulletinDialog(QtWidgets.QDialog): + """ + Dialog window to display the announcement. + """ app: "MainWindow" - def __init__(self, *args, **kwargs): - super(BulletinDialog, self).__init__(*args, **kwargs) + def __init__(self, *args, **kwargs): + super(BulletinDialog, self).__init__(*args, **kwargs) self.setWindowTitle("Announcement") - self.info_msg() + self.info_msg() - def info_msg(self): - """Display information about changes.""" - msg = QtWidgets.QMessageBox() + def info_msg(self): + """Display information about changes.""" + msg = QtWidgets.QMessageBox() information = self.app.state["announcement"] - msg.setText( - self.app.state["announcement"] - ) + msg.setText(information) msg.exec_() - From 2c6851868bd3d867ae87d4b09af9012824b6ba46 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Thu, 2 Nov 2023 00:19:43 -0700 Subject: [PATCH 03/29] Add bulletin popup to application --- docs/bulletin.json | 7 + sleap/gui/app.py | 60 +- sleap/gui/dialogs/bulletin.py | 39 +- sleap/gui/dialogs/bulletin/markdown.css | 317 ++++ sleap/gui/dialogs/bulletin/markdown.html | 30 + sleap/gui/dialogs/bulletin/marked.js | 1514 +++++++++++++++++ .../test_bulletin.json | 2 +- 7 files changed, 1943 insertions(+), 26 deletions(-) create mode 100644 docs/bulletin.json create mode 100644 sleap/gui/dialogs/bulletin/markdown.css create mode 100644 sleap/gui/dialogs/bulletin/markdown.html create mode 100644 sleap/gui/dialogs/bulletin/marked.js diff --git a/docs/bulletin.json b/docs/bulletin.json new file mode 100644 index 000000000..e8f90db4a --- /dev/null +++ b/docs/bulletin.json @@ -0,0 +1,7 @@ +[ + { + "title": "SLEAP v1.3.2", + "date": "10/30/2023", + "content": "\n\n\nSLEAP 1.3.2 adds some nice usability features thanks to both the community ideas and new contributors! See [1.3.0](https://github.com/talmolab/sleap/releases/tag/v1.3.0) and [1.3.1](https://github.com/talmolab/sleap/releases/tag/v1.3.1) for previous notable changes. As a reminder:\n\n> The 1.3.1 dependency update requires [Mamba](https://mamba.readthedocs.io/en/latest/index.html) for faster dependency resolution. If you already have anaconda installed, then you _can_ set the solver to libmamba in the base environment:\n>```\n>conda update -n base conda\n>conda install -n base conda-libmamba-solver\n>conda config --set solver libmamba\n>```\n>Any subsequent `mamba` commands in the docs will need to be replaced with `conda` if you choose to use your existing Anaconda installation. \n>\n>Otherwise, follow the [recommended installation instruction for Mamba](https://mamba.readthedocs.io/en/latest/installation.html).\n\n### Quick install\n**`mamba` (Windows/Linux/GPU)**:\n```\nmamba create -y -n sleap -c conda-forge -c nvidia -c sleap -c anaconda sleap=1.3.2\n```\n\n**`mamba` (Mac)**:\n```\nmamba create -y -n sleap -c conda-forge -c anaconda -c sleap sleap=1.3.2\n```\n\n**`pip` (any OS except Apple Silicon)**:\n```\npip install sleap[pypi]==1.3.2\n```\n\n### Highlights\n* Limit max tracks via track-local queues by @shrivaths16 and @talmo in https://github.com/talmolab/sleap/pull/1447\n* Add option to remove videos in batch by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1382 and https://github.com/talmolab/sleap/pull/1406\n* Add shortcut to export analysis for current video by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1414 and https://github.com/talmolab/sleap/pull/1444\n* Add video path and frame indices to metrics by @roomrys in https://github.com/talmolab/sleap/pull/1396\n* Add a button for copying model config to clipboard by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1433\n* Add Option to Export CSV by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1438\n\n### Full Changelog\n\n#### Enhancements\n* Add option to remove videos in batch by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1382 and https://github.com/talmolab/sleap/pull/1406\n* Add `Track` when add `Instance` by @roomrys in https://github.com/talmolab/sleap/pull/1408\n* Add `Video` to cache when adding `Track` by @roomrys in https://github.com/talmolab/sleap/pull/1407\n* Add shortcut to export analysis for current video by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1414 and https://github.com/talmolab/sleap/pull/1444\n* Add video path and frame indices to metrics by @roomrys in https://github.com/talmolab/sleap/pull/1396\n* Improve error message for detecting video backend by @roomrys in https://github.com/talmolab/sleap/pull/1441\n* Add a button for copying model config to clipboard by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1433\n* Add Option to Export CSV by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1438\n* Limit max tracks via track-local queues by @shrivaths16 and @talmo in https://github.com/talmolab/sleap/pull/1447\n\n#### Fixes\n* Minor fix in computation of OKS by @shrivaths16 in https://github.com/talmolab/sleap/pull/1383 and https://github.com/talmolab/sleap/pull/1399\n* Fix `Filedialog` to work across (mac)OS by @roomrys in https://github.com/talmolab/sleap/pull/1393\n* Fix panning bounding box by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1398\n* Fix skeleton templates by @roomrys in https://github.com/talmolab/sleap/pull/1404\n* Fix labels export for json by @roomrys in https://github.com/talmolab/sleap/pull/1410\n* Correct GUI state emulation by @roomrys in https://github.com/talmolab/sleap/pull/1422\n* Update status message on status bar by @shrivaths16 in https://github.com/talmolab/sleap/pull/1411\n* Fix error thrown when last video is deleted by @shrivaths16 in https://github.com/talmolab/sleap/pull/1421\n* Add model folder to the unzip path by @roomrys in https://github.com/talmolab/sleap/pull/1445\n* Fix drag and drop by @talmo in https://github.com/talmolab/sleap/pull/1449\n\n#### Dependencies\n* Pin micromamba version by @roomrys in https://github.com/talmolab/sleap/pull/1376\n* Add pip extras by @roomrys in https://github.com/talmolab/sleap/pull/1481\n\n#### New Contributors\n* @shrivaths16 made their first contribution in https://github.com/talmolab/sleap/pull/1383\n* @gitttt-1234 made their first contribution in https://github.com/talmolab/sleap/pull/1382\n* @KevinZ0217 made their first contribution in https://github.com/talmolab/sleap/pull/1414\n\n**Full Changelog**: https://github.com/talmolab/sleap/compare/v1.3.1...v1.3.2\n" + } +] \ No newline at end of file diff --git a/sleap/gui/app.py b/sleap/gui/app.py index d00ab5362..41098abb6 100644 --- a/sleap/gui/app.py +++ b/sleap/gui/app.py @@ -53,11 +53,15 @@ from logging import getLogger from pathlib import Path from typing import Callable, List, Optional, Tuple +from datetime import date from qtpy import QtCore, QtGui -from qtpy.QtCore import QEvent, Qt +from qtpy.QtCore import QEvent, Qt, QUrl from qtpy.QtWidgets import QApplication, QMainWindow, QMessageBox +from qtpy.QtWebChannel import QWebChannel +from qtpy.QtWebEngineWidgets import QWebEngineView + import sleap from sleap.gui.color import ColorManager from sleap.gui.commands import CommandContext, UpdateTopic @@ -159,8 +163,21 @@ def __init__( self.state["share usage data"] = prefs["share usage data"] self.state["skeleton_preview_image"] = None self.state["skeleton_description"] = "No skeleton loaded yet" - self.state["announcement last seen date"] = prefs["announcement last seen date"] - self.state["announcement"] = prefs["announcement"] + + if prefs["announcement last seen date"]: + self.state["announcement last seen date"] = prefs[ + "announcement last seen date" + ] + else: + self.state["announcement last seen date"] = date.today().strftime( + "%m/%d/%Y" + ) + + if prefs["announcement"]: + self.state["announcement"] = prefs["announcement"] + else: + self.state["announcement"] = "No data to display" + if no_usage_data: self.state["share usage data"] = False self.state["clipboard_track"] = None @@ -171,12 +188,9 @@ def __init__( self.state.connect("show non-visible nodes", self.plotFrame) self.release_checker = ReleaseChecker() - self.announcement_checker = AnnouncementChecker(state=self.state) - self.new_announcement = self.announcement_checker.new_announcement - - if self.new_announcement: - self.display_bulletin = BulletinDialog(app=self) + self.announcement_checker = AnnouncementChecker(state=self.state) + self.new_announcement_available = self.announcement_checker.new_announcement if self.state["share usage data"]: ping_analytics() @@ -197,6 +211,36 @@ def __init__( else: self.state["project_loaded"] = False + # Display announcement bulletin popup + if self.new_announcement_available: + self.display_bulletin = BulletinDialog() + self.bulletin_dialog() + + def bulletin_dialog(self): + """Displays bulletin dialog is new announcement is available.""" + announcement = self.announcement_checker.get_latest_announcement() + + if announcement: + title, date, content = announcement + bulletin_markdown = "\n".join(content.split("\n")) + + channel = QWebChannel() + channel.registerObject("content", self.display_bulletin) + + self.display_bulletin.set_text(bulletin_markdown) + + view = QWebEngineView() + view.page().setWebChannel(channel) + base_path = os.path.dirname(os.path.abspath(os.path.join(__file__))) + filepath = os.path.join( + base_path, "..", "gui", "dialogs", "bulletin", "markdown.html" + ) + url = QUrl.fromLocalFile(filepath) + view.load(url) + view.resize(720, 540) + view.show() + QApplication.exec_() + def setWindowTitle(self, value): """Sets window title (if value is not None).""" if value is not None: diff --git a/sleap/gui/dialogs/bulletin.py b/sleap/gui/dialogs/bulletin.py index c91d8cd8d..cbf19ebdb 100644 --- a/sleap/gui/dialogs/bulletin.py +++ b/sleap/gui/dialogs/bulletin.py @@ -2,26 +2,31 @@ GUI for displaying the new announcement. """ -from typing import List -from qtpy import QtWidgets +import os +import sleap +import sleap.gui.web +from qtpy.QtWidgets import QApplication +from qtpy.QtCore import Property, Signal, QObject, QUrl +from qtpy.QtWebChannel import QWebChannel +from qtpy.QtWebEngineWidgets import QWebEngineView +from sleap.gui.commands import CommandContext +from sleap.io.dataset import Labels -class BulletinDialog(QtWidgets.QDialog): - """ - Dialog window to display the announcement. - """ +class BulletinDialog(QObject): + textChanged = Signal(str) - app: "MainWindow" + def __init__(self, parent=None): + super().__init__(parent) + self.m_text = "" - def __init__(self, *args, **kwargs): - super(BulletinDialog, self).__init__(*args, **kwargs) + def get_text(self): + return self.m_text - self.setWindowTitle("Announcement") - self.info_msg() + def set_text(self, text): + if self.m_text == text: + return + self.m_text = text + self.textChanged.emit(self.m_text) - def info_msg(self): - """Display information about changes.""" - msg = QtWidgets.QMessageBox() - information = self.app.state["announcement"] - msg.setText(information) - msg.exec_() + text = Property(str, fget=get_text, fset=set_text, notify=textChanged) diff --git a/sleap/gui/dialogs/bulletin/markdown.css b/sleap/gui/dialogs/bulletin/markdown.css new file mode 100644 index 000000000..66a5105c5 --- /dev/null +++ b/sleap/gui/dialogs/bulletin/markdown.css @@ -0,0 +1,317 @@ +body { + margin: 0 auto; + font-family: Georgia, Palatino, serif; + color: #444444; + line-height: 1; + max-width: 960px; + padding: 30px; +} + +h1, +h2, +h3, +h4 { + color: #111111; + font-weight: 400; +} + +h1, +h2, +h3, +h4, +h5, +p { + margin-bottom: 24px; + padding: 0; +} + +h1 { + font-size: 48px; +} + +h2 { + font-size: 36px; + /* The bottom margin is small. It's designed to be used with gray meta text + * below a post title. */ + margin: 24px 0 6px; +} + +h3 { + font-size: 24px; +} + +h4 { + font-size: 21px; +} + +h5 { + font-size: 18px; +} + +a { + color: #0099ff; + margin: 0; + padding: 0; + vertical-align: baseline; +} + +a:hover { + text-decoration: none; + color: #ff6600; +} + +a:visited { + color: purple; +} + +ul, +ol { + padding: 0; + margin: 0; +} + +li { + line-height: 24px; +} + +li ul, +li ul { + margin-left: 24px; +} + +p, +ul, +ol { + font-size: 16px; + line-height: 24px; + max-width: 540px; +} + +pre { + padding: 0px 24px; + max-width: 800px; + white-space: pre-wrap; +} + +code { + font-family: Consolas, Monaco, Andale Mono, monospace; + line-height: 1.5; + font-size: 13px; +} + +aside { + display: block; + float: right; + width: 390px; +} + +blockquote { + border-left: .5em solid #eee; + padding: 0 2em; + margin-left: 0; + max-width: 476px; +} + +blockquote cite { + font-size: 14px; + line-height: 20px; + color: #bfbfbf; +} + +blockquote cite:before { + content: '\2014 \00A0'; +} + +blockquote p { + color: #666; + max-width: 460px; +} + +hr { + width: 540px; + text-align: left; + margin: 0 auto 0 0; + color: #999; +} + +/* Code below this line is copyright Twitter Inc. */ + +button, +input, +select, +textarea { + font-size: 100%; + margin: 0; + vertical-align: baseline; + *vertical-align: middle; +} + +button, +input { + line-height: normal; + *overflow: visible; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} + +input[type=checkbox], +input[type=radio] { + cursor: pointer; +} + +/* override default chrome & firefox settings */ +input:not([type="image"]), +textarea { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +input[type="search"] { + -webkit-appearance: textfield; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +label, +input, +select, +textarea { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: normal; + line-height: normal; + margin-bottom: 18px; +} + +input[type=checkbox], +input[type=radio] { + cursor: pointer; + margin-bottom: 0; +} + +input[type=text], +input[type=password], +textarea, +select { + display: inline-block; + width: 210px; + padding: 4px; + font-size: 13px; + font-weight: normal; + line-height: 18px; + height: 18px; + color: #808080; + border: 1px solid #ccc; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +select, +input[type=file] { + height: 27px; + line-height: 27px; +} + +textarea { + height: auto; +} + +/* grey out placeholders */ +:-moz-placeholder { + color: #bfbfbf; +} + +::-webkit-input-placeholder { + color: #bfbfbf; +} + +input[type=text], +input[type=password], +select, +textarea { + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; + -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); +} + +input[type=text]:focus, +input[type=password]:focus, +textarea:focus { + outline: none; + border-color: rgba(82, 168, 236, 0.8); + -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); + -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); +} + +/* buttons */ +button { + display: inline-block; + padding: 4px 14px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + line-height: 18px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + background-color: #0064cd; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd)); + background-image: -moz-linear-gradient(top, #049cdb, #0064cd); + background-image: -ms-linear-gradient(top, #049cdb, #0064cd); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd)); + background-image: -webkit-linear-gradient(top, #049cdb, #0064cd); + background-image: -o-linear-gradient(top, #049cdb, #0064cd); + background-image: linear-gradient(top, #049cdb, #0064cd); + color: #fff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + border: 1px solid #004b9a; + border-bottom-color: #003f81; + -webkit-transition: 0.1s linear all; + -moz-transition: 0.1s linear all; + transition: 0.1s linear all; + border-color: #0064cd #0064cd #003f81; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); +} + +button:hover { + color: #fff; + background-position: 0 -15px; + text-decoration: none; +} + +button:active { + -webkit-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +button::-moz-focus-inner { + padding: 0; + border: 0; +} \ No newline at end of file diff --git a/sleap/gui/dialogs/bulletin/markdown.html b/sleap/gui/dialogs/bulletin/markdown.html new file mode 100644 index 000000000..0627f0868 --- /dev/null +++ b/sleap/gui/dialogs/bulletin/markdown.html @@ -0,0 +1,30 @@ + + + + + Bulletin + + + + + +
+ + + \ No newline at end of file diff --git a/sleap/gui/dialogs/bulletin/marked.js b/sleap/gui/dialogs/bulletin/marked.js new file mode 100644 index 000000000..25be68253 --- /dev/null +++ b/sleap/gui/dialogs/bulletin/marked.js @@ -0,0 +1,1514 @@ +/** + * marked - a markdown parser + * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ + +; (function (root) { + 'use strict'; + + /** + * Block-Level Grammar + */ + + var block = { + newline: /^\n+/, + code: /^( {4}[^\n]+\n*)+/, + fences: noop, + hr: /^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/, + heading: /^ *(#{1,6}) *([^\n]+?) *(?:#+ *)?(?:\n+|$)/, + nptable: noop, + blockquote: /^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/, + list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, + html: '^ {0,3}(?:' // optional indentation + + '<(script|pre|style)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)' // (1) + + '|comment[^\\n]*(\\n+|$)' // (2) + + '|<\\?[\\s\\S]*?\\?>\\n*' // (3) + + '|\\n*' // (4) + + '|\\n*' // (5) + + '|)[\\s\\S]*?(?:\\n{2,}|$)' // (6) + + '|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=\\h*\\n)[\\s\\S]*?(?:\\n{2,}|$)' // (7) open tag + + '|(?=\\h*\\n)[\\s\\S]*?(?:\\n{2,}|$)' // (7) closing tag + + ')', + def: /^ {0,3}\[(label)\]: *\n? *]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/, + table: noop, + lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/, + paragraph: /^([^\n]+(?:\n(?!hr|heading|lheading| {0,3}>|<\/?(?:tag)(?: +|\n|\/?>)|<(?:script|pre|style|!--))[^\n]+)*)/, + text: /^[^\n]+/ + }; + + block._label = /(?!\s*\])(?:\\[\[\]]|[^\[\]])+/; + block._title = /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/; + block.def = edit(block.def) + .replace('label', block._label) + .replace('title', block._title) + .getRegex(); + + block.bullet = /(?:[*+-]|\d+\.)/; + block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/; + block.item = edit(block.item, 'gm') + .replace(/bull/g, block.bullet) + .getRegex(); + + block.list = edit(block.list) + .replace(/bull/g, block.bullet) + .replace('hr', '\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))') + .replace('def', '\\n+(?=' + block.def.source + ')') + .getRegex(); + + block._tag = 'address|article|aside|base|basefont|blockquote|body|caption' + + '|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption' + + '|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe' + + '|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option' + + '|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr' + + '|track|ul'; + block._comment = //; + block.html = edit(block.html, 'i') + .replace('comment', block._comment) + .replace('tag', block._tag) + .replace('attribute', / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/) + .getRegex(); + + block.paragraph = edit(block.paragraph) + .replace('hr', block.hr) + .replace('heading', block.heading) + .replace('lheading', block.lheading) + .replace('tag', block._tag) // pars can be interrupted by type (6) html blocks + .getRegex(); + + block.blockquote = edit(block.blockquote) + .replace('paragraph', block.paragraph) + .getRegex(); + + /** + * Normal Block Grammar + */ + + block.normal = merge({}, block); + + /** + * GFM Block Grammar + */ + + block.gfm = merge({}, block.normal, { + fences: /^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\n? *\1 *(?:\n+|$)/, + paragraph: /^/, + heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/ + }); + + block.gfm.paragraph = edit(block.paragraph) + .replace('(?!', '(?!' + + block.gfm.fences.source.replace('\\1', '\\2') + '|' + + block.list.source.replace('\\1', '\\3') + '|') + .getRegex(); + + /** + * GFM + Tables Block Grammar + */ + + block.tables = merge({}, block.gfm, { + nptable: /^ *([^|\n ].*\|.*)\n *([-:]+ *\|[-| :]*)(?:\n((?:.*[^>\n ].*(?:\n|$))*)\n*|$)/, + table: /^ *\|(.+)\n *\|?( *[-:]+[-| :]*)(?:\n((?: *[^>\n ].*(?:\n|$))*)\n*|$)/ + }); + + /** + * Pedantic grammar + */ + + block.pedantic = merge({}, block.normal, { + html: edit( + '^ *(?:comment *(?:\\n|\\s*$)' + + '|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)' // closed tag + + '|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))') + .replace('comment', block._comment) + .replace(/tag/g, '(?!(?:' + + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub' + + '|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)' + + '\\b)\\w+(?!:|[^\\w\\s@]*@)\\b') + .getRegex(), + def: /^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/ + }); + + /** + * Block Lexer + */ + + function Lexer(options) { + this.tokens = []; + this.tokens.links = {}; + this.options = options || marked.defaults; + this.rules = block.normal; + + if (this.options.pedantic) { + this.rules = block.pedantic; + } else if (this.options.gfm) { + if (this.options.tables) { + this.rules = block.tables; + } else { + this.rules = block.gfm; + } + } + } + + /** + * Expose Block Rules + */ + + Lexer.rules = block; + + /** + * Static Lex Method + */ + + Lexer.lex = function (src, options) { + var lexer = new Lexer(options); + return lexer.lex(src); + }; + + /** + * Preprocessing + */ + + Lexer.prototype.lex = function (src) { + src = src + .replace(/\r\n|\r/g, '\n') + .replace(/\t/g, ' ') + .replace(/\u00a0/g, ' ') + .replace(/\u2424/g, '\n'); + + return this.token(src, true); + }; + + /** + * Lexing + */ + + Lexer.prototype.token = function (src, top) { + src = src.replace(/^ +$/gm, ''); + var next, + loose, + cap, + bull, + b, + item, + space, + i, + tag, + l, + isordered, + istask, + ischecked; + + while (src) { + // newline + if (cap = this.rules.newline.exec(src)) { + src = src.substring(cap[0].length); + if (cap[0].length > 1) { + this.tokens.push({ + type: 'space' + }); + } + } + + // code + if (cap = this.rules.code.exec(src)) { + src = src.substring(cap[0].length); + cap = cap[0].replace(/^ {4}/gm, ''); + this.tokens.push({ + type: 'code', + text: !this.options.pedantic + ? cap.replace(/\n+$/, '') + : cap + }); + continue; + } + + // fences (gfm) + if (cap = this.rules.fences.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'code', + lang: cap[2], + text: cap[3] || '' + }); + continue; + } + + // heading + if (cap = this.rules.heading.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'heading', + depth: cap[1].length, + text: cap[2] + }); + continue; + } + + // table no leading pipe (gfm) + if (top && (cap = this.rules.nptable.exec(src))) { + item = { + type: 'table', + header: splitCells(cap[1].replace(/^ *| *\| *$/g, '')), + align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), + cells: cap[3] ? cap[3].replace(/\n$/, '').split('\n') : [] + }; + + if (item.header.length === item.align.length) { + src = src.substring(cap[0].length); + + for (i = 0; i < item.align.length; i++) { + if (/^ *-+: *$/.test(item.align[i])) { + item.align[i] = 'right'; + } else if (/^ *:-+: *$/.test(item.align[i])) { + item.align[i] = 'center'; + } else if (/^ *:-+ *$/.test(item.align[i])) { + item.align[i] = 'left'; + } else { + item.align[i] = null; + } + } + + for (i = 0; i < item.cells.length; i++) { + item.cells[i] = splitCells(item.cells[i], item.header.length); + } + + this.tokens.push(item); + + continue; + } + } + + // hr + if (cap = this.rules.hr.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'hr' + }); + continue; + } + + // blockquote + if (cap = this.rules.blockquote.exec(src)) { + src = src.substring(cap[0].length); + + this.tokens.push({ + type: 'blockquote_start' + }); + + cap = cap[0].replace(/^ *> ?/gm, ''); + + // Pass `top` to keep the current + // "toplevel" state. This is exactly + // how markdown.pl works. + this.token(cap, top); + + this.tokens.push({ + type: 'blockquote_end' + }); + + continue; + } + + // list + if (cap = this.rules.list.exec(src)) { + src = src.substring(cap[0].length); + bull = cap[2]; + isordered = bull.length > 1; + + this.tokens.push({ + type: 'list_start', + ordered: isordered, + start: isordered ? +bull : '' + }); + + // Get each top-level item. + cap = cap[0].match(this.rules.item); + + next = false; + l = cap.length; + i = 0; + + for (; i < l; i++) { + item = cap[i]; + + // Remove the list item's bullet + // so it is seen as the next token. + space = item.length; + item = item.replace(/^ *([*+-]|\d+\.) +/, ''); + + // Outdent whatever the + // list item contains. Hacky. + if (~item.indexOf('\n ')) { + space -= item.length; + item = !this.options.pedantic + ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '') + : item.replace(/^ {1,4}/gm, ''); + } + + // Determine whether the next list item belongs here. + // Backpedal if it does not belong in this list. + if (this.options.smartLists && i !== l - 1) { + b = block.bullet.exec(cap[i + 1])[0]; + if (bull !== b && !(bull.length > 1 && b.length > 1)) { + src = cap.slice(i + 1).join('\n') + src; + i = l - 1; + } + } + + // Determine whether item is loose or not. + // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ + // for discount behavior. + loose = next || /\n\n(?!\s*$)/.test(item); + if (i !== l - 1) { + next = item.charAt(item.length - 1) === '\n'; + if (!loose) loose = next; + } + + // Check for task list items + istask = /^\[[ xX]\] /.test(item); + ischecked = undefined; + if (istask) { + ischecked = item[1] !== ' '; + item = item.replace(/^\[[ xX]\] +/, ''); + } + + this.tokens.push({ + type: loose + ? 'loose_item_start' + : 'list_item_start', + task: istask, + checked: ischecked + }); + + // Recurse. + this.token(item, false); + + this.tokens.push({ + type: 'list_item_end' + }); + } + + this.tokens.push({ + type: 'list_end' + }); + + continue; + } + + // html + if (cap = this.rules.html.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: this.options.sanitize + ? 'paragraph' + : 'html', + pre: !this.options.sanitizer + && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'), + text: cap[0] + }); + continue; + } + + // def + if (top && (cap = this.rules.def.exec(src))) { + src = src.substring(cap[0].length); + if (cap[3]) cap[3] = cap[3].substring(1, cap[3].length - 1); + tag = cap[1].toLowerCase().replace(/\s+/g, ' '); + if (!this.tokens.links[tag]) { + this.tokens.links[tag] = { + href: cap[2], + title: cap[3] + }; + } + continue; + } + + // table (gfm) + if (top && (cap = this.rules.table.exec(src))) { + item = { + type: 'table', + header: splitCells(cap[1].replace(/^ *| *\| *$/g, '')), + align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), + cells: cap[3] ? cap[3].replace(/(?: *\| *)?\n$/, '').split('\n') : [] + }; + + if (item.header.length === item.align.length) { + src = src.substring(cap[0].length); + + for (i = 0; i < item.align.length; i++) { + if (/^ *-+: *$/.test(item.align[i])) { + item.align[i] = 'right'; + } else if (/^ *:-+: *$/.test(item.align[i])) { + item.align[i] = 'center'; + } else if (/^ *:-+ *$/.test(item.align[i])) { + item.align[i] = 'left'; + } else { + item.align[i] = null; + } + } + + for (i = 0; i < item.cells.length; i++) { + item.cells[i] = splitCells( + item.cells[i].replace(/^ *\| *| *\| *$/g, ''), + item.header.length); + } + + this.tokens.push(item); + + continue; + } + } + + // lheading + if (cap = this.rules.lheading.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'heading', + depth: cap[2] === '=' ? 1 : 2, + text: cap[1] + }); + continue; + } + + // top-level paragraph + if (top && (cap = this.rules.paragraph.exec(src))) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'paragraph', + text: cap[1].charAt(cap[1].length - 1) === '\n' + ? cap[1].slice(0, -1) + : cap[1] + }); + continue; + } + + // text + if (cap = this.rules.text.exec(src)) { + // Top-level should never reach here. + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'text', + text: cap[0] + }); + continue; + } + + if (src) { + throw new Error('Infinite loop on byte: ' + src.charCodeAt(0)); + } + } + + return this.tokens; + }; + + /** + * Inline-Level Grammar + */ + + var inline = { + escape: /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/, + autolink: /^<(scheme:[^\s\x00-\x1f<>]*|email)>/, + url: noop, + tag: '^comment' + + '|^' // self-closing tag + + '|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>' // open tag + + '|^<\\?[\\s\\S]*?\\?>' // processing instruction, e.g. + + '|^' // declaration, e.g. + + '|^', // CDATA section + link: /^!?\[(label)\]\(href(?:\s+(title))?\s*\)/, + reflink: /^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/, + nolink: /^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/, + strong: /^__([^\s][\s\S]*?[^\s])__(?!_)|^\*\*([^\s][\s\S]*?[^\s])\*\*(?!\*)|^__([^\s])__(?!_)|^\*\*([^\s])\*\*(?!\*)/, + em: /^_([^\s][\s\S]*?[^\s_])_(?!_)|^_([^\s_][\s\S]*?[^\s])_(?!_)|^\*([^\s][\s\S]*?[^\s*])\*(?!\*)|^\*([^\s*][\s\S]*?[^\s])\*(?!\*)|^_([^\s_])_(?!_)|^\*([^\s*])\*(?!\*)/, + code: /^(`+)\s*([\s\S]*?[^`]?)\s*\1(?!`)/, + br: /^ {2,}\n(?!\s*$)/, + del: noop, + text: /^[\s\S]+?(?=[\\?@\[\]\\^_`{|}~])/g; + + inline._scheme = /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/; + inline._email = /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/; + inline.autolink = edit(inline.autolink) + .replace('scheme', inline._scheme) + .replace('email', inline._email) + .getRegex(); + + inline._attribute = /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/; + + inline.tag = edit(inline.tag) + .replace('comment', block._comment) + .replace('attribute', inline._attribute) + .getRegex(); + + inline._label = /(?:\[[^\[\]]*\]|\\[\[\]]?|`[^`]*`|[^\[\]\\])*?/; + inline._href = /\s*(<(?:\\[<>]?|[^\s<>\\])*>|(?:\\[()]?|\([^\s\x00-\x1f()\\]*\)|[^\s\x00-\x1f()\\])*?)/; + inline._title = /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/; + + inline.link = edit(inline.link) + .replace('label', inline._label) + .replace('href', inline._href) + .replace('title', inline._title) + .getRegex(); + + inline.reflink = edit(inline.reflink) + .replace('label', inline._label) + .getRegex(); + + /** + * Normal Inline Grammar + */ + + inline.normal = merge({}, inline); + + /** + * Pedantic Inline Grammar + */ + + inline.pedantic = merge({}, inline.normal, { + strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, + em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/, + link: edit(/^!?\[(label)\]\((.*?)\)/) + .replace('label', inline._label) + .getRegex(), + reflink: edit(/^!?\[(label)\]\s*\[([^\]]*)\]/) + .replace('label', inline._label) + .getRegex() + }); + + /** + * GFM Inline Grammar + */ + + inline.gfm = merge({}, inline.normal, { + escape: edit(inline.escape).replace('])', '~|])').getRegex(), + url: edit(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/) + .replace('email', inline._email) + .getRegex(), + _backpedal: /(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/, + del: /^~~(?=\S)([\s\S]*?\S)~~/, + text: edit(inline.text) + .replace(']|', '~]|') + .replace('|', '|https?://|ftp://|www\\.|[a-zA-Z0-9.!#$%&\'*+/=?^_`{\\|}~-]+@|') + .getRegex() + }); + + /** + * GFM + Line Breaks Inline Grammar + */ + + inline.breaks = merge({}, inline.gfm, { + br: edit(inline.br).replace('{2,}', '*').getRegex(), + text: edit(inline.gfm.text).replace('{2,}', '*').getRegex() + }); + + /** + * Inline Lexer & Compiler + */ + + function InlineLexer(links, options) { + this.options = options || marked.defaults; + this.links = links; + this.rules = inline.normal; + this.renderer = this.options.renderer || new Renderer(); + this.renderer.options = this.options; + + if (!this.links) { + throw new Error('Tokens array requires a `links` property.'); + } + + if (this.options.pedantic) { + this.rules = inline.pedantic; + } else if (this.options.gfm) { + if (this.options.breaks) { + this.rules = inline.breaks; + } else { + this.rules = inline.gfm; + } + } + } + + /** + * Expose Inline Rules + */ + + InlineLexer.rules = inline; + + /** + * Static Lexing/Compiling Method + */ + + InlineLexer.output = function (src, links, options) { + var inline = new InlineLexer(links, options); + return inline.output(src); + }; + + /** + * Lexing/Compiling + */ + + InlineLexer.prototype.output = function (src) { + var out = '', + link, + text, + href, + title, + cap; + + while (src) { + // escape + if (cap = this.rules.escape.exec(src)) { + src = src.substring(cap[0].length); + out += cap[1]; + continue; + } + + // autolink + if (cap = this.rules.autolink.exec(src)) { + src = src.substring(cap[0].length); + if (cap[2] === '@') { + text = escape(this.mangle(cap[1])); + href = 'mailto:' + text; + } else { + text = escape(cap[1]); + href = text; + } + out += this.renderer.link(href, null, text); + continue; + } + + // url (gfm) + if (!this.inLink && (cap = this.rules.url.exec(src))) { + cap[0] = this.rules._backpedal.exec(cap[0])[0]; + src = src.substring(cap[0].length); + if (cap[2] === '@') { + text = escape(cap[0]); + href = 'mailto:' + text; + } else { + text = escape(cap[0]); + if (cap[1] === 'www.') { + href = 'http://' + text; + } else { + href = text; + } + } + out += this.renderer.link(href, null, text); + continue; + } + + // tag + if (cap = this.rules.tag.exec(src)) { + if (!this.inLink && /^/i.test(cap[0])) { + this.inLink = false; + } + src = src.substring(cap[0].length); + out += this.options.sanitize + ? this.options.sanitizer + ? this.options.sanitizer(cap[0]) + : escape(cap[0]) + : cap[0] + continue; + } + + // link + if (cap = this.rules.link.exec(src)) { + src = src.substring(cap[0].length); + this.inLink = true; + href = cap[2]; + if (this.options.pedantic) { + link = /^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(href); + + if (link) { + href = link[1]; + title = link[3]; + } else { + title = ''; + } + } else { + title = cap[3] ? cap[3].slice(1, -1) : ''; + } + href = href.trim().replace(/^<([\s\S]*)>$/, '$1'); + out += this.outputLink(cap, { + href: InlineLexer.escapes(href), + title: InlineLexer.escapes(title) + }); + this.inLink = false; + continue; + } + + // reflink, nolink + if ((cap = this.rules.reflink.exec(src)) + || (cap = this.rules.nolink.exec(src))) { + src = src.substring(cap[0].length); + link = (cap[2] || cap[1]).replace(/\s+/g, ' '); + link = this.links[link.toLowerCase()]; + if (!link || !link.href) { + out += cap[0].charAt(0); + src = cap[0].substring(1) + src; + continue; + } + this.inLink = true; + out += this.outputLink(cap, link); + this.inLink = false; + continue; + } + + // strong + if (cap = this.rules.strong.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.strong(this.output(cap[4] || cap[3] || cap[2] || cap[1])); + continue; + } + + // em + if (cap = this.rules.em.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.em(this.output(cap[6] || cap[5] || cap[4] || cap[3] || cap[2] || cap[1])); + continue; + } + + // code + if (cap = this.rules.code.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.codespan(escape(cap[2].trim(), true)); + continue; + } + + // br + if (cap = this.rules.br.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.br(); + continue; + } + + // del (gfm) + if (cap = this.rules.del.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.del(this.output(cap[1])); + continue; + } + + // text + if (cap = this.rules.text.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.text(escape(this.smartypants(cap[0]))); + continue; + } + + if (src) { + throw new Error('Infinite loop on byte: ' + src.charCodeAt(0)); + } + } + + return out; + }; + + InlineLexer.escapes = function (text) { + return text ? text.replace(InlineLexer.rules._escapes, '$1') : text; + } + + /** + * Compile Link + */ + + InlineLexer.prototype.outputLink = function (cap, link) { + var href = link.href, + title = link.title ? escape(link.title) : null; + + return cap[0].charAt(0) !== '!' + ? this.renderer.link(href, title, this.output(cap[1])) + : this.renderer.image(href, title, escape(cap[1])); + }; + + /** + * Smartypants Transformations + */ + + InlineLexer.prototype.smartypants = function (text) { + if (!this.options.smartypants) return text; + return text + // em-dashes + .replace(/---/g, '\u2014') + // en-dashes + .replace(/--/g, '\u2013') + // opening singles + .replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018') + // closing singles & apostrophes + .replace(/'/g, '\u2019') + // opening doubles + .replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c') + // closing doubles + .replace(/"/g, '\u201d') + // ellipses + .replace(/\.{3}/g, '\u2026'); + }; + + /** + * Mangle Links + */ + + InlineLexer.prototype.mangle = function (text) { + if (!this.options.mangle) return text; + var out = '', + l = text.length, + i = 0, + ch; + + for (; i < l; i++) { + ch = text.charCodeAt(i); + if (Math.random() > 0.5) { + ch = 'x' + ch.toString(16); + } + out += '&#' + ch + ';'; + } + + return out; + }; + + /** + * Renderer + */ + + function Renderer(options) { + this.options = options || marked.defaults; + } + + Renderer.prototype.code = function (code, lang, escaped) { + if (this.options.highlight) { + var out = this.options.highlight(code, lang); + if (out != null && out !== code) { + escaped = true; + code = out; + } + } + + if (!lang) { + return '
'
+                + (escaped ? code : escape(code, true))
+                + '
'; + } + + return '
'
+            + (escaped ? code : escape(code, true))
+            + '
\n'; + }; + + Renderer.prototype.blockquote = function (quote) { + return '
\n' + quote + '
\n'; + }; + + Renderer.prototype.html = function (html) { + return html; + }; + + Renderer.prototype.heading = function (text, level, raw) { + if (this.options.headerIds) { + return '' + + text + + '\n'; + } + // ignore IDs + return '' + text + '\n'; + }; + + Renderer.prototype.hr = function () { + return this.options.xhtml ? '
\n' : '
\n'; + }; + + Renderer.prototype.list = function (body, ordered, start) { + var type = ordered ? 'ol' : 'ul', + startatt = (ordered && start !== 1) ? (' start="' + start + '"') : ''; + return '<' + type + startatt + '>\n' + body + '\n'; + }; + + Renderer.prototype.listitem = function (text) { + return '
  • ' + text + '
  • \n'; + }; + + Renderer.prototype.checkbox = function (checked) { + return ' '; + } + + Renderer.prototype.paragraph = function (text) { + return '

    ' + text + '

    \n'; + }; + + Renderer.prototype.table = function (header, body) { + if (body) body = '' + body + ''; + + return '\n' + + '\n' + + header + + '\n' + + body + + '
    \n'; + }; + + Renderer.prototype.tablerow = function (content) { + return '\n' + content + '\n'; + }; + + Renderer.prototype.tablecell = function (content, flags) { + var type = flags.header ? 'th' : 'td'; + var tag = flags.align + ? '<' + type + ' align="' + flags.align + '">' + : '<' + type + '>'; + return tag + content + '\n'; + }; + + // span level renderer + Renderer.prototype.strong = function (text) { + return '' + text + ''; + }; + + Renderer.prototype.em = function (text) { + return '' + text + ''; + }; + + Renderer.prototype.codespan = function (text) { + return '' + text + ''; + }; + + Renderer.prototype.br = function () { + return this.options.xhtml ? '
    ' : '
    '; + }; + + Renderer.prototype.del = function (text) { + return '' + text + ''; + }; + + Renderer.prototype.link = function (href, title, text) { + if (this.options.sanitize) { + try { + var prot = decodeURIComponent(unescape(href)) + .replace(/[^\w:]/g, '') + .toLowerCase(); + } catch (e) { + return text; + } + if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) { + return text; + } + } + if (this.options.baseUrl && !originIndependentUrl.test(href)) { + href = resolveUrl(this.options.baseUrl, href); + } + try { + href = encodeURI(href).replace(/%25/g, '%'); + } catch (e) { + return text; + } + var out = '
    '; + return out; + }; + + Renderer.prototype.image = function (href, title, text) { + if (this.options.baseUrl && !originIndependentUrl.test(href)) { + href = resolveUrl(this.options.baseUrl, href); + } + var out = '' + text + '' : '>'; + return out; + }; + + Renderer.prototype.text = function (text) { + return text; + }; + + /** + * TextRenderer + * returns only the textual part of the token + */ + + function TextRenderer() { } + + // no need for block level renderers + + TextRenderer.prototype.strong = + TextRenderer.prototype.em = + TextRenderer.prototype.codespan = + TextRenderer.prototype.del = + TextRenderer.prototype.text = function (text) { + return text; + } + + TextRenderer.prototype.link = + TextRenderer.prototype.image = function (href, title, text) { + return '' + text; + } + + TextRenderer.prototype.br = function () { + return ''; + } + + /** + * Parsing & Compiling + */ + + function Parser(options) { + this.tokens = []; + this.token = null; + this.options = options || marked.defaults; + this.options.renderer = this.options.renderer || new Renderer(); + this.renderer = this.options.renderer; + this.renderer.options = this.options; + } + + /** + * Static Parse Method + */ + + Parser.parse = function (src, options) { + var parser = new Parser(options); + return parser.parse(src); + }; + + /** + * Parse Loop + */ + + Parser.prototype.parse = function (src) { + this.inline = new InlineLexer(src.links, this.options); + // use an InlineLexer with a TextRenderer to extract pure text + this.inlineText = new InlineLexer( + src.links, + merge({}, this.options, { renderer: new TextRenderer() }) + ); + this.tokens = src.reverse(); + + var out = ''; + while (this.next()) { + out += this.tok(); + } + + return out; + }; + + /** + * Next Token + */ + + Parser.prototype.next = function () { + return this.token = this.tokens.pop(); + }; + + /** + * Preview Next Token + */ + + Parser.prototype.peek = function () { + return this.tokens[this.tokens.length - 1] || 0; + }; + + /** + * Parse Text Tokens + */ + + Parser.prototype.parseText = function () { + var body = this.token.text; + + while (this.peek().type === 'text') { + body += '\n' + this.next().text; + } + + return this.inline.output(body); + }; + + /** + * Parse Current Token + */ + + Parser.prototype.tok = function () { + switch (this.token.type) { + case 'space': { + return ''; + } + case 'hr': { + return this.renderer.hr(); + } + case 'heading': { + return this.renderer.heading( + this.inline.output(this.token.text), + this.token.depth, + unescape(this.inlineText.output(this.token.text))); + } + case 'code': { + return this.renderer.code(this.token.text, + this.token.lang, + this.token.escaped); + } + case 'table': { + var header = '', + body = '', + i, + row, + cell, + j; + + // header + cell = ''; + for (i = 0; i < this.token.header.length; i++) { + cell += this.renderer.tablecell( + this.inline.output(this.token.header[i]), + { header: true, align: this.token.align[i] } + ); + } + header += this.renderer.tablerow(cell); + + for (i = 0; i < this.token.cells.length; i++) { + row = this.token.cells[i]; + + cell = ''; + for (j = 0; j < row.length; j++) { + cell += this.renderer.tablecell( + this.inline.output(row[j]), + { header: false, align: this.token.align[j] } + ); + } + + body += this.renderer.tablerow(cell); + } + return this.renderer.table(header, body); + } + case 'blockquote_start': { + body = ''; + + while (this.next().type !== 'blockquote_end') { + body += this.tok(); + } + + return this.renderer.blockquote(body); + } + case 'list_start': { + body = ''; + var ordered = this.token.ordered, + start = this.token.start; + + while (this.next().type !== 'list_end') { + body += this.tok(); + } + + return this.renderer.list(body, ordered, start); + } + case 'list_item_start': { + body = ''; + + if (this.token.task) { + body += this.renderer.checkbox(this.token.checked); + } + + while (this.next().type !== 'list_item_end') { + body += this.token.type === 'text' + ? this.parseText() + : this.tok(); + } + + return this.renderer.listitem(body); + } + case 'loose_item_start': { + body = ''; + + while (this.next().type !== 'list_item_end') { + body += this.tok(); + } + + return this.renderer.listitem(body); + } + case 'html': { + // TODO parse inline content if parameter markdown=1 + return this.renderer.html(this.token.text); + } + case 'paragraph': { + return this.renderer.paragraph(this.inline.output(this.token.text)); + } + case 'text': { + return this.renderer.paragraph(this.parseText()); + } + } + }; + + /** + * Helpers + */ + + function escape(html, encode) { + return html + .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + function unescape(html) { + // explicitly match decimal, hex, and named HTML entities + return html.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig, function (_, n) { + n = n.toLowerCase(); + if (n === 'colon') return ':'; + if (n.charAt(0) === '#') { + return n.charAt(1) === 'x' + ? String.fromCharCode(parseInt(n.substring(2), 16)) + : String.fromCharCode(+n.substring(1)); + } + return ''; + }); + } + + function edit(regex, opt) { + regex = regex.source || regex; + opt = opt || ''; + return { + replace: function (name, val) { + val = val.source || val; + val = val.replace(/(^|[^\[])\^/g, '$1'); + regex = regex.replace(name, val); + return this; + }, + getRegex: function () { + return new RegExp(regex, opt); + } + }; + } + + function resolveUrl(base, href) { + if (!baseUrls[' ' + base]) { + // we can ignore everything in base after the last slash of its path component, + // but we might need to add _that_ + // https://tools.ietf.org/html/rfc3986#section-3 + if (/^[^:]+:\/*[^/]*$/.test(base)) { + baseUrls[' ' + base] = base + '/'; + } else { + baseUrls[' ' + base] = base.replace(/[^/]*$/, ''); + } + } + base = baseUrls[' ' + base]; + + if (href.slice(0, 2) === '//') { + return base.replace(/:[\s\S]*/, ':') + href; + } else if (href.charAt(0) === '/') { + return base.replace(/(:\/*[^/]*)[\s\S]*/, '$1') + href; + } else { + return base + href; + } + } + var baseUrls = {}; + var originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i; + + function noop() { } + noop.exec = noop; + + function merge(obj) { + var i = 1, + target, + key; + + for (; i < arguments.length; i++) { + target = arguments[i]; + for (key in target) { + if (Object.prototype.hasOwnProperty.call(target, key)) { + obj[key] = target[key]; + } + } + } + + return obj; + } + + function splitCells(tableRow, count) { + var cells = tableRow.replace(/([^\\])\|/g, '$1 |').split(/ +\| */), + i = 0; + + if (cells.length > count) { + cells.splice(count); + } else { + while (cells.length < count) cells.push(''); + } + + for (; i < cells.length; i++) { + cells[i] = cells[i].replace(/\\\|/g, '|'); + } + return cells; + } + + /** + * Marked + */ + + function marked(src, opt, callback) { + // throw error in case of non string input + if (typeof src === 'undefined' || src === null) { + throw new Error('marked(): input parameter is undefined or null'); + } + if (typeof src !== 'string') { + throw new Error('marked(): input parameter is of type ' + + Object.prototype.toString.call(src) + ', string expected'); + } + + if (callback || typeof opt === 'function') { + if (!callback) { + callback = opt; + opt = null; + } + + opt = merge({}, marked.defaults, opt || {}); + + var highlight = opt.highlight, + tokens, + pending, + i = 0; + + try { + tokens = Lexer.lex(src, opt) + } catch (e) { + return callback(e); + } + + pending = tokens.length; + + var done = function (err) { + if (err) { + opt.highlight = highlight; + return callback(err); + } + + var out; + + try { + out = Parser.parse(tokens, opt); + } catch (e) { + err = e; + } + + opt.highlight = highlight; + + return err + ? callback(err) + : callback(null, out); + }; + + if (!highlight || highlight.length < 3) { + return done(); + } + + delete opt.highlight; + + if (!pending) return done(); + + for (; i < tokens.length; i++) { + (function (token) { + if (token.type !== 'code') { + return --pending || done(); + } + return highlight(token.text, token.lang, function (err, code) { + if (err) return done(err); + if (code == null || code === token.text) { + return --pending || done(); + } + token.text = code; + token.escaped = true; + --pending || done(); + }); + })(tokens[i]); + } + + return; + } + try { + if (opt) opt = merge({}, marked.defaults, opt); + return Parser.parse(Lexer.lex(src, opt), opt); + } catch (e) { + e.message += '\nPlease report this to https://github.com/markedjs/marked.'; + if ((opt || marked.defaults).silent) { + return '

    An error occurred:

    '
    +                    + escape(e.message + '', true)
    +                    + '
    '; + } + throw e; + } + } + + /** + * Options + */ + + marked.options = + marked.setOptions = function (opt) { + merge(marked.defaults, opt); + return marked; + }; + + marked.getDefaults = function () { + return { + baseUrl: null, + breaks: false, + gfm: true, + headerIds: true, + headerPrefix: '', + highlight: null, + langPrefix: 'language-', + mangle: true, + pedantic: false, + renderer: new Renderer(), + sanitize: false, + sanitizer: null, + silent: false, + smartLists: false, + smartypants: false, + tables: true, + xhtml: false + }; + } + + marked.defaults = marked.getDefaults(); + + /** + * Expose + */ + + marked.Parser = Parser; + marked.parser = Parser.parse; + + marked.Renderer = Renderer; + marked.TextRenderer = TextRenderer; + + marked.Lexer = Lexer; + marked.lexer = Lexer.lex; + + marked.InlineLexer = InlineLexer; + marked.inlineLexer = InlineLexer.output; + + marked.parse = marked; + + if (typeof module !== 'undefined' && typeof exports === 'object') { + module.exports = marked; + } else if (typeof define === 'function' && define.amd) { + define(function () { return marked; }); + } else { + root.marked = marked; + } +})(this || (typeof window !== 'undefined' ? window : global)); \ No newline at end of file diff --git a/tests/data/announcement_checker_bulletin/test_bulletin.json b/tests/data/announcement_checker_bulletin/test_bulletin.json index a6dbf01a4..4c9ef56b7 100644 --- a/tests/data/announcement_checker_bulletin/test_bulletin.json +++ b/tests/data/announcement_checker_bulletin/test_bulletin.json @@ -1 +1 @@ -[{"title": "title1", "date": "10/12/2023", "content": "New announcement"}, {"title": "title2", "date": "10/07/2023", "content": "Old Announcment"}] \ No newline at end of file +[{"title": "title1", "date": "10/12/2023", "content": "# Bulletin\n\n\n\nSLEAP 1.3.2 adds some nice usability features thanks to both the community ideas and new contributors! See [1.3.0](https://github.com/talmolab/sleap/releases/tag/v1.3.0) and [1.3.1](https://github.com/talmolab/sleap/releases/tag/v1.3.1) for previous notable changes. As a reminder:\n\n> The 1.3.1 dependency update requires [Mamba](https://mamba.readthedocs.io/en/latest/index.html) for faster dependency resolution. If you already have anaconda installed, then you _can_ set the solver to libmamba in the base environment:\n>```\n>conda update -n base conda\n>conda install -n base conda-libmamba-solver\n>conda config --set solver libmamba\n>```\n>Any subsequent `mamba` commands in the docs will need to be replaced with `conda` if you choose to use your existing Anaconda installation. \n>\n>Otherwise, follow the [recommended installation instruction for Mamba](https://mamba.readthedocs.io/en/latest/installation.html).\n\n**`mamba` (Windows/Linux/GPU)**:\n```\nmamba create -y -n sleap -c conda-forge -c nvidia -c sleap -c anaconda sleap=1.3.2\n```\n\n**`mamba` (Mac)**:\n```\nmamba create -y -n sleap -c conda-forge -c anaconda -c sleap sleap=1.3.2\n```\n\n**`pip` (any OS except Apple Silicon)**:\n```\npip install sleap[pypi]==1.3.2\n```\n\n* Limit max tracks via track-local queues by @shrivaths16 and @talmo in https://github.com/talmolab/sleap/pull/1447\n* Add option to remove videos in batch by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1382 and https://github.com/talmolab/sleap/pull/1406\n* Add shortcut to export analysis for current video by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1414 and https://github.com/talmolab/sleap/pull/1444\n* Add video path and frame indices to metrics by @roomrys in https://github.com/talmolab/sleap/pull/1396\n* Add a button for copying model config to clipboard by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1433\n* Add Option to Export CSV by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1438\n\n\n* Add option to remove videos in batch by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1382 and https://github.com/talmolab/sleap/pull/1406\n* Add `Track` when add `Instance` by @roomrys in https://github.com/talmolab/sleap/pull/1408\n* Add `Video` to cache when adding `Track` by @roomrys in https://github.com/talmolab/sleap/pull/1407\n* Add shortcut to export analysis for current video by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1414 and https://github.com/talmolab/sleap/pull/1444\n* Add video path and frame indices to metrics by @roomrys in https://github.com/talmolab/sleap/pull/1396\n* Improve error message for detecting video backend by @roomrys in https://github.com/talmolab/sleap/pull/1441\n* Add a button for copying model config to clipboard by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1433\n* Add Option to Export CSV by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1438\n* Limit max tracks via track-local queues by @shrivaths16 and @talmo in https://github.com/talmolab/sleap/pull/1447\n\n* Minor fix in computation of OKS by @shrivaths16 in https://github.com/talmolab/sleap/pull/1383 and https://github.com/talmolab/sleap/pull/1399\n* Fix `Filedialog` to work across (mac)OS by @roomrys in https://github.com/talmolab/sleap/pull/1393\n* Fix panning bounding box by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1398\n* Fix skeleton templates by @roomrys in https://github.com/talmolab/sleap/pull/1404\n* Fix labels export for json by @roomrys in https://github.com/talmolab/sleap/pull/1410\n* Correct GUI state emulation by @roomrys in https://github.com/talmolab/sleap/pull/1422\n* Update status message on status bar by @shrivaths16 in https://github.com/talmolab/sleap/pull/1411\n* Fix error thrown when last video is deleted by @shrivaths16 in https://github.com/talmolab/sleap/pull/1421\n* Add model folder to the unzip path by @roomrys in https://github.com/talmolab/sleap/pull/1445\n* Fix drag and drop by @talmo in https://github.com/talmolab/sleap/pull/1449\n\n* Pin micromamba version by @roomrys in https://github.com/talmolab/sleap/pull/1376\n* Add pip extras by @roomrys in https://github.com/talmolab/sleap/pull/1481\n\n* @shrivaths16 made their first contribution in https://github.com/talmolab/sleap/pull/1383\n* @gitttt-1234 made their first contribution in https://github.com/talmolab/sleap/pull/1382\n* @KevinZ0217 made their first contribution in https://github.com/talmolab/sleap/pull/1414\n\n**Full Changelog**: https://github.com/talmolab/sleap/compare/v1.3.1...v1.3.2\n"}, {"title": "title2", "date": "10/07/2023", "content": "Old Announcment"}] \ No newline at end of file From ca253472cb28dd49eb8c99834bb8eef5859cbfcb Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Tue, 14 Nov 2023 09:13:50 -0800 Subject: [PATCH 04/29] Add Qthread to multiple windows --- sleap/gui/app.py | 23 +++++----------- sleap/gui/dialogs/bulletin.py | 51 ++++++++++++++++++++--------------- 2 files changed, 35 insertions(+), 39 deletions(-) diff --git a/sleap/gui/app.py b/sleap/gui/app.py index 41098abb6..44d3f3e1c 100644 --- a/sleap/gui/app.py +++ b/sleap/gui/app.py @@ -69,7 +69,7 @@ from sleap.gui.dialogs.formbuilder import FormBuilderModalDialog from sleap.gui.dialogs.metrics import MetricsTableDialog from sleap.gui.dialogs.shortcuts import ShortcutDialog -from sleap.gui.dialogs.bulletin import BulletinDialog +from sleap.gui.dialogs.bulletin import BulletinDialog, BulletinWorker from sleap.gui.overlays.instance import InstanceOverlay from sleap.gui.overlays.tracks import TrackListOverlay, TrackTrailOverlay from sleap.gui.shortcuts import Shortcuts @@ -213,7 +213,6 @@ def __init__( # Display announcement bulletin popup if self.new_announcement_available: - self.display_bulletin = BulletinDialog() self.bulletin_dialog() def bulletin_dialog(self): @@ -224,22 +223,12 @@ def bulletin_dialog(self): title, date, content = announcement bulletin_markdown = "\n".join(content.split("\n")) - channel = QWebChannel() - channel.registerObject("content", self.display_bulletin) + popup_dialog = BulletinDialog(self) + popup_dialog.show() - self.display_bulletin.set_text(bulletin_markdown) - - view = QWebEngineView() - view.page().setWebChannel(channel) - base_path = os.path.dirname(os.path.abspath(os.path.join(__file__))) - filepath = os.path.join( - base_path, "..", "gui", "dialogs", "bulletin", "markdown.html" - ) - url = QUrl.fromLocalFile(filepath) - view.load(url) - view.resize(720, 540) - view.show() - QApplication.exec_() + popup_worker = BulletinWorker(bulletin_markdown) + popup_worker.text_updated.connect(popup_dialog.updateText) + popup_worker.start() def setWindowTitle(self, value): """Sets window title (if value is not None).""" diff --git a/sleap/gui/dialogs/bulletin.py b/sleap/gui/dialogs/bulletin.py index cbf19ebdb..795ccf2f7 100644 --- a/sleap/gui/dialogs/bulletin.py +++ b/sleap/gui/dialogs/bulletin.py @@ -2,31 +2,38 @@ GUI for displaying the new announcement. """ -import os -import sleap -import sleap.gui.web -from qtpy.QtWidgets import QApplication -from qtpy.QtCore import Property, Signal, QObject, QUrl -from qtpy.QtWebChannel import QWebChannel -from qtpy.QtWebEngineWidgets import QWebEngineView -from sleap.gui.commands import CommandContext -from sleap.io.dataset import Labels +from qtpy.QtWidgets import ( + QApplication, + QDialog, + QVBoxLayout, + QMainWindow, + QHBoxLayout, + QWidget, + QLabel, +) +from qtpy.QtCore import QObject, Signal, Slot, QThread, Property -class BulletinDialog(QObject): - textChanged = Signal(str) +class BulletinWorker(QThread): + text_updated = Signal(str) - def __init__(self, parent=None): - super().__init__(parent) - self.m_text = "" + def __init__(self, content, parent=None): + super(BulletinWorker, self).__init__(parent) + self.content = content + + def run(self): + self.text_updated.emit(self.content) - def get_text(self): - return self.m_text - def set_text(self, text): - if self.m_text == text: - return - self.m_text = text - self.textChanged.emit(self.m_text) +class BulletinDialog(QDialog): + def __init__(self, parent=None): + super(BulletinDialog, self).__init__(parent) + + self.label = QLabel() + layout = QVBoxLayout() + layout.addWidget(self.label) + self.setLayout(layout) - text = Property(str, fget=get_text, fset=set_text, notify=textChanged) + @Slot(str) + def updateText(self, text): + self.label.setText(text) From 27f0d28ae601be00dce47ca3f8f78edc1a41ea8b Mon Sep 17 00:00:00 2001 From: roomrys Date: Tue, 14 Nov 2023 11:06:13 -0800 Subject: [PATCH 05/29] Set dialog to top, maintain reference to thread, remove unused imports --- sleap/gui/app.py | 19 +++++++++++++++---- sleap/gui/dialogs/bulletin.py | 6 +----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/sleap/gui/app.py b/sleap/gui/app.py index 44d3f3e1c..0df73e4cb 100644 --- a/sleap/gui/app.py +++ b/sleap/gui/app.py @@ -56,12 +56,9 @@ from datetime import date from qtpy import QtCore, QtGui -from qtpy.QtCore import QEvent, Qt, QUrl +from qtpy.QtCore import QEvent, Qt from qtpy.QtWidgets import QApplication, QMainWindow, QMessageBox -from qtpy.QtWebChannel import QWebChannel -from qtpy.QtWebEngineWidgets import QWebEngineView - import sleap from sleap.gui.color import ColorManager from sleap.gui.commands import CommandContext, UpdateTopic @@ -223,13 +220,27 @@ def bulletin_dialog(self): title, date, content = announcement bulletin_markdown = "\n".join(content.split("\n")) + # Create the pop-up dialog popup_dialog = BulletinDialog(self) + + # Set the dialog as a top-level window with the Qt.WindowStaysOnTopHint flag + popup_dialog.setWindowFlags( + popup_dialog.windowFlags() | Qt.WindowStaysOnTopHint + ) + + # Show the dialog popup_dialog.show() + # Create a worker thread to update the text in the dialog popup_worker = BulletinWorker(bulletin_markdown) popup_worker.text_updated.connect(popup_dialog.updateText) popup_worker.start() + # Save the dialog and worker so we can close them later + # Otherwise get "QThread: Destroyed while thread is still running" + self._child_windows["bulletin_dialog"] = popup_dialog # Not really needed + self._child_windows["bulletin_worker"] = popup_worker # Needed! + def setWindowTitle(self, value): """Sets window title (if value is not None).""" if value is not None: diff --git a/sleap/gui/dialogs/bulletin.py b/sleap/gui/dialogs/bulletin.py index 795ccf2f7..24ff1f733 100644 --- a/sleap/gui/dialogs/bulletin.py +++ b/sleap/gui/dialogs/bulletin.py @@ -3,15 +3,11 @@ """ from qtpy.QtWidgets import ( - QApplication, QDialog, QVBoxLayout, - QMainWindow, - QHBoxLayout, - QWidget, QLabel, ) -from qtpy.QtCore import QObject, Signal, Slot, QThread, Property +from qtpy.QtCore import Signal, Slot, QThread class BulletinWorker(QThread): From 168056e634a153111b9095d75ba0e472c6288145 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Mon, 27 Nov 2023 16:23:37 -0800 Subject: [PATCH 06/29] bulletin pop up on top, change to pathlib --- sleap/gui/app.py | 24 +++---------- sleap/gui/dialogs/bulletin.py | 64 +++++++++++++++++++++++------------ 2 files changed, 48 insertions(+), 40 deletions(-) diff --git a/sleap/gui/app.py b/sleap/gui/app.py index 0df73e4cb..365bc97f6 100644 --- a/sleap/gui/app.py +++ b/sleap/gui/app.py @@ -66,7 +66,7 @@ from sleap.gui.dialogs.formbuilder import FormBuilderModalDialog from sleap.gui.dialogs.metrics import MetricsTableDialog from sleap.gui.dialogs.shortcuts import ShortcutDialog -from sleap.gui.dialogs.bulletin import BulletinDialog, BulletinWorker +from sleap.gui.dialogs.bulletin import BulletinWorker from sleap.gui.overlays.instance import InstanceOverlay from sleap.gui.overlays.tracks import TrackListOverlay, TrackTrailOverlay from sleap.gui.shortcuts import Shortcuts @@ -220,25 +220,11 @@ def bulletin_dialog(self): title, date, content = announcement bulletin_markdown = "\n".join(content.split("\n")) - # Create the pop-up dialog - popup_dialog = BulletinDialog(self) + # initialize the bulletin popup worker + popup_worker = BulletinWorker(bulletin_markdown, self) + popup_worker.show_bulletin() - # Set the dialog as a top-level window with the Qt.WindowStaysOnTopHint flag - popup_dialog.setWindowFlags( - popup_dialog.windowFlags() | Qt.WindowStaysOnTopHint - ) - - # Show the dialog - popup_dialog.show() - - # Create a worker thread to update the text in the dialog - popup_worker = BulletinWorker(bulletin_markdown) - popup_worker.text_updated.connect(popup_dialog.updateText) - popup_worker.start() - - # Save the dialog and worker so we can close them later - # Otherwise get "QThread: Destroyed while thread is still running" - self._child_windows["bulletin_dialog"] = popup_dialog # Not really needed + # Save the bulletin worker so we can close them later self._child_windows["bulletin_worker"] = popup_worker # Needed! def setWindowTitle(self, value): diff --git a/sleap/gui/dialogs/bulletin.py b/sleap/gui/dialogs/bulletin.py index 24ff1f733..c9b7c6237 100644 --- a/sleap/gui/dialogs/bulletin.py +++ b/sleap/gui/dialogs/bulletin.py @@ -2,34 +2,56 @@ GUI for displaying the new announcement. """ -from qtpy.QtWidgets import ( - QDialog, - QVBoxLayout, - QLabel, -) -from qtpy.QtCore import Signal, Slot, QThread +from qtpy.QtCore import Signal, Qt +from qtpy.QtWebEngineWidgets import QWebEngineView +from qtpy.QtCore import Property, Signal, QObject, QUrl +from qtpy.QtWebChannel import QWebChannel +from qtpy import QtWidgets +from pathlib import Path -class BulletinWorker(QThread): - text_updated = Signal(str) - +class BulletinWorker(QtWidgets.QMainWindow): def __init__(self, content, parent=None): super(BulletinWorker, self).__init__(parent) - self.content = content + self._content = content + # Set the window to stay on top + self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint) + + def show_bulletin(self): + + self.document = Document() + + # Set the webchannel + self.channel = QWebChannel() + self.channel.registerObject("content", self.document) - def run(self): - self.text_updated.emit(self.content) + self.document.set_text(self._content) + self.view = QWebEngineView() + self.view.page().setWebChannel(self.channel) + filename = str(Path(__file__).resolve().parent / "bulletin/markdown.html") + url = QUrl.fromLocalFile(filename) + self.view.load(url) + + # Set the central window with view + self.setCentralWidget(self.view) + self.show() + + +class Document(QObject): + textChanged = Signal(str) -class BulletinDialog(QDialog): def __init__(self, parent=None): - super(BulletinDialog, self).__init__(parent) + super().__init__(parent) + self.m_text = "" + + def get_text(self): + return self.m_text - self.label = QLabel() - layout = QVBoxLayout() - layout.addWidget(self.label) - self.setLayout(layout) + def set_text(self, text): + if self.m_text == text: + return + self.m_text = text + self.textChanged.emit(self.m_text) - @Slot(str) - def updateText(self, text): - self.label.setText(text) + text = Property(str, fget=get_text, fset=set_text, notify=textChanged) From 561a1098d7819f73dd1e602ffe1832e67a2a0fa5 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Tue, 5 Dec 2023 00:55:30 -0800 Subject: [PATCH 07/29] Fix bulletin dialog display conditions --- docs/bulletin.json | 2 +- sleap/gui/app.py | 37 +++++++------------------ sleap/gui/dialogs/bulletin/markdown.css | 23 ++++++++------- tests/gui/test_app.py | 10 +++++++ 4 files changed, 32 insertions(+), 40 deletions(-) diff --git a/docs/bulletin.json b/docs/bulletin.json index e8f90db4a..b42063fb9 100644 --- a/docs/bulletin.json +++ b/docs/bulletin.json @@ -1,7 +1,7 @@ [ { "title": "SLEAP v1.3.2", - "date": "10/30/2023", + "date": "12/8/2023", "content": "\n\n\nSLEAP 1.3.2 adds some nice usability features thanks to both the community ideas and new contributors! See [1.3.0](https://github.com/talmolab/sleap/releases/tag/v1.3.0) and [1.3.1](https://github.com/talmolab/sleap/releases/tag/v1.3.1) for previous notable changes. As a reminder:\n\n> The 1.3.1 dependency update requires [Mamba](https://mamba.readthedocs.io/en/latest/index.html) for faster dependency resolution. If you already have anaconda installed, then you _can_ set the solver to libmamba in the base environment:\n>```\n>conda update -n base conda\n>conda install -n base conda-libmamba-solver\n>conda config --set solver libmamba\n>```\n>Any subsequent `mamba` commands in the docs will need to be replaced with `conda` if you choose to use your existing Anaconda installation. \n>\n>Otherwise, follow the [recommended installation instruction for Mamba](https://mamba.readthedocs.io/en/latest/installation.html).\n\n### Quick install\n**`mamba` (Windows/Linux/GPU)**:\n```\nmamba create -y -n sleap -c conda-forge -c nvidia -c sleap -c anaconda sleap=1.3.2\n```\n\n**`mamba` (Mac)**:\n```\nmamba create -y -n sleap -c conda-forge -c anaconda -c sleap sleap=1.3.2\n```\n\n**`pip` (any OS except Apple Silicon)**:\n```\npip install sleap[pypi]==1.3.2\n```\n\n### Highlights\n* Limit max tracks via track-local queues by @shrivaths16 and @talmo in https://github.com/talmolab/sleap/pull/1447\n* Add option to remove videos in batch by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1382 and https://github.com/talmolab/sleap/pull/1406\n* Add shortcut to export analysis for current video by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1414 and https://github.com/talmolab/sleap/pull/1444\n* Add video path and frame indices to metrics by @roomrys in https://github.com/talmolab/sleap/pull/1396\n* Add a button for copying model config to clipboard by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1433\n* Add Option to Export CSV by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1438\n\n### Full Changelog\n\n#### Enhancements\n* Add option to remove videos in batch by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1382 and https://github.com/talmolab/sleap/pull/1406\n* Add `Track` when add `Instance` by @roomrys in https://github.com/talmolab/sleap/pull/1408\n* Add `Video` to cache when adding `Track` by @roomrys in https://github.com/talmolab/sleap/pull/1407\n* Add shortcut to export analysis for current video by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1414 and https://github.com/talmolab/sleap/pull/1444\n* Add video path and frame indices to metrics by @roomrys in https://github.com/talmolab/sleap/pull/1396\n* Improve error message for detecting video backend by @roomrys in https://github.com/talmolab/sleap/pull/1441\n* Add a button for copying model config to clipboard by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1433\n* Add Option to Export CSV by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1438\n* Limit max tracks via track-local queues by @shrivaths16 and @talmo in https://github.com/talmolab/sleap/pull/1447\n\n#### Fixes\n* Minor fix in computation of OKS by @shrivaths16 in https://github.com/talmolab/sleap/pull/1383 and https://github.com/talmolab/sleap/pull/1399\n* Fix `Filedialog` to work across (mac)OS by @roomrys in https://github.com/talmolab/sleap/pull/1393\n* Fix panning bounding box by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1398\n* Fix skeleton templates by @roomrys in https://github.com/talmolab/sleap/pull/1404\n* Fix labels export for json by @roomrys in https://github.com/talmolab/sleap/pull/1410\n* Correct GUI state emulation by @roomrys in https://github.com/talmolab/sleap/pull/1422\n* Update status message on status bar by @shrivaths16 in https://github.com/talmolab/sleap/pull/1411\n* Fix error thrown when last video is deleted by @shrivaths16 in https://github.com/talmolab/sleap/pull/1421\n* Add model folder to the unzip path by @roomrys in https://github.com/talmolab/sleap/pull/1445\n* Fix drag and drop by @talmo in https://github.com/talmolab/sleap/pull/1449\n\n#### Dependencies\n* Pin micromamba version by @roomrys in https://github.com/talmolab/sleap/pull/1376\n* Add pip extras by @roomrys in https://github.com/talmolab/sleap/pull/1481\n\n#### New Contributors\n* @shrivaths16 made their first contribution in https://github.com/talmolab/sleap/pull/1383\n* @gitttt-1234 made their first contribution in https://github.com/talmolab/sleap/pull/1382\n* @KevinZ0217 made their first contribution in https://github.com/talmolab/sleap/pull/1414\n\n**Full Changelog**: https://github.com/talmolab/sleap/compare/v1.3.1...v1.3.2\n" } ] \ No newline at end of file diff --git a/sleap/gui/app.py b/sleap/gui/app.py index 365bc97f6..6cbb820ee 100644 --- a/sleap/gui/app.py +++ b/sleap/gui/app.py @@ -161,20 +161,6 @@ def __init__( self.state["skeleton_preview_image"] = None self.state["skeleton_description"] = "No skeleton loaded yet" - if prefs["announcement last seen date"]: - self.state["announcement last seen date"] = prefs[ - "announcement last seen date" - ] - else: - self.state["announcement last seen date"] = date.today().strftime( - "%m/%d/%Y" - ) - - if prefs["announcement"]: - self.state["announcement"] = prefs["announcement"] - else: - self.state["announcement"] = "No data to display" - if no_usage_data: self.state["share usage data"] = False self.state["clipboard_track"] = None @@ -186,8 +172,10 @@ def __init__( self.release_checker = ReleaseChecker() + self.state["announcement last seen date"] = prefs["announcement last seen date"] + self.state["announcement"] = prefs["announcement"] + self.announcement_checker = AnnouncementChecker(state=self.state) - self.new_announcement_available = self.announcement_checker.new_announcement if self.state["share usage data"]: ping_analytics() @@ -209,23 +197,18 @@ def __init__( self.state["project_loaded"] = False # Display announcement bulletin popup - if self.new_announcement_available: + if self.announcement_checker.new_announcement_available(): self.bulletin_dialog() + self.announcement_checker.update_announcement() def bulletin_dialog(self): """Displays bulletin dialog is new announcement is available.""" - announcement = self.announcement_checker.get_latest_announcement() - - if announcement: - title, date, content = announcement - bulletin_markdown = "\n".join(content.split("\n")) - - # initialize the bulletin popup worker - popup_worker = BulletinWorker(bulletin_markdown, self) - popup_worker.show_bulletin() + # Initialize the bulletin popup worker + popup_worker = BulletinWorker(self.state["announcement"], self) + popup_worker.show_bulletin() - # Save the bulletin worker so we can close them later - self._child_windows["bulletin_worker"] = popup_worker # Needed! + # Save the bulletin worker so we can close them later + self._child_windows["bulletin_worker"] = popup_worker # Needed! def setWindowTitle(self, value): """Sets window title (if value is not None).""" diff --git a/sleap/gui/dialogs/bulletin/markdown.css b/sleap/gui/dialogs/bulletin/markdown.css index 66a5105c5..1cc32b362 100644 --- a/sleap/gui/dialogs/bulletin/markdown.css +++ b/sleap/gui/dialogs/bulletin/markdown.css @@ -1,7 +1,7 @@ body { margin: 0 auto; - font-family: Georgia, Palatino, serif; - color: #444444; + font-family: Helvetica, Arial, Sans-Serif; + color: #000000; line-height: 1; max-width: 960px; padding: 30px; @@ -11,8 +11,7 @@ h1, h2, h3, h4 { - color: #111111; - font-weight: 400; + font-weight: 600; } h1, @@ -26,30 +25,30 @@ p { } h1 { - font-size: 48px; + font-size: 50px; } h2 { - font-size: 36px; + font-size: 38px; /* The bottom margin is small. It's designed to be used with gray meta text * below a post title. */ margin: 24px 0 6px; } h3 { - font-size: 24px; + font-size: 26px; } h4 { - font-size: 21px; + font-size: 22px; } h5 { - font-size: 18px; + font-size: 20px; } a { - color: #0099ff; + color: #0099ffd5; margin: 0; padding: 0; vertical-align: baseline; @@ -57,11 +56,11 @@ a { a:hover { text-decoration: none; - color: #ff6600; + color: #ff6600b7; } a:visited { - color: purple; + color: rgba(128, 0, 128, 0.815); } ul, diff --git a/tests/gui/test_app.py b/tests/gui/test_app.py index bacda4ae3..7cf1b4da6 100644 --- a/tests/gui/test_app.py +++ b/tests/gui/test_app.py @@ -3,6 +3,7 @@ from sleap.gui.app import MainWindow from sleap.gui.commands import * +from sleap.gui.dialogs.bulletin import BulletinWorker def test_app_workflow( @@ -10,6 +11,15 @@ def test_app_workflow( ): app = MainWindow(no_usage_data=True) + # Check if the bulletin is shown or not + bulletin_dialog = app._child_windows.get("bulletin_worker", False) + if app.announcement_checker.new_announcement_available(): + print("bulletin available") + assert isinstance(bulletin_dialog, BulletinWorker) + else: + print("no bulletin") + assert bulletin_dialog == False + # Add nodes app.commands.newNode() app.commands.newNode() From f7c510da9c877078910faca48ba09d0946057038 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Tue, 5 Dec 2023 17:10:01 -0800 Subject: [PATCH 08/29] small test modification --- sleap/gui/app.py | 5 ++++- tests/gui/test_app.py | 4 +--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/sleap/gui/app.py b/sleap/gui/app.py index 6cbb820ee..f463a04e3 100644 --- a/sleap/gui/app.py +++ b/sleap/gui/app.py @@ -196,8 +196,11 @@ def __init__( else: self.state["project_loaded"] = False + self.new_announcement_available = ( + self.announcement_checker.new_announcement_available() + ) # Display announcement bulletin popup - if self.announcement_checker.new_announcement_available(): + if self.new_announcement_available: self.bulletin_dialog() self.announcement_checker.update_announcement() diff --git a/tests/gui/test_app.py b/tests/gui/test_app.py index 7cf1b4da6..ea48b813f 100644 --- a/tests/gui/test_app.py +++ b/tests/gui/test_app.py @@ -13,11 +13,9 @@ def test_app_workflow( # Check if the bulletin is shown or not bulletin_dialog = app._child_windows.get("bulletin_worker", False) - if app.announcement_checker.new_announcement_available(): - print("bulletin available") + if app.new_announcement_available: assert isinstance(bulletin_dialog, BulletinWorker) else: - print("no bulletin") assert bulletin_dialog == False # Add nodes From 58ffca8ba6585faf35a9246ba31276ab3751dea4 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Mon, 18 Dec 2023 13:26:56 -0800 Subject: [PATCH 09/29] pyside6 >=6.0 --- environment_mac.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment_mac.yml b/environment_mac.yml index 85ef7d3b9..d63baaa1d 100644 --- a/environment_mac.yml +++ b/environment_mac.yml @@ -23,7 +23,7 @@ dependencies: - conda-forge::pillow - conda-forge::psutil - conda-forge::pykalman - - conda-forge::pyside2 >=5.12 # To ensure application works correctly with QtPy. + - conda-forge::pyside6 >=6.0 # To ensure application works correctly with QtPy. - conda-forge::python ~=3.9 - conda-forge::python-rapidjson - conda-forge::pyyaml From 02aa24916a42ba9ebbde431eef8766b1e79a1be2 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Mon, 18 Dec 2023 15:34:56 -0800 Subject: [PATCH 10/29] setting environment variable QT_API to pyside2 --- environment_mac.yml | 2 +- sleap/gui/dialogs/bulletin.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/environment_mac.yml b/environment_mac.yml index d63baaa1d..0e4f5d2df 100644 --- a/environment_mac.yml +++ b/environment_mac.yml @@ -23,7 +23,7 @@ dependencies: - conda-forge::pillow - conda-forge::psutil - conda-forge::pykalman - - conda-forge::pyside6 >=6.0 # To ensure application works correctly with QtPy. + - conda-forge::pyside2 >=5.12 # To ensure application works correctly with QtPy. - conda-forge::python ~=3.9 - conda-forge::python-rapidjson - conda-forge::pyyaml diff --git a/sleap/gui/dialogs/bulletin.py b/sleap/gui/dialogs/bulletin.py index c9b7c6237..004519609 100644 --- a/sleap/gui/dialogs/bulletin.py +++ b/sleap/gui/dialogs/bulletin.py @@ -2,6 +2,8 @@ GUI for displaying the new announcement. """ +import os +os.environ['QT_API'] = 'pyside2' from qtpy.QtCore import Signal, Qt from qtpy.QtWebEngineWidgets import QWebEngineView from qtpy.QtCore import Property, Signal, QObject, QUrl From a724ec22a7585929fc8317afc87d79619dfc7284 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Mon, 18 Dec 2023 17:15:39 -0800 Subject: [PATCH 11/29] try with pyside6 and set environment variable --- environment_mac.yml | 2 +- sleap/gui/dialogs/bulletin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/environment_mac.yml b/environment_mac.yml index 0e4f5d2df..ee977a482 100644 --- a/environment_mac.yml +++ b/environment_mac.yml @@ -23,7 +23,7 @@ dependencies: - conda-forge::pillow - conda-forge::psutil - conda-forge::pykalman - - conda-forge::pyside2 >=5.12 # To ensure application works correctly with QtPy. + - conda-forge::pyside6 >=6.2 # To ensure application works correctly with QtPy. - conda-forge::python ~=3.9 - conda-forge::python-rapidjson - conda-forge::pyyaml diff --git a/sleap/gui/dialogs/bulletin.py b/sleap/gui/dialogs/bulletin.py index 004519609..9097295bd 100644 --- a/sleap/gui/dialogs/bulletin.py +++ b/sleap/gui/dialogs/bulletin.py @@ -3,7 +3,7 @@ """ import os -os.environ['QT_API'] = 'pyside2' +os.environ['QT_API'] = 'pyside6' from qtpy.QtCore import Signal, Qt from qtpy.QtWebEngineWidgets import QWebEngineView from qtpy.QtCore import Property, Signal, QObject, QUrl From 982751056a0692a1eca75a1d3edeb64e0e62cfb3 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Tue, 19 Dec 2023 12:02:29 -0800 Subject: [PATCH 12/29] pyside2 == 5.14.1 --- environment_mac.yml | 2 +- sleap/gui/dialogs/bulletin.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/environment_mac.yml b/environment_mac.yml index ee977a482..1cce74901 100644 --- a/environment_mac.yml +++ b/environment_mac.yml @@ -23,7 +23,7 @@ dependencies: - conda-forge::pillow - conda-forge::psutil - conda-forge::pykalman - - conda-forge::pyside6 >=6.2 # To ensure application works correctly with QtPy. + - conda-forge::pyside2 ==5.14.1 # To ensure application works correctly with QtPy. - conda-forge::python ~=3.9 - conda-forge::python-rapidjson - conda-forge::pyyaml diff --git a/sleap/gui/dialogs/bulletin.py b/sleap/gui/dialogs/bulletin.py index 9097295bd..fd68c2977 100644 --- a/sleap/gui/dialogs/bulletin.py +++ b/sleap/gui/dialogs/bulletin.py @@ -3,9 +3,9 @@ """ import os -os.environ['QT_API'] = 'pyside6' +# os.environ['QT_API'] = 'pyside6' from qtpy.QtCore import Signal, Qt -from qtpy.QtWebEngineWidgets import QWebEngineView +from qtpy import QtWebEngineWidgets #import QWebEngineView from qtpy.QtCore import Property, Signal, QObject, QUrl from qtpy.QtWebChannel import QWebChannel from qtpy import QtWidgets @@ -28,7 +28,7 @@ def show_bulletin(self): self.channel.registerObject("content", self.document) self.document.set_text(self._content) - self.view = QWebEngineView() + self.view = QtWebEngineWidgets.QWebEngineView() self.view.page().setWebChannel(self.channel) filename = str(Path(__file__).resolve().parent / "bulletin/markdown.html") From cd98d7cc854b5309dc8d86bfc0d6489baa7c93cf Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Tue, 19 Dec 2023 12:25:08 -0800 Subject: [PATCH 13/29] pyside6 ==6.2.2.1 --- sleap/gui/dialogs/bulletin.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sleap/gui/dialogs/bulletin.py b/sleap/gui/dialogs/bulletin.py index fd68c2977..c9b7c6237 100644 --- a/sleap/gui/dialogs/bulletin.py +++ b/sleap/gui/dialogs/bulletin.py @@ -2,10 +2,8 @@ GUI for displaying the new announcement. """ -import os -# os.environ['QT_API'] = 'pyside6' from qtpy.QtCore import Signal, Qt -from qtpy import QtWebEngineWidgets #import QWebEngineView +from qtpy.QtWebEngineWidgets import QWebEngineView from qtpy.QtCore import Property, Signal, QObject, QUrl from qtpy.QtWebChannel import QWebChannel from qtpy import QtWidgets @@ -28,7 +26,7 @@ def show_bulletin(self): self.channel.registerObject("content", self.document) self.document.set_text(self._content) - self.view = QtWebEngineWidgets.QWebEngineView() + self.view = QWebEngineView() self.view.page().setWebChannel(self.channel) filename = str(Path(__file__).resolve().parent / "bulletin/markdown.html") From a9673d8e86e8d17de31e0a090ab4d74ffd2d64d1 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Tue, 19 Dec 2023 16:40:07 -0800 Subject: [PATCH 14/29] pyside6 ==6.2.2.1 --- environment_mac.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment_mac.yml b/environment_mac.yml index 1cce74901..f40b3a8f6 100644 --- a/environment_mac.yml +++ b/environment_mac.yml @@ -23,7 +23,7 @@ dependencies: - conda-forge::pillow - conda-forge::psutil - conda-forge::pykalman - - conda-forge::pyside2 ==5.14.1 # To ensure application works correctly with QtPy. + - conda-forge::pyside6 ==6.2.2.1 # To ensure application works correctly with QtPy. - conda-forge::python ~=3.9 - conda-forge::python-rapidjson - conda-forge::pyyaml From b7e475d57731bbcc56c97fd297211ef7a414c4e4 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Tue, 19 Dec 2023 16:47:44 -0800 Subject: [PATCH 15/29] Resetting back to default --- environment_mac.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment_mac.yml b/environment_mac.yml index f40b3a8f6..0e4f5d2df 100644 --- a/environment_mac.yml +++ b/environment_mac.yml @@ -23,7 +23,7 @@ dependencies: - conda-forge::pillow - conda-forge::psutil - conda-forge::pykalman - - conda-forge::pyside6 ==6.2.2.1 # To ensure application works correctly with QtPy. + - conda-forge::pyside2 >=5.12 # To ensure application works correctly with QtPy. - conda-forge::python ~=3.9 - conda-forge::python-rapidjson - conda-forge::pyyaml From 18974dbda944004340b7cd633b1ab7e836237ed3 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Thu, 21 Dec 2023 00:04:04 -0800 Subject: [PATCH 16/29] add pip install PySide2==5.14.1 in conda_mac build.sh --- .conda_mac/build.sh | 1 + sleap/gui/dialogs/bulletin/markdown.html | 2 +- sleap/gui/dialogs/bulletin/marked.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.conda_mac/build.sh b/.conda_mac/build.sh index 2036035f6..af519131a 100644 --- a/.conda_mac/build.sh +++ b/.conda_mac/build.sh @@ -8,5 +8,6 @@ export PIP_NO_DEPENDENCIES=False export PIP_IGNORE_INSTALLED=False pip install --no-cache-dir -r requirements.txt +pip install PySide2==5.14.1 python setup.py install --single-version-externally-managed --record=record.txt \ No newline at end of file diff --git a/sleap/gui/dialogs/bulletin/markdown.html b/sleap/gui/dialogs/bulletin/markdown.html index 0627f0868..69ff1039d 100644 --- a/sleap/gui/dialogs/bulletin/markdown.html +++ b/sleap/gui/dialogs/bulletin/markdown.html @@ -2,7 +2,7 @@ - Bulletin + What's New? diff --git a/sleap/gui/dialogs/bulletin/marked.js b/sleap/gui/dialogs/bulletin/marked.js index 25be68253..fff171b58 100644 --- a/sleap/gui/dialogs/bulletin/marked.js +++ b/sleap/gui/dialogs/bulletin/marked.js @@ -1020,7 +1020,7 @@ if (title) { out += ' title="' + title + '"'; } - out += '>' + text + '
    '; + out += 'target="_blank" >' + text + ''; return out; }; From cada26ca64545b362325f8067b4fe5c6095b7967 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Thu, 21 Dec 2023 01:54:55 -0800 Subject: [PATCH 17/29] Add title to bulletin popup --- .conda_mac/build.sh | 1 - sleap/gui/app.py | 2 +- sleap/gui/dialogs/bulletin.py | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.conda_mac/build.sh b/.conda_mac/build.sh index af519131a..2036035f6 100644 --- a/.conda_mac/build.sh +++ b/.conda_mac/build.sh @@ -8,6 +8,5 @@ export PIP_NO_DEPENDENCIES=False export PIP_IGNORE_INSTALLED=False pip install --no-cache-dir -r requirements.txt -pip install PySide2==5.14.1 python setup.py install --single-version-externally-managed --record=record.txt \ No newline at end of file diff --git a/sleap/gui/app.py b/sleap/gui/app.py index f463a04e3..ed75b1ac6 100644 --- a/sleap/gui/app.py +++ b/sleap/gui/app.py @@ -207,7 +207,7 @@ def __init__( def bulletin_dialog(self): """Displays bulletin dialog is new announcement is available.""" # Initialize the bulletin popup worker - popup_worker = BulletinWorker(self.state["announcement"], self) + popup_worker = BulletinWorker("# What's New? \n#" + self.state["announcement"], self) popup_worker.show_bulletin() # Save the bulletin worker so we can close them later diff --git a/sleap/gui/dialogs/bulletin.py b/sleap/gui/dialogs/bulletin.py index c9b7c6237..31efca608 100644 --- a/sleap/gui/dialogs/bulletin.py +++ b/sleap/gui/dialogs/bulletin.py @@ -16,6 +16,7 @@ def __init__(self, content, parent=None): self._content = content # Set the window to stay on top self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint) + self.setWindowTitle("What's New?") def show_bulletin(self): From 3c35dda7c3abfe9229b4f34fb30e207554169513 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Mon, 1 Jan 2024 16:23:57 -0800 Subject: [PATCH 18/29] add navbar --- docs/bulletin.json | 9 +++- sleap/gui/app.py | 6 ++- sleap/gui/dialogs/bulletin.py | 31 ++++++++++++++ sleap/gui/dialogs/bulletin/markdown.css | 54 ++++++++++++++++++------ sleap/gui/dialogs/bulletin/markdown.html | 43 ++++++++++++++++++- sleap/gui/dialogs/bulletin/marked.js | 2 +- 6 files changed, 125 insertions(+), 20 deletions(-) diff --git a/docs/bulletin.json b/docs/bulletin.json index b42063fb9..2ba619af6 100644 --- a/docs/bulletin.json +++ b/docs/bulletin.json @@ -1,7 +1,12 @@ [ + { + "title": "SLEAP v1.3.3", + "date": "12/12/2023", + "content": "\n\n\nThis is a brown-bag release following insufficient restrictions on allowable `tensorflow` versions for the \"pypi\" extra `sleap[pypi]` in 1.3.2. While the conda packages for 1.3.2 were not affected (since `tensorflow` is pulled in from anaconda), the PyPI only package installed via `pip install sleap[pypi]` had conflicts between the version of `tensorflow` and the version of `keras`. See [1.3.0](https://github.com/talmolab/sleap/releases/tag/v1.3.0), [1.3.1](https://github.com/talmolab/sleap/releases/tag/v1.3.1), and [1.3.2](https://github.com/talmolab/sleap/releases/tag/v1.3.2) for previous notable changes. \n\n**From 1.3.2+, to install SLEAP through pip use `pip install sleap[pypi]` to ensure all dependencies are gathered.**\n\nAs a reminder:\n\n> The 1.3.1 dependency update requires [Mamba](https://mamba.readthedocs.io/en/latest/index.html) for faster dependency resolution. If you already have anaconda installed, then you _can_ set the solver to libmamba in the base environment:\n>```\n>conda update -n base conda\n>conda install -n base conda-libmamba-solver\n>conda config --set solver libmamba\n>```\n>Any subsequent `mamba` commands in the docs will need to be replaced with `conda` if you choose to use your existing Anaconda installation. \n>\n>Otherwise, follow the [recommended installation instruction for Mamba](https://mamba.readthedocs.io/en/latest/installation.html).\n\n### Quick install\n**`mamba` (Windows/Linux/GPU)**:\n```\nmamba create -y -n sleap -c conda-forge -c nvidia -c sleap -c anaconda sleap=1.3.3\n```\n\n**`mamba` (Mac)**:\n```\nmamba create -y -n sleap -c conda-forge -c anaconda -c sleap sleap=1.3.3\n```\n\n**`pip` (any OS except Apple Silicon)**:\n```\npip install sleap[pypi]==1.3.3\n```\n\n### Full Changelog\n\n##### Fixes\n* Do not try to remove item if already deleted by @roomrys in https://github.com/talmolab/sleap/pull/1498\n* Set `LD_LIBRARY_PATH` on mamba activate by @roomrys in https://github.com/talmolab/sleap/pull/1496\n* Reset `LD_LIBRARY_PATH` on deactivate by @roomrys in #1502\n\n##### Dependencies\n* Add version restirctions to tendorflow for pypi by @roomrys in #1485\n* Remove `imageio` pin by @roomrys in #1501\n\n**Full Changelog**: https://github.com/talmolab/sleap/compare/v1.3.2...v1.3.3\n\n" + }, { "title": "SLEAP v1.3.2", - "date": "12/8/2023", - "content": "\n\n\nSLEAP 1.3.2 adds some nice usability features thanks to both the community ideas and new contributors! See [1.3.0](https://github.com/talmolab/sleap/releases/tag/v1.3.0) and [1.3.1](https://github.com/talmolab/sleap/releases/tag/v1.3.1) for previous notable changes. As a reminder:\n\n> The 1.3.1 dependency update requires [Mamba](https://mamba.readthedocs.io/en/latest/index.html) for faster dependency resolution. If you already have anaconda installed, then you _can_ set the solver to libmamba in the base environment:\n>```\n>conda update -n base conda\n>conda install -n base conda-libmamba-solver\n>conda config --set solver libmamba\n>```\n>Any subsequent `mamba` commands in the docs will need to be replaced with `conda` if you choose to use your existing Anaconda installation. \n>\n>Otherwise, follow the [recommended installation instruction for Mamba](https://mamba.readthedocs.io/en/latest/installation.html).\n\n### Quick install\n**`mamba` (Windows/Linux/GPU)**:\n```\nmamba create -y -n sleap -c conda-forge -c nvidia -c sleap -c anaconda sleap=1.3.2\n```\n\n**`mamba` (Mac)**:\n```\nmamba create -y -n sleap -c conda-forge -c anaconda -c sleap sleap=1.3.2\n```\n\n**`pip` (any OS except Apple Silicon)**:\n```\npip install sleap[pypi]==1.3.2\n```\n\n### Highlights\n* Limit max tracks via track-local queues by @shrivaths16 and @talmo in https://github.com/talmolab/sleap/pull/1447\n* Add option to remove videos in batch by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1382 and https://github.com/talmolab/sleap/pull/1406\n* Add shortcut to export analysis for current video by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1414 and https://github.com/talmolab/sleap/pull/1444\n* Add video path and frame indices to metrics by @roomrys in https://github.com/talmolab/sleap/pull/1396\n* Add a button for copying model config to clipboard by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1433\n* Add Option to Export CSV by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1438\n\n### Full Changelog\n\n#### Enhancements\n* Add option to remove videos in batch by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1382 and https://github.com/talmolab/sleap/pull/1406\n* Add `Track` when add `Instance` by @roomrys in https://github.com/talmolab/sleap/pull/1408\n* Add `Video` to cache when adding `Track` by @roomrys in https://github.com/talmolab/sleap/pull/1407\n* Add shortcut to export analysis for current video by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1414 and https://github.com/talmolab/sleap/pull/1444\n* Add video path and frame indices to metrics by @roomrys in https://github.com/talmolab/sleap/pull/1396\n* Improve error message for detecting video backend by @roomrys in https://github.com/talmolab/sleap/pull/1441\n* Add a button for copying model config to clipboard by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1433\n* Add Option to Export CSV by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1438\n* Limit max tracks via track-local queues by @shrivaths16 and @talmo in https://github.com/talmolab/sleap/pull/1447\n\n#### Fixes\n* Minor fix in computation of OKS by @shrivaths16 in https://github.com/talmolab/sleap/pull/1383 and https://github.com/talmolab/sleap/pull/1399\n* Fix `Filedialog` to work across (mac)OS by @roomrys in https://github.com/talmolab/sleap/pull/1393\n* Fix panning bounding box by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1398\n* Fix skeleton templates by @roomrys in https://github.com/talmolab/sleap/pull/1404\n* Fix labels export for json by @roomrys in https://github.com/talmolab/sleap/pull/1410\n* Correct GUI state emulation by @roomrys in https://github.com/talmolab/sleap/pull/1422\n* Update status message on status bar by @shrivaths16 in https://github.com/talmolab/sleap/pull/1411\n* Fix error thrown when last video is deleted by @shrivaths16 in https://github.com/talmolab/sleap/pull/1421\n* Add model folder to the unzip path by @roomrys in https://github.com/talmolab/sleap/pull/1445\n* Fix drag and drop by @talmo in https://github.com/talmolab/sleap/pull/1449\n\n#### Dependencies\n* Pin micromamba version by @roomrys in https://github.com/talmolab/sleap/pull/1376\n* Add pip extras by @roomrys in https://github.com/talmolab/sleap/pull/1481\n\n#### New Contributors\n* @shrivaths16 made their first contribution in https://github.com/talmolab/sleap/pull/1383\n* @gitttt-1234 made their first contribution in https://github.com/talmolab/sleap/pull/1382\n* @KevinZ0217 made their first contribution in https://github.com/talmolab/sleap/pull/1414\n\n**Full Changelog**: https://github.com/talmolab/sleap/compare/v1.3.1...v1.3.2\n" + "date": "12/10/2023", + "content": "\n\n\nSLEAP 1.3.2 adds some nice usability features thanks to both the community ideas and new contributors! See [1.3.0](https://github.com/talmolab/sleap/releases/tag/v1.3.0) and [1.3.1](https://github.com/talmolab/sleap/releases/tag/v1.3.1) for previous notable changes. As a reminder:\n\n> The 1.3.1 dependency update requires [Mamba](https://mamba.readthedocs.io/en/latest/index.html) for faster dependency resolution. If you already have anaconda installed, then you _can_ set the solver to libmamba in the base environment:\n>```\n>conda update -n base conda\n>conda install -n base conda-libmamba-solver\n>conda config --set solver libmamba\n>```\n>Any subsequent `mamba` commands in the docs will need to be replaced with `conda` if you choose to use your existing Anaconda installation. \n>\n>Otherwise, follow the [recommended installation instruction for Mamba](https://mamba.readthedocs.io/en/latest/installation.html).\n\n### Quick install\n**`mamba` (Windows/Linux/GPU)**:\n```\nmamba create -y -n sleap -c conda-forge -c nvidia -c sleap -c anaconda sleap=1.3.2\n```\n\n**`mamba` (Mac)**:\n```\nmamba create -y -n sleap -c conda-forge -c anaconda -c sleap sleap=1.3.2\n```\n\n**`pip` (any OS except Apple Silicon)**:\n```\npip install sleap[pypi]==1.3.2\n```\n\n### Highlights\n* Limit max tracks via track-local queues by @shrivaths16 and @talmo in https://github.com/talmolab/sleap/pull/1447\n* Add option to remove videos in batch by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1382 and https://github.com/talmolab/sleap/pull/1406\n* Add shortcut to export analysis for current video by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1414 and https://github.com/talmolab/sleap/pull/1444\n* Add video path and frame indices to metrics by @roomrys in https://github.com/talmolab/sleap/pull/1396\n* Add a button for copying model config to clipboard by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1433\n* Add Option to Export CSV by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1438\n\n### Full Changelog\n\n##### Enhancements\n* Add option to remove videos in batch by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1382 and https://github.com/talmolab/sleap/pull/1406\n* Add `Track` when add `Instance` by @roomrys in https://github.com/talmolab/sleap/pull/1408\n* Add `Video` to cache when adding `Track` by @roomrys in https://github.com/talmolab/sleap/pull/1407\n* Add shortcut to export analysis for current video by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1414 and https://github.com/talmolab/sleap/pull/1444\n* Add video path and frame indices to metrics by @roomrys in https://github.com/talmolab/sleap/pull/1396\n* Improve error message for detecting video backend by @roomrys in https://github.com/talmolab/sleap/pull/1441\n* Add a button for copying model config to clipboard by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1433\n* Add Option to Export CSV by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1438\n* Limit max tracks via track-local queues by @shrivaths16 and @talmo in https://github.com/talmolab/sleap/pull/1447\n\n##### Fixes\n* Minor fix in computation of OKS by @shrivaths16 in https://github.com/talmolab/sleap/pull/1383 and https://github.com/talmolab/sleap/pull/1399\n* Fix `Filedialog` to work across (mac)OS by @roomrys in https://github.com/talmolab/sleap/pull/1393\n* Fix panning bounding box by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1398\n* Fix skeleton templates by @roomrys in https://github.com/talmolab/sleap/pull/1404\n* Fix labels export for json by @roomrys in https://github.com/talmolab/sleap/pull/1410\n* Correct GUI state emulation by @roomrys in https://github.com/talmolab/sleap/pull/1422\n* Update status message on status bar by @shrivaths16 in https://github.com/talmolab/sleap/pull/1411\n* Fix error thrown when last video is deleted by @shrivaths16 in https://github.com/talmolab/sleap/pull/1421\n* Add model folder to the unzip path by @roomrys in https://github.com/talmolab/sleap/pull/1445\n* Fix drag and drop by @talmo in https://github.com/talmolab/sleap/pull/1449\n\n##### Dependencies\n* Pin micromamba version by @roomrys in https://github.com/talmolab/sleap/pull/1376\n* Add pip extras by @roomrys in https://github.com/talmolab/sleap/pull/1481\n\n##### New Contributors\n* @shrivaths16 made their first contribution in https://github.com/talmolab/sleap/pull/1383\n* @gitttt-1234 made their first contribution in https://github.com/talmolab/sleap/pull/1382\n* @KevinZ0217 made their first contribution in https://github.com/talmolab/sleap/pull/1414\n\n**Full Changelog**: https://github.com/talmolab/sleap/compare/v1.3.1...v1.3.2\n" } ] \ No newline at end of file diff --git a/sleap/gui/app.py b/sleap/gui/app.py index ed75b1ac6..00ac33b7b 100644 --- a/sleap/gui/app.py +++ b/sleap/gui/app.py @@ -201,13 +201,15 @@ def __init__( ) # Display announcement bulletin popup if self.new_announcement_available: - self.bulletin_dialog() self.announcement_checker.update_announcement() + self.bulletin_dialog() def bulletin_dialog(self): """Displays bulletin dialog is new announcement is available.""" # Initialize the bulletin popup worker - popup_worker = BulletinWorker("# What's New? \n#" + self.state["announcement"], self) + popup_worker = BulletinWorker( + "".join(["## What's New? \n#", self.state["announcement"]]), self + ) popup_worker.show_bulletin() # Save the bulletin worker so we can close them later diff --git a/sleap/gui/dialogs/bulletin.py b/sleap/gui/dialogs/bulletin.py index 31efca608..28ff2f8c9 100644 --- a/sleap/gui/dialogs/bulletin.py +++ b/sleap/gui/dialogs/bulletin.py @@ -17,6 +17,19 @@ def __init__(self, content, parent=None): # Set the window to stay on top self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint) self.setWindowTitle("What's New?") + self.setGeometry(0, 0, 900, 750) + self.center_on_screen() + + def center_on_screen(self): + # Get the screen geometry + screen_geometry = QtWidgets.QDesktopWidget().screenGeometry() + + # Calculate the center of the screen + center_x = (screen_geometry.width() - self.width()) // 2 + center_y = (screen_geometry.height() - self.height()) // 2 + + # Move the window to the center of the screen + self.move(center_x, center_y) def show_bulletin(self): @@ -34,6 +47,7 @@ def show_bulletin(self): url = QUrl.fromLocalFile(filename) self.view.load(url) + self.view.setGeometry(0, 0, 600, 400) # Set the central window with view self.setCentralWidget(self.view) self.show() @@ -56,3 +70,20 @@ def set_text(self, text): self.textChanged.emit(self.m_text) text = Property(str, fget=get_text, fset=set_text, notify=textChanged) + + +# class MyWebEngineView(QWebEngineView): +# def createWindow(self, type): +# new_view = MyWebEngineView(self) +# new_view.show() +# return new_view + +# def acceptNavigationRequest(self, url, navigation_type, is_main_frame): +# if navigation_type == QWebEnginePage.NavigationTypeLinkClicked: +# # Emit the linkClicked signal when a link is clicked +# self.page().mainFrame().javaScriptWindowObjectCleared.connect( +# lambda: self.page().mainFrame().addToJavaScriptWindowObject("linkHandler", self) +# ) +# self.page().runJavaScript("linkHandler.linkClicked('%s');" % url.toString()) +# return False +# return super().acceptNavigationRequest(url, navigation_type, is_main_frame) diff --git a/sleap/gui/dialogs/bulletin/markdown.css b/sleap/gui/dialogs/bulletin/markdown.css index 1cc32b362..139823934 100644 --- a/sleap/gui/dialogs/bulletin/markdown.css +++ b/sleap/gui/dialogs/bulletin/markdown.css @@ -1,17 +1,18 @@ body { - margin: 0 auto; + margin: 0px auto; font-family: Helvetica, Arial, Sans-Serif; color: #000000; line-height: 1; max-width: 960px; - padding: 30px; + padding: 2px 30px; + font-size: 18px; } h1, h2, h3, h4 { - font-weight: 600; + font-weight: 500; } h1, @@ -25,22 +26,19 @@ p { } h1 { - font-size: 50px; + font-size: 48px; } h2 { - font-size: 38px; - /* The bottom margin is small. It's designed to be used with gray meta text - * below a post title. */ - margin: 24px 0 6px; + font-size: 41px; } h3 { - font-size: 26px; + font-size: 34px; } h4 { - font-size: 22px; + font-size: 27px; } h5 { @@ -57,10 +55,11 @@ a { a:hover { text-decoration: none; color: #ff6600b7; + cursor: pointer; } a:visited { - color: rgba(128, 0, 128, 0.815); + color: rgb(128, 0, 128); } ul, @@ -94,8 +93,8 @@ pre { code { font-family: Consolas, Monaco, Andale Mono, monospace; - line-height: 1.5; - font-size: 13px; + line-height: 1.2; + font-size: 15px; } aside { @@ -313,4 +312,33 @@ button:active { button::-moz-focus-inner { padding: 0; border: 0; +} + +#placeholder { + text-decoration: none; +} + +#navbar { + position: fixed; + top: 0; + right: 0; + padding: 10px; + background-color: #ffffff00; + color: black; + z-index: 1000; +} + +#navbar a { + margin-right: 10px; + text-decoration: underline; + scroll-behavior: smooth; +} + +#navbar a:hover { + cursor: pointer; + color: gray +} + +#navbar a:visited { + color: black; } \ No newline at end of file diff --git a/sleap/gui/dialogs/bulletin/markdown.html b/sleap/gui/dialogs/bulletin/markdown.html index 69ff1039d..70347d0c7 100644 --- a/sleap/gui/dialogs/bulletin/markdown.html +++ b/sleap/gui/dialogs/bulletin/markdown.html @@ -1,7 +1,8 @@ - + - + + What's New? @@ -9,6 +10,7 @@
    + \ No newline at end of file diff --git a/sleap/gui/dialogs/bulletin/marked.js b/sleap/gui/dialogs/bulletin/marked.js index fff171b58..14ad05009 100644 --- a/sleap/gui/dialogs/bulletin/marked.js +++ b/sleap/gui/dialogs/bulletin/marked.js @@ -1020,7 +1020,7 @@ if (title) { out += ' title="' + title + '"'; } - out += 'target="_blank" >' + text + ''; + out += ' target="_blank" >' + text + ''; return out; }; From 17375616d83c38f0b9ee945fc61f38d1247906e8 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Wed, 10 Jan 2024 23:55:13 -0800 Subject: [PATCH 19/29] test skip method for macos --- docs/bulletin.json | 12 - sleap/gui/app.py | 4 +- sleap/gui/dialogs/bulletin.py | 17 -- sleap/gui/dialogs/bulletin/markdown.css | 347 ++--------------------- sleap/gui/dialogs/bulletin/markdown.html | 71 ++--- sleap/gui/dialogs/bulletin/pico.min.css | 5 + tests/io/test_formats.py | 3 + 7 files changed, 71 insertions(+), 388 deletions(-) delete mode 100644 docs/bulletin.json create mode 100644 sleap/gui/dialogs/bulletin/pico.min.css diff --git a/docs/bulletin.json b/docs/bulletin.json deleted file mode 100644 index 2ba619af6..000000000 --- a/docs/bulletin.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "title": "SLEAP v1.3.3", - "date": "12/12/2023", - "content": "\n\n\nThis is a brown-bag release following insufficient restrictions on allowable `tensorflow` versions for the \"pypi\" extra `sleap[pypi]` in 1.3.2. While the conda packages for 1.3.2 were not affected (since `tensorflow` is pulled in from anaconda), the PyPI only package installed via `pip install sleap[pypi]` had conflicts between the version of `tensorflow` and the version of `keras`. See [1.3.0](https://github.com/talmolab/sleap/releases/tag/v1.3.0), [1.3.1](https://github.com/talmolab/sleap/releases/tag/v1.3.1), and [1.3.2](https://github.com/talmolab/sleap/releases/tag/v1.3.2) for previous notable changes. \n\n**From 1.3.2+, to install SLEAP through pip use `pip install sleap[pypi]` to ensure all dependencies are gathered.**\n\nAs a reminder:\n\n> The 1.3.1 dependency update requires [Mamba](https://mamba.readthedocs.io/en/latest/index.html) for faster dependency resolution. If you already have anaconda installed, then you _can_ set the solver to libmamba in the base environment:\n>```\n>conda update -n base conda\n>conda install -n base conda-libmamba-solver\n>conda config --set solver libmamba\n>```\n>Any subsequent `mamba` commands in the docs will need to be replaced with `conda` if you choose to use your existing Anaconda installation. \n>\n>Otherwise, follow the [recommended installation instruction for Mamba](https://mamba.readthedocs.io/en/latest/installation.html).\n\n### Quick install\n**`mamba` (Windows/Linux/GPU)**:\n```\nmamba create -y -n sleap -c conda-forge -c nvidia -c sleap -c anaconda sleap=1.3.3\n```\n\n**`mamba` (Mac)**:\n```\nmamba create -y -n sleap -c conda-forge -c anaconda -c sleap sleap=1.3.3\n```\n\n**`pip` (any OS except Apple Silicon)**:\n```\npip install sleap[pypi]==1.3.3\n```\n\n### Full Changelog\n\n##### Fixes\n* Do not try to remove item if already deleted by @roomrys in https://github.com/talmolab/sleap/pull/1498\n* Set `LD_LIBRARY_PATH` on mamba activate by @roomrys in https://github.com/talmolab/sleap/pull/1496\n* Reset `LD_LIBRARY_PATH` on deactivate by @roomrys in #1502\n\n##### Dependencies\n* Add version restirctions to tendorflow for pypi by @roomrys in #1485\n* Remove `imageio` pin by @roomrys in #1501\n\n**Full Changelog**: https://github.com/talmolab/sleap/compare/v1.3.2...v1.3.3\n\n" - }, - { - "title": "SLEAP v1.3.2", - "date": "12/10/2023", - "content": "\n\n\nSLEAP 1.3.2 adds some nice usability features thanks to both the community ideas and new contributors! See [1.3.0](https://github.com/talmolab/sleap/releases/tag/v1.3.0) and [1.3.1](https://github.com/talmolab/sleap/releases/tag/v1.3.1) for previous notable changes. As a reminder:\n\n> The 1.3.1 dependency update requires [Mamba](https://mamba.readthedocs.io/en/latest/index.html) for faster dependency resolution. If you already have anaconda installed, then you _can_ set the solver to libmamba in the base environment:\n>```\n>conda update -n base conda\n>conda install -n base conda-libmamba-solver\n>conda config --set solver libmamba\n>```\n>Any subsequent `mamba` commands in the docs will need to be replaced with `conda` if you choose to use your existing Anaconda installation. \n>\n>Otherwise, follow the [recommended installation instruction for Mamba](https://mamba.readthedocs.io/en/latest/installation.html).\n\n### Quick install\n**`mamba` (Windows/Linux/GPU)**:\n```\nmamba create -y -n sleap -c conda-forge -c nvidia -c sleap -c anaconda sleap=1.3.2\n```\n\n**`mamba` (Mac)**:\n```\nmamba create -y -n sleap -c conda-forge -c anaconda -c sleap sleap=1.3.2\n```\n\n**`pip` (any OS except Apple Silicon)**:\n```\npip install sleap[pypi]==1.3.2\n```\n\n### Highlights\n* Limit max tracks via track-local queues by @shrivaths16 and @talmo in https://github.com/talmolab/sleap/pull/1447\n* Add option to remove videos in batch by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1382 and https://github.com/talmolab/sleap/pull/1406\n* Add shortcut to export analysis for current video by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1414 and https://github.com/talmolab/sleap/pull/1444\n* Add video path and frame indices to metrics by @roomrys in https://github.com/talmolab/sleap/pull/1396\n* Add a button for copying model config to clipboard by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1433\n* Add Option to Export CSV by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1438\n\n### Full Changelog\n\n##### Enhancements\n* Add option to remove videos in batch by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1382 and https://github.com/talmolab/sleap/pull/1406\n* Add `Track` when add `Instance` by @roomrys in https://github.com/talmolab/sleap/pull/1408\n* Add `Video` to cache when adding `Track` by @roomrys in https://github.com/talmolab/sleap/pull/1407\n* Add shortcut to export analysis for current video by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1414 and https://github.com/talmolab/sleap/pull/1444\n* Add video path and frame indices to metrics by @roomrys in https://github.com/talmolab/sleap/pull/1396\n* Improve error message for detecting video backend by @roomrys in https://github.com/talmolab/sleap/pull/1441\n* Add a button for copying model config to clipboard by @KevinZ0217 in https://github.com/talmolab/sleap/pull/1433\n* Add Option to Export CSV by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1438\n* Limit max tracks via track-local queues by @shrivaths16 and @talmo in https://github.com/talmolab/sleap/pull/1447\n\n##### Fixes\n* Minor fix in computation of OKS by @shrivaths16 in https://github.com/talmolab/sleap/pull/1383 and https://github.com/talmolab/sleap/pull/1399\n* Fix `Filedialog` to work across (mac)OS by @roomrys in https://github.com/talmolab/sleap/pull/1393\n* Fix panning bounding box by @gitttt-1234 in https://github.com/talmolab/sleap/pull/1398\n* Fix skeleton templates by @roomrys in https://github.com/talmolab/sleap/pull/1404\n* Fix labels export for json by @roomrys in https://github.com/talmolab/sleap/pull/1410\n* Correct GUI state emulation by @roomrys in https://github.com/talmolab/sleap/pull/1422\n* Update status message on status bar by @shrivaths16 in https://github.com/talmolab/sleap/pull/1411\n* Fix error thrown when last video is deleted by @shrivaths16 in https://github.com/talmolab/sleap/pull/1421\n* Add model folder to the unzip path by @roomrys in https://github.com/talmolab/sleap/pull/1445\n* Fix drag and drop by @talmo in https://github.com/talmolab/sleap/pull/1449\n\n##### Dependencies\n* Pin micromamba version by @roomrys in https://github.com/talmolab/sleap/pull/1376\n* Add pip extras by @roomrys in https://github.com/talmolab/sleap/pull/1481\n\n##### New Contributors\n* @shrivaths16 made their first contribution in https://github.com/talmolab/sleap/pull/1383\n* @gitttt-1234 made their first contribution in https://github.com/talmolab/sleap/pull/1382\n* @KevinZ0217 made their first contribution in https://github.com/talmolab/sleap/pull/1414\n\n**Full Changelog**: https://github.com/talmolab/sleap/compare/v1.3.1...v1.3.2\n" - } -] \ No newline at end of file diff --git a/sleap/gui/app.py b/sleap/gui/app.py index 00ac33b7b..21f97ed80 100644 --- a/sleap/gui/app.py +++ b/sleap/gui/app.py @@ -201,14 +201,14 @@ def __init__( ) # Display announcement bulletin popup if self.new_announcement_available: - self.announcement_checker.update_announcement() + self.announcement_checker.update_latest_announcement() self.bulletin_dialog() def bulletin_dialog(self): """Displays bulletin dialog is new announcement is available.""" # Initialize the bulletin popup worker popup_worker = BulletinWorker( - "".join(["## What's New? \n#", self.state["announcement"]]), self + "".join(["# What's New? \n", self.state["announcement"]]), self ) popup_worker.show_bulletin() diff --git a/sleap/gui/dialogs/bulletin.py b/sleap/gui/dialogs/bulletin.py index 28ff2f8c9..6595e08d4 100644 --- a/sleap/gui/dialogs/bulletin.py +++ b/sleap/gui/dialogs/bulletin.py @@ -70,20 +70,3 @@ def set_text(self, text): self.textChanged.emit(self.m_text) text = Property(str, fget=get_text, fset=set_text, notify=textChanged) - - -# class MyWebEngineView(QWebEngineView): -# def createWindow(self, type): -# new_view = MyWebEngineView(self) -# new_view.show() -# return new_view - -# def acceptNavigationRequest(self, url, navigation_type, is_main_frame): -# if navigation_type == QWebEnginePage.NavigationTypeLinkClicked: -# # Emit the linkClicked signal when a link is clicked -# self.page().mainFrame().javaScriptWindowObjectCleared.connect( -# lambda: self.page().mainFrame().addToJavaScriptWindowObject("linkHandler", self) -# ) -# self.page().runJavaScript("linkHandler.linkClicked('%s');" % url.toString()) -# return False -# return super().acceptNavigationRequest(url, navigation_type, is_main_frame) diff --git a/sleap/gui/dialogs/bulletin/markdown.css b/sleap/gui/dialogs/bulletin/markdown.css index 139823934..7c94a2d49 100644 --- a/sleap/gui/dialogs/bulletin/markdown.css +++ b/sleap/gui/dialogs/bulletin/markdown.css @@ -1,344 +1,47 @@ body { - margin: 0px auto; - font-family: Helvetica, Arial, Sans-Serif; - color: #000000; - line-height: 1; - max-width: 960px; - padding: 2px 30px; - font-size: 18px; -} - -h1, -h2, -h3, -h4 { - font-weight: 500; + font-family: Helvetica, Arial, sans-serif; } h1, h2, h3, h4, -h5, -p { - margin-bottom: 24px; - padding: 0; -} - -h1 { - font-size: 48px; -} - -h2 { - font-size: 41px; -} - -h3 { - font-size: 34px; -} - -h4 { - font-size: 27px; -} - h5 { - font-size: 20px; -} - -a { - color: #0099ffd5; - margin: 0; - padding: 0; - vertical-align: baseline; -} - -a:hover { - text-decoration: none; - color: #ff6600b7; - cursor: pointer; -} - -a:visited { - color: rgb(128, 0, 128); + color: #636364; + font-style: italic; } -ul, -ol { - padding: 0; - margin: 0; -} - -li { - line-height: 24px; -} - -li ul, -li ul { - margin-left: 24px; -} - -p, -ul, -ol { - font-size: 16px; - line-height: 24px; - max-width: 540px; -} - -pre { - padding: 0px 24px; - max-width: 800px; - white-space: pre-wrap; -} - -code { - font-family: Consolas, Monaco, Andale Mono, monospace; - line-height: 1.2; - font-size: 15px; -} - -aside { - display: block; - float: right; - width: 390px; -} - -blockquote { - border-left: .5em solid #eee; - padding: 0 2em; - margin-left: 0; - max-width: 476px; -} - -blockquote cite { - font-size: 14px; - line-height: 20px; - color: #bfbfbf; -} - -blockquote cite:before { - content: '\2014 \00A0'; -} - -blockquote p { - color: #666; - max-width: 460px; -} - -hr { - width: 540px; - text-align: left; - margin: 0 auto 0 0; - color: #999; -} - -/* Code below this line is copyright Twitter Inc. */ - -button, -input, -select, -textarea { - font-size: 100%; - margin: 0; - vertical-align: baseline; - *vertical-align: middle; -} - -button, -input { - line-height: normal; - *overflow: visible; -} - -button::-moz-focus-inner, -input::-moz-focus-inner { - border: 0; - padding: 0; -} - -button, -input[type="button"], -input[type="reset"], -input[type="submit"] { - cursor: pointer; - -webkit-appearance: button; -} - -input[type=checkbox], -input[type=radio] { - cursor: pointer; -} - -/* override default chrome & firefox settings */ -input:not([type="image"]), -textarea { - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; -} - -input[type="search"] { - -webkit-appearance: textfield; - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; -} - -input[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -label, -input, -select, -textarea { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 13px; - font-weight: normal; - line-height: normal; - margin-bottom: 18px; -} - -input[type=checkbox], -input[type=radio] { - cursor: pointer; - margin-bottom: 0; -} - -input[type=text], -input[type=password], -textarea, -select { - display: inline-block; - width: 210px; - padding: 4px; - font-size: 13px; - font-weight: normal; - line-height: 18px; - height: 18px; - color: #808080; - border: 1px solid #ccc; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} - -select, -input[type=file] { - height: 27px; - line-height: 27px; -} - -textarea { - height: auto; -} - -/* grey out placeholders */ -:-moz-placeholder { - color: #bfbfbf; -} - -::-webkit-input-placeholder { - color: #bfbfbf; -} - -input[type=text], -input[type=password], -select, -textarea { - -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; - -moz-transition: border linear 0.2s, box-shadow linear 0.2s; - transition: border linear 0.2s, box-shadow linear 0.2s; - -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); - -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); - box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); -} - -input[type=text]:focus, -input[type=password]:focus, -textarea:focus { - outline: none; - border-color: rgba(82, 168, 236, 0.8); - -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); - -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); - box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); -} - -/* buttons */ -button { - display: inline-block; - padding: 4px 14px; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 13px; - line-height: 18px; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - background-color: #0064cd; - background-repeat: repeat-x; - background-image: -khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd)); - background-image: -moz-linear-gradient(top, #049cdb, #0064cd); - background-image: -ms-linear-gradient(top, #049cdb, #0064cd); - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd)); - background-image: -webkit-linear-gradient(top, #049cdb, #0064cd); - background-image: -o-linear-gradient(top, #049cdb, #0064cd); - background-image: linear-gradient(top, #049cdb, #0064cd); - color: #fff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - border: 1px solid #004b9a; - border-bottom-color: #003f81; - -webkit-transition: 0.1s linear all; - -moz-transition: 0.1s linear all; - transition: 0.1s linear all; - border-color: #0064cd #0064cd #003f81; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); -} - -button:hover { - color: #fff; - background-position: 0 -15px; - text-decoration: none; -} - -button:active { - -webkit-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -button::-moz-focus-inner { - padding: 0; - border: 0; -} - -#placeholder { - text-decoration: none; +#container { + display: flex; } #navbar { + width: 200px; /* Set the fixed width for the navbar */ position: fixed; top: 0; - right: 0; - padding: 10px; - background-color: #ffffff00; - color: black; - z-index: 1000; + left: 0; + height: 100vh; + background-color: #2e2e2e; + color: #fff; + padding: 12px; + box-sizing: border-box; + flex-grow: 1; + list-style: disc; } -#navbar a { - margin-right: 10px; - text-decoration: underline; - scroll-behavior: smooth; +#navbar ul { + margin-left: 2px; + padding-left: 12px; + } -#navbar a:hover { - cursor: pointer; - color: gray +#navbar a { + color: white; } -#navbar a:visited { - color: black; +#content { + flex-grow: 1; + padding: 20px; + box-sizing: border-box; + margin-left: 200px; /* Adjust margin to match navbar width */ } \ No newline at end of file diff --git a/sleap/gui/dialogs/bulletin/markdown.html b/sleap/gui/dialogs/bulletin/markdown.html index 70347d0c7..f83884b72 100644 --- a/sleap/gui/dialogs/bulletin/markdown.html +++ b/sleap/gui/dialogs/bulletin/markdown.html @@ -4,23 +4,27 @@ What's New? + -
    - +
    +
    + +
    \ No newline at end of file diff --git a/sleap/gui/dialogs/bulletin/pico.min.css b/sleap/gui/dialogs/bulletin/pico.min.css new file mode 100644 index 000000000..d0d5ba441 --- /dev/null +++ b/sleap/gui/dialogs/bulletin/pico.min.css @@ -0,0 +1,5 @@ +@charset "UTF-8";/*! + * Pico CSS v1.5.11 (https://picocss.com) + * Copyright 2019-2023 - Licensed under MIT + */:root{--font-family:system-ui,-apple-system,"Segoe UI","Roboto","Ubuntu","Cantarell","Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--line-height:1.5;--font-weight:400;--font-size:16px;--border-radius:0.25rem;--border-width:1px;--outline-width:3px;--spacing:1rem;--typography-spacing-vertical:1.5rem;--block-spacing-vertical:calc(var(--spacing) * 2);--block-spacing-horizontal:var(--spacing);--grid-spacing-vertical:0;--grid-spacing-horizontal:var(--spacing);--form-element-spacing-vertical:0.75rem;--form-element-spacing-horizontal:1rem;--nav-element-spacing-vertical:1rem;--nav-element-spacing-horizontal:0.5rem;--nav-link-spacing-vertical:0.5rem;--nav-link-spacing-horizontal:0.5rem;--form-label-font-weight:var(--font-weight);--transition:0.2s ease-in-out;--modal-overlay-backdrop-filter:blur(0.25rem)}@media (min-width:576px){:root{--font-size:17px}}@media (min-width:768px){:root{--font-size:18px}}@media (min-width:992px){:root{--font-size:19px}}@media (min-width:1200px){:root{--font-size:20px}}@media (min-width:576px){body>footer,body>header,body>main,section{--block-spacing-vertical:calc(var(--spacing) * 2.5)}}@media (min-width:768px){body>footer,body>header,body>main,section{--block-spacing-vertical:calc(var(--spacing) * 3)}}@media (min-width:992px){body>footer,body>header,body>main,section{--block-spacing-vertical:calc(var(--spacing) * 3.5)}}@media (min-width:1200px){body>footer,body>header,body>main,section{--block-spacing-vertical:calc(var(--spacing) * 4)}}@media (min-width:576px){article{--block-spacing-horizontal:calc(var(--spacing) * 1.25)}}@media (min-width:768px){article{--block-spacing-horizontal:calc(var(--spacing) * 1.5)}}@media (min-width:992px){article{--block-spacing-horizontal:calc(var(--spacing) * 1.75)}}@media (min-width:1200px){article{--block-spacing-horizontal:calc(var(--spacing) * 2)}}dialog>article{--block-spacing-vertical:calc(var(--spacing) * 2);--block-spacing-horizontal:var(--spacing)}@media (min-width:576px){dialog>article{--block-spacing-vertical:calc(var(--spacing) * 2.5);--block-spacing-horizontal:calc(var(--spacing) * 1.25)}}@media (min-width:768px){dialog>article{--block-spacing-vertical:calc(var(--spacing) * 3);--block-spacing-horizontal:calc(var(--spacing) * 1.5)}}a{--text-decoration:none}a.contrast,a.secondary{--text-decoration:underline}small{--font-size:0.875em}h1,h2,h3,h4,h5,h6{--font-weight:700}h1{--font-size:2rem;--typography-spacing-vertical:3rem}h2{--font-size:1.75rem;--typography-spacing-vertical:2.625rem}h3{--font-size:1.5rem;--typography-spacing-vertical:2.25rem}h4{--font-size:1.25rem;--typography-spacing-vertical:1.874rem}h5{--font-size:1.125rem;--typography-spacing-vertical:1.6875rem}[type=checkbox],[type=radio]{--border-width:2px}[type=checkbox][role=switch]{--border-width:3px}tfoot td,tfoot th,thead td,thead th{--border-width:3px}:not(thead,tfoot)>*>td{--font-size:0.875em}code,kbd,pre,samp{--font-family:"Menlo","Consolas","Roboto Mono","Ubuntu Monospace","Noto Mono","Oxygen Mono","Liberation Mono",monospace,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"}kbd{--font-weight:bolder}:root:not([data-theme=dark]),[data-theme=light]{--background-color:#fff;--color:hsl(205, 20%, 32%);--h1-color:hsl(205, 30%, 15%);--h2-color:#24333e;--h3-color:hsl(205, 25%, 23%);--h4-color:#374956;--h5-color:hsl(205, 20%, 32%);--h6-color:#4d606d;--muted-color:hsl(205, 10%, 50%);--muted-border-color:hsl(205, 20%, 94%);--primary:hsl(195, 85%, 41%);--primary-hover:hsl(195, 90%, 32%);--primary-focus:rgba(16, 149, 193, 0.125);--primary-inverse:#fff;--secondary:hsl(205, 15%, 41%);--secondary-hover:hsl(205, 20%, 32%);--secondary-focus:rgba(89, 107, 120, 0.125);--secondary-inverse:#fff;--contrast:hsl(205, 30%, 15%);--contrast-hover:#000;--contrast-focus:rgba(89, 107, 120, 0.125);--contrast-inverse:#fff;--mark-background-color:#fff2ca;--mark-color:#543a26;--ins-color:#388e3c;--del-color:#c62828;--blockquote-border-color:var(--muted-border-color);--blockquote-footer-color:var(--muted-color);--button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--form-element-background-color:transparent;--form-element-border-color:hsl(205, 14%, 68%);--form-element-color:var(--color);--form-element-placeholder-color:var(--muted-color);--form-element-active-background-color:transparent;--form-element-active-border-color:var(--primary);--form-element-focus-color:var(--primary-focus);--form-element-disabled-background-color:hsl(205, 18%, 86%);--form-element-disabled-border-color:hsl(205, 14%, 68%);--form-element-disabled-opacity:0.5;--form-element-invalid-border-color:#c62828;--form-element-invalid-active-border-color:#d32f2f;--form-element-invalid-focus-color:rgba(211, 47, 47, 0.125);--form-element-valid-border-color:#388e3c;--form-element-valid-active-border-color:#43a047;--form-element-valid-focus-color:rgba(67, 160, 71, 0.125);--switch-background-color:hsl(205, 16%, 77%);--switch-color:var(--primary-inverse);--switch-checked-background-color:var(--primary);--range-border-color:hsl(205, 18%, 86%);--range-active-border-color:hsl(205, 16%, 77%);--range-thumb-border-color:var(--background-color);--range-thumb-color:var(--secondary);--range-thumb-hover-color:var(--secondary-hover);--range-thumb-active-color:var(--primary);--table-border-color:var(--muted-border-color);--table-row-stripped-background-color:#f6f8f9;--code-background-color:hsl(205, 20%, 94%);--code-color:var(--muted-color);--code-kbd-background-color:var(--contrast);--code-kbd-color:var(--contrast-inverse);--code-tag-color:hsl(330, 40%, 50%);--code-property-color:hsl(185, 40%, 40%);--code-value-color:hsl(40, 20%, 50%);--code-comment-color:hsl(205, 14%, 68%);--accordion-border-color:var(--muted-border-color);--accordion-close-summary-color:var(--color);--accordion-open-summary-color:var(--muted-color);--card-background-color:var(--background-color);--card-border-color:var(--muted-border-color);--card-box-shadow:0.0145rem 0.029rem 0.174rem rgba(27, 40, 50, 0.01698),0.0335rem 0.067rem 0.402rem rgba(27, 40, 50, 0.024),0.0625rem 0.125rem 0.75rem rgba(27, 40, 50, 0.03),0.1125rem 0.225rem 1.35rem rgba(27, 40, 50, 0.036),0.2085rem 0.417rem 2.502rem rgba(27, 40, 50, 0.04302),0.5rem 1rem 6rem rgba(27, 40, 50, 0.06),0 0 0 0.0625rem rgba(27, 40, 50, 0.015);--card-sectionning-background-color:#fbfbfc;--dropdown-background-color:#fbfbfc;--dropdown-border-color:#e1e6eb;--dropdown-box-shadow:var(--card-box-shadow);--dropdown-color:var(--color);--dropdown-hover-background-color:hsl(205, 20%, 94%);--modal-overlay-background-color:rgba(213, 220, 226, 0.7);--progress-background-color:hsl(205, 18%, 86%);--progress-color:var(--primary);--loading-spinner-opacity:0.5;--tooltip-background-color:var(--contrast);--tooltip-color:var(--contrast-inverse);--icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(65, 84, 98)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button-inverse:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(115, 130, 140)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(65, 84, 98)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(198, 40, 40)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");--icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(65, 84, 98)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(65, 84, 98)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(56, 142, 60)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");color-scheme:light}@media only screen and (prefers-color-scheme:dark){:root:not([data-theme]){--background-color:#11191f;--color:hsl(205, 16%, 77%);--h1-color:hsl(205, 20%, 94%);--h2-color:#e1e6eb;--h3-color:hsl(205, 18%, 86%);--h4-color:#c8d1d8;--h5-color:hsl(205, 16%, 77%);--h6-color:#afbbc4;--muted-color:hsl(205, 10%, 50%);--muted-border-color:#1f2d38;--primary:hsl(195, 85%, 41%);--primary-hover:hsl(195, 80%, 50%);--primary-focus:rgba(16, 149, 193, 0.25);--primary-inverse:#fff;--secondary:hsl(205, 15%, 41%);--secondary-hover:hsl(205, 10%, 50%);--secondary-focus:rgba(115, 130, 140, 0.25);--secondary-inverse:#fff;--contrast:hsl(205, 20%, 94%);--contrast-hover:#fff;--contrast-focus:rgba(115, 130, 140, 0.25);--contrast-inverse:#000;--mark-background-color:#d1c284;--mark-color:#11191f;--ins-color:#388e3c;--del-color:#c62828;--blockquote-border-color:var(--muted-border-color);--blockquote-footer-color:var(--muted-color);--button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--form-element-background-color:#11191f;--form-element-border-color:#374956;--form-element-color:var(--color);--form-element-placeholder-color:var(--muted-color);--form-element-active-background-color:var(--form-element-background-color);--form-element-active-border-color:var(--primary);--form-element-focus-color:var(--primary-focus);--form-element-disabled-background-color:hsl(205, 25%, 23%);--form-element-disabled-border-color:hsl(205, 20%, 32%);--form-element-disabled-opacity:0.5;--form-element-invalid-border-color:#b71c1c;--form-element-invalid-active-border-color:#c62828;--form-element-invalid-focus-color:rgba(198, 40, 40, 0.25);--form-element-valid-border-color:#2e7d32;--form-element-valid-active-border-color:#388e3c;--form-element-valid-focus-color:rgba(56, 142, 60, 0.25);--switch-background-color:#374956;--switch-color:var(--primary-inverse);--switch-checked-background-color:var(--primary);--range-border-color:#24333e;--range-active-border-color:hsl(205, 25%, 23%);--range-thumb-border-color:var(--background-color);--range-thumb-color:var(--secondary);--range-thumb-hover-color:var(--secondary-hover);--range-thumb-active-color:var(--primary);--table-border-color:var(--muted-border-color);--table-row-stripped-background-color:rgba(115, 130, 140, 0.05);--code-background-color:#18232c;--code-color:var(--muted-color);--code-kbd-background-color:var(--contrast);--code-kbd-color:var(--contrast-inverse);--code-tag-color:hsl(330, 30%, 50%);--code-property-color:hsl(185, 30%, 50%);--code-value-color:hsl(40, 10%, 50%);--code-comment-color:#4d606d;--accordion-border-color:var(--muted-border-color);--accordion-active-summary-color:var(--primary);--accordion-close-summary-color:var(--color);--accordion-open-summary-color:var(--muted-color);--card-background-color:#141e26;--card-border-color:var(--card-background-color);--card-box-shadow:0.0145rem 0.029rem 0.174rem rgba(0, 0, 0, 0.01698),0.0335rem 0.067rem 0.402rem rgba(0, 0, 0, 0.024),0.0625rem 0.125rem 0.75rem rgba(0, 0, 0, 0.03),0.1125rem 0.225rem 1.35rem rgba(0, 0, 0, 0.036),0.2085rem 0.417rem 2.502rem rgba(0, 0, 0, 0.04302),0.5rem 1rem 6rem rgba(0, 0, 0, 0.06),0 0 0 0.0625rem rgba(0, 0, 0, 0.015);--card-sectionning-background-color:#18232c;--dropdown-background-color:hsl(205, 30%, 15%);--dropdown-border-color:#24333e;--dropdown-box-shadow:var(--card-box-shadow);--dropdown-color:var(--color);--dropdown-hover-background-color:rgba(36, 51, 62, 0.75);--modal-overlay-background-color:rgba(36, 51, 62, 0.8);--progress-background-color:#24333e;--progress-color:var(--primary);--loading-spinner-opacity:0.5;--tooltip-background-color:var(--contrast);--tooltip-color:var(--contrast-inverse);--icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button-inverse:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(0, 0, 0)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(115, 130, 140)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(183, 28, 28)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");--icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(46, 125, 50)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");color-scheme:dark}}[data-theme=dark]{--background-color:#11191f;--color:hsl(205, 16%, 77%);--h1-color:hsl(205, 20%, 94%);--h2-color:#e1e6eb;--h3-color:hsl(205, 18%, 86%);--h4-color:#c8d1d8;--h5-color:hsl(205, 16%, 77%);--h6-color:#afbbc4;--muted-color:hsl(205, 10%, 50%);--muted-border-color:#1f2d38;--primary:hsl(195, 85%, 41%);--primary-hover:hsl(195, 80%, 50%);--primary-focus:rgba(16, 149, 193, 0.25);--primary-inverse:#fff;--secondary:hsl(205, 15%, 41%);--secondary-hover:hsl(205, 10%, 50%);--secondary-focus:rgba(115, 130, 140, 0.25);--secondary-inverse:#fff;--contrast:hsl(205, 20%, 94%);--contrast-hover:#fff;--contrast-focus:rgba(115, 130, 140, 0.25);--contrast-inverse:#000;--mark-background-color:#d1c284;--mark-color:#11191f;--ins-color:#388e3c;--del-color:#c62828;--blockquote-border-color:var(--muted-border-color);--blockquote-footer-color:var(--muted-color);--button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--form-element-background-color:#11191f;--form-element-border-color:#374956;--form-element-color:var(--color);--form-element-placeholder-color:var(--muted-color);--form-element-active-background-color:var(--form-element-background-color);--form-element-active-border-color:var(--primary);--form-element-focus-color:var(--primary-focus);--form-element-disabled-background-color:hsl(205, 25%, 23%);--form-element-disabled-border-color:hsl(205, 20%, 32%);--form-element-disabled-opacity:0.5;--form-element-invalid-border-color:#b71c1c;--form-element-invalid-active-border-color:#c62828;--form-element-invalid-focus-color:rgba(198, 40, 40, 0.25);--form-element-valid-border-color:#2e7d32;--form-element-valid-active-border-color:#388e3c;--form-element-valid-focus-color:rgba(56, 142, 60, 0.25);--switch-background-color:#374956;--switch-color:var(--primary-inverse);--switch-checked-background-color:var(--primary);--range-border-color:#24333e;--range-active-border-color:hsl(205, 25%, 23%);--range-thumb-border-color:var(--background-color);--range-thumb-color:var(--secondary);--range-thumb-hover-color:var(--secondary-hover);--range-thumb-active-color:var(--primary);--table-border-color:var(--muted-border-color);--table-row-stripped-background-color:rgba(115, 130, 140, 0.05);--code-background-color:#18232c;--code-color:var(--muted-color);--code-kbd-background-color:var(--contrast);--code-kbd-color:var(--contrast-inverse);--code-tag-color:hsl(330, 30%, 50%);--code-property-color:hsl(185, 30%, 50%);--code-value-color:hsl(40, 10%, 50%);--code-comment-color:#4d606d;--accordion-border-color:var(--muted-border-color);--accordion-active-summary-color:var(--primary);--accordion-close-summary-color:var(--color);--accordion-open-summary-color:var(--muted-color);--card-background-color:#141e26;--card-border-color:var(--card-background-color);--card-box-shadow:0.0145rem 0.029rem 0.174rem rgba(0, 0, 0, 0.01698),0.0335rem 0.067rem 0.402rem rgba(0, 0, 0, 0.024),0.0625rem 0.125rem 0.75rem rgba(0, 0, 0, 0.03),0.1125rem 0.225rem 1.35rem rgba(0, 0, 0, 0.036),0.2085rem 0.417rem 2.502rem rgba(0, 0, 0, 0.04302),0.5rem 1rem 6rem rgba(0, 0, 0, 0.06),0 0 0 0.0625rem rgba(0, 0, 0, 0.015);--card-sectionning-background-color:#18232c;--dropdown-background-color:hsl(205, 30%, 15%);--dropdown-border-color:#24333e;--dropdown-box-shadow:var(--card-box-shadow);--dropdown-color:var(--color);--dropdown-hover-background-color:rgba(36, 51, 62, 0.75);--modal-overlay-background-color:rgba(36, 51, 62, 0.8);--progress-background-color:#24333e;--progress-color:var(--primary);--loading-spinner-opacity:0.5;--tooltip-background-color:var(--contrast);--tooltip-color:var(--contrast-inverse);--icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button-inverse:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(0, 0, 0)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(115, 130, 140)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(183, 28, 28)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");--icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(46, 125, 50)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");color-scheme:dark}[type=checkbox],[type=radio],[type=range],progress{accent-color:var(--primary)}*,::after,::before{box-sizing:border-box;background-repeat:no-repeat}::after,::before{text-decoration:inherit;vertical-align:inherit}:where(:root){-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;background-color:var(--background-color);color:var(--color);font-weight:var(--font-weight);font-size:var(--font-size);line-height:var(--line-height);font-family:var(--font-family);text-rendering:optimizeLegibility;overflow-wrap:break-word;cursor:default;-moz-tab-size:4;-o-tab-size:4;tab-size:4}main{display:block}body{width:100%;margin:0}body>footer,body>header,body>main{width:100%;margin-right:auto;margin-left:auto;padding:var(--block-spacing-vertical) 0}.container,.container-fluid{width:100%;margin-right:auto;margin-left:auto;padding-right:var(--spacing);padding-left:var(--spacing)}@media (min-width:576px){.container{max-width:510px;padding-right:0;padding-left:0}}@media (min-width:768px){.container{max-width:700px}}@media (min-width:992px){.container{max-width:920px}}@media (min-width:1200px){.container{max-width:1130px}}section{margin-bottom:var(--block-spacing-vertical)}.grid{grid-column-gap:var(--grid-spacing-horizontal);grid-row-gap:var(--grid-spacing-vertical);display:grid;grid-template-columns:1fr;margin:0}@media (min-width:992px){.grid{grid-template-columns:repeat(auto-fit,minmax(0%,1fr))}}.grid>*{min-width:0}figure{display:block;margin:0;padding:0;overflow-x:auto}figure figcaption{padding:calc(var(--spacing) * .5) 0;color:var(--muted-color)}b,strong{font-weight:bolder}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}address,blockquote,dl,figure,form,ol,p,pre,table,ul{margin-top:0;margin-bottom:var(--typography-spacing-vertical);color:var(--color);font-style:normal;font-weight:var(--font-weight);font-size:var(--font-size)}[role=link],a{--color:var(--primary);--background-color:transparent;outline:0;background-color:var(--background-color);color:var(--color);-webkit-text-decoration:var(--text-decoration);text-decoration:var(--text-decoration);transition:background-color var(--transition),color var(--transition),box-shadow var(--transition),-webkit-text-decoration var(--transition);transition:background-color var(--transition),color var(--transition),text-decoration var(--transition),box-shadow var(--transition);transition:background-color var(--transition),color var(--transition),text-decoration var(--transition),box-shadow var(--transition),-webkit-text-decoration var(--transition)}[role=link]:is([aria-current],:hover,:active,:focus),a:is([aria-current],:hover,:active,:focus){--color:var(--primary-hover);--text-decoration:underline}[role=link]:focus,a:focus{--background-color:var(--primary-focus)}[role=link].secondary,a.secondary{--color:var(--secondary)}[role=link].secondary:is([aria-current],:hover,:active,:focus),a.secondary:is([aria-current],:hover,:active,:focus){--color:var(--secondary-hover)}[role=link].secondary:focus,a.secondary:focus{--background-color:var(--secondary-focus)}[role=link].contrast,a.contrast{--color:var(--contrast)}[role=link].contrast:is([aria-current],:hover,:active,:focus),a.contrast:is([aria-current],:hover,:active,:focus){--color:var(--contrast-hover)}[role=link].contrast:focus,a.contrast:focus{--background-color:var(--contrast-focus)}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:var(--typography-spacing-vertical);color:var(--color);font-weight:var(--font-weight);font-size:var(--font-size);font-family:var(--font-family)}h1{--color:var(--h1-color)}h2{--color:var(--h2-color)}h3{--color:var(--h3-color)}h4{--color:var(--h4-color)}h5{--color:var(--h5-color)}h6{--color:var(--h6-color)}:where(address,blockquote,dl,figure,form,ol,p,pre,table,ul)~:is(h1,h2,h3,h4,h5,h6){margin-top:var(--typography-spacing-vertical)}.headings,hgroup{margin-bottom:var(--typography-spacing-vertical)}.headings>*,hgroup>*{margin-bottom:0}.headings>:last-child,hgroup>:last-child{--color:var(--muted-color);--font-weight:unset;font-size:1rem;font-family:unset}p{margin-bottom:var(--typography-spacing-vertical)}small{font-size:var(--font-size)}:where(dl,ol,ul){padding-right:0;padding-left:var(--spacing);padding-inline-start:var(--spacing);padding-inline-end:0}:where(dl,ol,ul) li{margin-bottom:calc(var(--typography-spacing-vertical) * .25)}:where(dl,ol,ul) :is(dl,ol,ul){margin:0;margin-top:calc(var(--typography-spacing-vertical) * .25)}ul li{list-style:square}mark{padding:.125rem .25rem;background-color:var(--mark-background-color);color:var(--mark-color);vertical-align:baseline}blockquote{display:block;margin:var(--typography-spacing-vertical) 0;padding:var(--spacing);border-right:none;border-left:.25rem solid var(--blockquote-border-color);border-inline-start:0.25rem solid var(--blockquote-border-color);border-inline-end:none}blockquote footer{margin-top:calc(var(--typography-spacing-vertical) * .5);color:var(--blockquote-footer-color)}abbr[title]{border-bottom:1px dotted;text-decoration:none;cursor:help}ins{color:var(--ins-color);text-decoration:none}del{color:var(--del-color)}::-moz-selection{background-color:var(--primary-focus)}::selection{background-color:var(--primary-focus)}:where(audio,canvas,iframe,img,svg,video){vertical-align:middle}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}:where(iframe){border-style:none}img{max-width:100%;height:auto;border-style:none}:where(svg:not([fill])){fill:currentColor}svg:not(:root){overflow:hidden}button{margin:0;overflow:visible;font-family:inherit;text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}button{display:block;width:100%;margin-bottom:var(--spacing)}[role=button]{display:inline-block;text-decoration:none}[role=button],button,input[type=button],input[type=reset],input[type=submit]{--background-color:var(--primary);--border-color:var(--primary);--color:var(--primary-inverse);--box-shadow:var(--button-box-shadow, 0 0 0 rgba(0, 0, 0, 0));padding:var(--form-element-spacing-vertical) var(--form-element-spacing-horizontal);border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);font-size:1rem;line-height:var(--line-height);text-align:center;cursor:pointer;transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}[role=button]:is([aria-current],:hover,:active,:focus),button:is([aria-current],:hover,:active,:focus),input[type=button]:is([aria-current],:hover,:active,:focus),input[type=reset]:is([aria-current],:hover,:active,:focus),input[type=submit]:is([aria-current],:hover,:active,:focus){--background-color:var(--primary-hover);--border-color:var(--primary-hover);--box-shadow:var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0));--color:var(--primary-inverse)}[role=button]:focus,button:focus,input[type=button]:focus,input[type=reset]:focus,input[type=submit]:focus{--box-shadow:var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--outline-width) var(--primary-focus)}:is(button,input[type=submit],input[type=button],[role=button]).secondary,input[type=reset]{--background-color:var(--secondary);--border-color:var(--secondary);--color:var(--secondary-inverse);cursor:pointer}:is(button,input[type=submit],input[type=button],[role=button]).secondary:is([aria-current],:hover,:active,:focus),input[type=reset]:is([aria-current],:hover,:active,:focus){--background-color:var(--secondary-hover);--border-color:var(--secondary-hover);--color:var(--secondary-inverse)}:is(button,input[type=submit],input[type=button],[role=button]).secondary:focus,input[type=reset]:focus{--box-shadow:var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--outline-width) var(--secondary-focus)}:is(button,input[type=submit],input[type=button],[role=button]).contrast{--background-color:var(--contrast);--border-color:var(--contrast);--color:var(--contrast-inverse)}:is(button,input[type=submit],input[type=button],[role=button]).contrast:is([aria-current],:hover,:active,:focus){--background-color:var(--contrast-hover);--border-color:var(--contrast-hover);--color:var(--contrast-inverse)}:is(button,input[type=submit],input[type=button],[role=button]).contrast:focus{--box-shadow:var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--outline-width) var(--contrast-focus)}:is(button,input[type=submit],input[type=button],[role=button]).outline,input[type=reset].outline{--background-color:transparent;--color:var(--primary)}:is(button,input[type=submit],input[type=button],[role=button]).outline:is([aria-current],:hover,:active,:focus),input[type=reset].outline:is([aria-current],:hover,:active,:focus){--background-color:transparent;--color:var(--primary-hover)}:is(button,input[type=submit],input[type=button],[role=button]).outline.secondary,input[type=reset].outline{--color:var(--secondary)}:is(button,input[type=submit],input[type=button],[role=button]).outline.secondary:is([aria-current],:hover,:active,:focus),input[type=reset].outline:is([aria-current],:hover,:active,:focus){--color:var(--secondary-hover)}:is(button,input[type=submit],input[type=button],[role=button]).outline.contrast{--color:var(--contrast)}:is(button,input[type=submit],input[type=button],[role=button]).outline.contrast:is([aria-current],:hover,:active,:focus){--color:var(--contrast-hover)}:where(button,[type=submit],[type=button],[type=reset],[role=button])[disabled],:where(fieldset[disabled]) :is(button,[type=submit],[type=button],[type=reset],[role=button]),a[role=button]:not([href]){opacity:.5;pointer-events:none}input,optgroup,select,textarea{margin:0;font-size:1rem;line-height:var(--line-height);font-family:inherit;letter-spacing:inherit}input{overflow:visible}select{text-transform:none}legend{max-width:100%;padding:0;color:inherit;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio]{padding:0}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}::-moz-focus-inner{padding:0;border-style:none}:-moz-focusring{outline:0}:-moz-ui-invalid{box-shadow:none}::-ms-expand{display:none}[type=file],[type=range]{padding:0;border-width:0}input:not([type=checkbox],[type=radio],[type=range]){height:calc(1rem * var(--line-height) + var(--form-element-spacing-vertical) * 2 + var(--border-width) * 2)}fieldset{margin:0;margin-bottom:var(--spacing);padding:0;border:0}fieldset legend,label{display:block;margin-bottom:calc(var(--spacing) * .25);font-weight:var(--form-label-font-weight,var(--font-weight))}input:not([type=checkbox],[type=radio]),select,textarea{width:100%}input:not([type=checkbox],[type=radio],[type=range],[type=file]),select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:var(--form-element-spacing-vertical) var(--form-element-spacing-horizontal)}input,select,textarea{--background-color:var(--form-element-background-color);--border-color:var(--form-element-border-color);--color:var(--form-element-color);--box-shadow:none;border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}:where(select,textarea):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[readonly]):is(:active,:focus){--background-color:var(--form-element-active-background-color)}:where(select,textarea):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[role=switch],[readonly]):is(:active,:focus){--border-color:var(--form-element-active-border-color)}input:not([type=submit],[type=button],[type=reset],[type=range],[type=file],[readonly]):focus,select:focus,textarea:focus{--box-shadow:0 0 0 var(--outline-width) var(--form-element-focus-color)}:where(fieldset[disabled]) :is(input:not([type=submit],[type=button],[type=reset]),select,textarea),input:not([type=submit],[type=button],[type=reset])[disabled],select[disabled],textarea[disabled]{--background-color:var(--form-element-disabled-background-color);--border-color:var(--form-element-disabled-border-color);opacity:var(--form-element-disabled-opacity);pointer-events:none}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week])[aria-invalid]{padding-right:calc(var(--form-element-spacing-horizontal) + 1.5rem)!important;padding-left:var(--form-element-spacing-horizontal);padding-inline-start:var(--form-element-spacing-horizontal)!important;padding-inline-end:calc(var(--form-element-spacing-horizontal) + 1.5rem)!important;background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week])[aria-invalid=false]{background-image:var(--icon-valid)}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week])[aria-invalid=true]{background-image:var(--icon-invalid)}:where(input,select,textarea)[aria-invalid=false]{--border-color:var(--form-element-valid-border-color)}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus){--border-color:var(--form-element-valid-active-border-color)!important;--box-shadow:0 0 0 var(--outline-width) var(--form-element-valid-focus-color)!important}:where(input,select,textarea)[aria-invalid=true]{--border-color:var(--form-element-invalid-border-color)}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus){--border-color:var(--form-element-invalid-active-border-color)!important;--box-shadow:0 0 0 var(--outline-width) var(--form-element-invalid-focus-color)!important}[dir=rtl] :where(input,select,textarea):not([type=checkbox],[type=radio]):is([aria-invalid],[aria-invalid=true],[aria-invalid=false]){background-position:center left .75rem}input::-webkit-input-placeholder,input::placeholder,select:invalid,textarea::-webkit-input-placeholder,textarea::placeholder{color:var(--form-element-placeholder-color);opacity:1}input:not([type=checkbox],[type=radio]),select,textarea{margin-bottom:var(--spacing)}select::-ms-expand{border:0;background-color:transparent}select:not([multiple],[size]){padding-right:calc(var(--form-element-spacing-horizontal) + 1.5rem);padding-left:var(--form-element-spacing-horizontal);padding-inline-start:var(--form-element-spacing-horizontal);padding-inline-end:calc(var(--form-element-spacing-horizontal) + 1.5rem);background-image:var(--icon-chevron);background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}[dir=rtl] select:not([multiple],[size]){background-position:center left .75rem}:where(input,select,textarea,.grid)+small{display:block;width:100%;margin-top:calc(var(--spacing) * -.75);margin-bottom:var(--spacing);color:var(--muted-color)}label>:where(input,select,textarea){margin-top:calc(var(--spacing) * .25)}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:1.25em;height:1.25em;margin-top:-.125em;margin-right:.375em;margin-left:0;margin-inline-start:0;margin-inline-end:.375em;border-width:var(--border-width);font-size:inherit;vertical-align:middle;cursor:pointer}[type=checkbox]::-ms-check,[type=radio]::-ms-check{display:none}[type=checkbox]:checked,[type=checkbox]:checked:active,[type=checkbox]:checked:focus,[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--background-color:var(--primary);--border-color:var(--primary);background-image:var(--icon-checkbox);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=checkbox]~label,[type=radio]~label{display:inline-block;margin-right:.375em;margin-bottom:0;cursor:pointer}[type=checkbox]:indeterminate{--background-color:var(--primary);--border-color:var(--primary);background-image:var(--icon-minus);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=radio]{border-radius:50%}[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--background-color:var(--primary-inverse);border-width:.35em;background-image:none}[type=checkbox][role=switch]{--background-color:var(--switch-background-color);--border-color:var(--switch-background-color);--color:var(--switch-color);width:2.25em;height:1.25em;border:var(--border-width) solid var(--border-color);border-radius:1.25em;background-color:var(--background-color);line-height:1.25em}[type=checkbox][role=switch]:focus{--background-color:var(--switch-background-color);--border-color:var(--switch-background-color)}[type=checkbox][role=switch]:checked{--background-color:var(--switch-checked-background-color);--border-color:var(--switch-checked-background-color)}[type=checkbox][role=switch]:before{display:block;width:calc(1.25em - (var(--border-width) * 2));height:100%;border-radius:50%;background-color:var(--color);content:"";transition:margin .1s ease-in-out}[type=checkbox][role=switch]:checked{background-image:none}[type=checkbox][role=switch]:checked::before{margin-left:calc(1.125em - var(--border-width));margin-inline-start:calc(1.125em - var(--border-width))}[type=checkbox]:checked[aria-invalid=false],[type=checkbox][aria-invalid=false],[type=checkbox][role=switch]:checked[aria-invalid=false],[type=checkbox][role=switch][aria-invalid=false],[type=radio]:checked[aria-invalid=false],[type=radio][aria-invalid=false]{--border-color:var(--form-element-valid-border-color)}[type=checkbox]:checked[aria-invalid=true],[type=checkbox][aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true],[type=checkbox][role=switch][aria-invalid=true],[type=radio]:checked[aria-invalid=true],[type=radio][aria-invalid=true]{--border-color:var(--form-element-invalid-border-color)}[type=color]::-webkit-color-swatch-wrapper{padding:0}[type=color]::-moz-focus-inner{padding:0}[type=color]::-webkit-color-swatch{border:0;border-radius:calc(var(--border-radius) * .5)}[type=color]::-moz-color-swatch{border:0;border-radius:calc(var(--border-radius) * .5)}input:not([type=checkbox],[type=radio],[type=range],[type=file]):is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){--icon-position:0.75rem;--icon-width:1rem;padding-right:calc(var(--icon-width) + var(--icon-position));background-image:var(--icon-date);background-position:center right var(--icon-position);background-size:var(--icon-width) auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=time]{background-image:var(--icon-time)}[type=date]::-webkit-calendar-picker-indicator,[type=datetime-local]::-webkit-calendar-picker-indicator,[type=month]::-webkit-calendar-picker-indicator,[type=time]::-webkit-calendar-picker-indicator,[type=week]::-webkit-calendar-picker-indicator{width:var(--icon-width);margin-right:calc(var(--icon-width) * -1);margin-left:var(--icon-position);opacity:0}[dir=rtl] :is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){text-align:right}@-moz-document url-prefix(){[type=date],[type=datetime-local],[type=month],[type=time],[type=week]{padding-right:var(--form-element-spacing-horizontal)!important;background-image:none!important}}[type=file]{--color:var(--muted-color);padding:calc(var(--form-element-spacing-vertical) * .5) 0;border:0;border-radius:0;background:0 0}[type=file]::file-selector-button{--background-color:var(--secondary);--border-color:var(--secondary);--color:var(--secondary-inverse);margin-right:calc(var(--spacing)/ 2);margin-left:0;margin-inline-start:0;margin-inline-end:calc(var(--spacing)/ 2);padding:calc(var(--form-element-spacing-vertical) * .5) calc(var(--form-element-spacing-horizontal) * .5);border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);font-size:1rem;line-height:var(--line-height);text-align:center;cursor:pointer;transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}[type=file]::file-selector-button:is(:hover,:active,:focus){--background-color:var(--secondary-hover);--border-color:var(--secondary-hover)}[type=file]::-webkit-file-upload-button{--background-color:var(--secondary);--border-color:var(--secondary);--color:var(--secondary-inverse);margin-right:calc(var(--spacing)/ 2);margin-left:0;margin-inline-start:0;margin-inline-end:calc(var(--spacing)/ 2);padding:calc(var(--form-element-spacing-vertical) * .5) calc(var(--form-element-spacing-horizontal) * .5);border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);font-size:1rem;line-height:var(--line-height);text-align:center;cursor:pointer;-webkit-transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}[type=file]::-webkit-file-upload-button:is(:hover,:active,:focus){--background-color:var(--secondary-hover);--border-color:var(--secondary-hover)}[type=file]::-ms-browse{--background-color:var(--secondary);--border-color:var(--secondary);--color:var(--secondary-inverse);margin-right:calc(var(--spacing)/ 2);margin-left:0;margin-inline-start:0;margin-inline-end:calc(var(--spacing)/ 2);padding:calc(var(--form-element-spacing-vertical) * .5) calc(var(--form-element-spacing-horizontal) * .5);border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);font-size:1rem;line-height:var(--line-height);text-align:center;cursor:pointer;-ms-transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}[type=file]::-ms-browse:is(:hover,:active,:focus){--background-color:var(--secondary-hover);--border-color:var(--secondary-hover)}[type=range]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:1.25rem;background:0 0}[type=range]::-webkit-slider-runnable-track{width:100%;height:.25rem;border-radius:var(--border-radius);background-color:var(--range-border-color);-webkit-transition:background-color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),box-shadow var(--transition)}[type=range]::-moz-range-track{width:100%;height:.25rem;border-radius:var(--border-radius);background-color:var(--range-border-color);-moz-transition:background-color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),box-shadow var(--transition)}[type=range]::-ms-track{width:100%;height:.25rem;border-radius:var(--border-radius);background-color:var(--range-border-color);-ms-transition:background-color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),box-shadow var(--transition)}[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.5rem;border:2px solid var(--range-thumb-border-color);border-radius:50%;background-color:var(--range-thumb-color);cursor:pointer;-webkit-transition:background-color var(--transition),transform var(--transition);transition:background-color var(--transition),transform var(--transition)}[type=range]::-moz-range-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.5rem;border:2px solid var(--range-thumb-border-color);border-radius:50%;background-color:var(--range-thumb-color);cursor:pointer;-moz-transition:background-color var(--transition),transform var(--transition);transition:background-color var(--transition),transform var(--transition)}[type=range]::-ms-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.5rem;border:2px solid var(--range-thumb-border-color);border-radius:50%;background-color:var(--range-thumb-color);cursor:pointer;-ms-transition:background-color var(--transition),transform var(--transition);transition:background-color var(--transition),transform var(--transition)}[type=range]:focus,[type=range]:hover{--range-border-color:var(--range-active-border-color);--range-thumb-color:var(--range-thumb-hover-color)}[type=range]:active{--range-thumb-color:var(--range-thumb-active-color)}[type=range]:active::-webkit-slider-thumb{transform:scale(1.25)}[type=range]:active::-moz-range-thumb{transform:scale(1.25)}[type=range]:active::-ms-thumb{transform:scale(1.25)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{padding-inline-start:calc(var(--form-element-spacing-horizontal) + 1.75rem);border-radius:5rem;background-image:var(--icon-search);background-position:center left 1.125rem;background-size:1rem auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{padding-inline-start:calc(var(--form-element-spacing-horizontal) + 1.75rem)!important;background-position:center left 1.125rem,center right .75rem}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=false]{background-image:var(--icon-search),var(--icon-valid)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=true]{background-image:var(--icon-search),var(--icon-invalid)}[type=search]::-webkit-search-cancel-button{-webkit-appearance:none;display:none}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{background-position:center right 1.125rem}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{background-position:center right 1.125rem,center left .75rem}:where(table){width:100%;border-collapse:collapse;border-spacing:0;text-indent:0}td,th{padding:calc(var(--spacing)/ 2) var(--spacing);border-bottom:var(--border-width) solid var(--table-border-color);color:var(--color);font-weight:var(--font-weight);font-size:var(--font-size);text-align:left;text-align:start}tfoot td,tfoot th{border-top:var(--border-width) solid var(--table-border-color);border-bottom:0}table[role=grid] tbody tr:nth-child(odd){background-color:var(--table-row-stripped-background-color)}code,kbd,pre,samp{font-size:.875em;font-family:var(--font-family)}pre{-ms-overflow-style:scrollbar;overflow:auto}code,kbd,pre{border-radius:var(--border-radius);background:var(--code-background-color);color:var(--code-color);font-weight:var(--font-weight);line-height:initial}code,kbd{display:inline-block;padding:.375rem .5rem}pre{display:block;margin-bottom:var(--spacing);overflow-x:auto}pre>code{display:block;padding:var(--spacing);background:0 0;font-size:14px;line-height:var(--line-height)}code b{color:var(--code-tag-color);font-weight:var(--font-weight)}code i{color:var(--code-property-color);font-style:normal}code u{color:var(--code-value-color);text-decoration:none}code em{color:var(--code-comment-color);font-style:normal}kbd{background-color:var(--code-kbd-background-color);color:var(--code-kbd-color);vertical-align:baseline}hr{height:0;border:0;border-top:1px solid var(--muted-border-color);color:inherit}[hidden],template{display:none!important}canvas{display:inline-block}details{display:block;margin-bottom:var(--spacing);padding-bottom:var(--spacing);border-bottom:var(--border-width) solid var(--accordion-border-color)}details summary{line-height:1rem;list-style-type:none;cursor:pointer;transition:color var(--transition)}details summary:not([role]){color:var(--accordion-close-summary-color)}details summary::-webkit-details-marker{display:none}details summary::marker{display:none}details summary::-moz-list-bullet{list-style-type:none}details summary::after{display:block;width:1rem;height:1rem;margin-inline-start:calc(var(--spacing,1rem) * .5);float:right;transform:rotate(-90deg);background-image:var(--icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:"";transition:transform var(--transition)}details summary:focus{outline:0}details summary:focus:not([role=button]){color:var(--accordion-active-summary-color)}details summary[role=button]{width:100%;text-align:left}details summary[role=button]::after{height:calc(1rem * var(--line-height,1.5));background-image:var(--icon-chevron-button)}details summary[role=button]:not(.outline).contrast::after{background-image:var(--icon-chevron-button-inverse)}details[open]>summary{margin-bottom:calc(var(--spacing))}details[open]>summary:not([role]):not(:focus){color:var(--accordion-open-summary-color)}details[open]>summary::after{transform:rotate(0)}[dir=rtl] details summary{text-align:right}[dir=rtl] details summary::after{float:left;background-position:left center}article{margin:var(--block-spacing-vertical) 0;padding:var(--block-spacing-vertical) var(--block-spacing-horizontal);border-radius:var(--border-radius);background:var(--card-background-color);box-shadow:var(--card-box-shadow)}article>footer,article>header{margin-right:calc(var(--block-spacing-horizontal) * -1);margin-left:calc(var(--block-spacing-horizontal) * -1);padding:calc(var(--block-spacing-vertical) * .66) var(--block-spacing-horizontal);background-color:var(--card-sectionning-background-color)}article>header{margin-top:calc(var(--block-spacing-vertical) * -1);margin-bottom:var(--block-spacing-vertical);border-bottom:var(--border-width) solid var(--card-border-color);border-top-right-radius:var(--border-radius);border-top-left-radius:var(--border-radius)}article>footer{margin-top:var(--block-spacing-vertical);margin-bottom:calc(var(--block-spacing-vertical) * -1);border-top:var(--border-width) solid var(--card-border-color);border-bottom-right-radius:var(--border-radius);border-bottom-left-radius:var(--border-radius)}:root{--scrollbar-width:0px}dialog{display:flex;z-index:999;position:fixed;top:0;right:0;bottom:0;left:0;align-items:center;justify-content:center;width:inherit;min-width:100%;height:inherit;min-height:100%;padding:var(--spacing);border:0;-webkit-backdrop-filter:var(--modal-overlay-backdrop-filter);backdrop-filter:var(--modal-overlay-backdrop-filter);background-color:var(--modal-overlay-background-color);color:var(--color)}dialog article{max-height:calc(100vh - var(--spacing) * 2);overflow:auto}@media (min-width:576px){dialog article{max-width:510px}}@media (min-width:768px){dialog article{max-width:700px}}dialog article>footer,dialog article>header{padding:calc(var(--block-spacing-vertical) * .5) var(--block-spacing-horizontal)}dialog article>header .close{margin:0;margin-left:var(--spacing);float:right}dialog article>footer{text-align:right}dialog article>footer [role=button]{margin-bottom:0}dialog article>footer [role=button]:not(:first-of-type){margin-left:calc(var(--spacing) * .5)}dialog article p:last-of-type{margin:0}dialog article .close{display:block;width:1rem;height:1rem;margin-top:calc(var(--block-spacing-vertical) * -.5);margin-bottom:var(--typography-spacing-vertical);margin-left:auto;background-image:var(--icon-close);background-position:center;background-size:auto 1rem;background-repeat:no-repeat;opacity:.5;transition:opacity var(--transition)}dialog article .close:is([aria-current],:hover,:active,:focus){opacity:1}dialog:not([open]),dialog[open=false]{display:none}.modal-is-open{padding-right:var(--scrollbar-width,0);overflow:hidden;pointer-events:none;touch-action:none}.modal-is-open dialog{pointer-events:auto}:where(.modal-is-opening,.modal-is-closing) dialog,:where(.modal-is-opening,.modal-is-closing) dialog>article{animation-duration:.2s;animation-timing-function:ease-in-out;animation-fill-mode:both}:where(.modal-is-opening,.modal-is-closing) dialog{animation-duration:.8s;animation-name:modal-overlay}:where(.modal-is-opening,.modal-is-closing) dialog>article{animation-delay:.2s;animation-name:modal}.modal-is-closing dialog,.modal-is-closing dialog>article{animation-delay:0s;animation-direction:reverse}@keyframes modal-overlay{from{-webkit-backdrop-filter:none;backdrop-filter:none;background-color:transparent}}@keyframes modal{from{transform:translateY(-100%);opacity:0}}:where(nav li)::before{float:left;content:"​"}nav,nav ul{display:flex}nav{justify-content:space-between}nav ol,nav ul{align-items:center;margin-bottom:0;padding:0;list-style:none}nav ol:first-of-type,nav ul:first-of-type{margin-left:calc(var(--nav-element-spacing-horizontal) * -1)}nav ol:last-of-type,nav ul:last-of-type{margin-right:calc(var(--nav-element-spacing-horizontal) * -1)}nav li{display:inline-block;margin:0;padding:var(--nav-element-spacing-vertical) var(--nav-element-spacing-horizontal)}nav li>*{--spacing:0}nav :where(a,[role=link]){display:inline-block;margin:calc(var(--nav-link-spacing-vertical) * -1) calc(var(--nav-link-spacing-horizontal) * -1);padding:var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal);border-radius:var(--border-radius);text-decoration:none}nav :where(a,[role=link]):is([aria-current],:hover,:active,:focus){text-decoration:none}nav[aria-label=breadcrumb]{align-items:center;justify-content:start}nav[aria-label=breadcrumb] ul li:not(:first-child){margin-inline-start:var(--nav-link-spacing-horizontal)}nav[aria-label=breadcrumb] ul li:not(:last-child) ::after{position:absolute;width:calc(var(--nav-link-spacing-horizontal) * 2);margin-inline-start:calc(var(--nav-link-spacing-horizontal)/ 2);content:"/";color:var(--muted-color);text-align:center}nav[aria-label=breadcrumb] a[aria-current]{background-color:transparent;color:inherit;text-decoration:none;pointer-events:none}nav [role=button]{margin-right:inherit;margin-left:inherit;padding:var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal)}aside li,aside nav,aside ol,aside ul{display:block}aside li{padding:calc(var(--nav-element-spacing-vertical) * .5) var(--nav-element-spacing-horizontal)}aside li a{display:block}aside li [role=button]{margin:inherit}[dir=rtl] nav[aria-label=breadcrumb] ul li:not(:last-child) ::after{content:"\\"}progress{display:inline-block;vertical-align:baseline}progress{-webkit-appearance:none;-moz-appearance:none;display:inline-block;appearance:none;width:100%;height:.5rem;margin-bottom:calc(var(--spacing) * .5);overflow:hidden;border:0;border-radius:var(--border-radius);background-color:var(--progress-background-color);color:var(--progress-color)}progress::-webkit-progress-bar{border-radius:var(--border-radius);background:0 0}progress[value]::-webkit-progress-value{background-color:var(--progress-color)}progress::-moz-progress-bar{background-color:var(--progress-color)}@media (prefers-reduced-motion:no-preference){progress:indeterminate{background:var(--progress-background-color) linear-gradient(to right,var(--progress-color) 30%,var(--progress-background-color) 30%) top left/150% 150% no-repeat;animation:progress-indeterminate 1s linear infinite}progress:indeterminate[value]::-webkit-progress-value{background-color:transparent}progress:indeterminate::-moz-progress-bar{background-color:transparent}}@media (prefers-reduced-motion:no-preference){[dir=rtl] progress:indeterminate{animation-direction:reverse}}@keyframes progress-indeterminate{0%{background-position:200% 0}100%{background-position:-200% 0}}details[role=list],li[role=list]{position:relative}details[role=list] summary+ul,li[role=list]>ul{display:flex;z-index:99;position:absolute;top:auto;right:0;left:0;flex-direction:column;margin:0;padding:0;border:var(--border-width) solid var(--dropdown-border-color);border-radius:var(--border-radius);border-top-right-radius:0;border-top-left-radius:0;background-color:var(--dropdown-background-color);box-shadow:var(--card-box-shadow);color:var(--dropdown-color);white-space:nowrap}details[role=list] summary+ul li,li[role=list]>ul li{width:100%;margin-bottom:0;padding:calc(var(--form-element-spacing-vertical) * .5) var(--form-element-spacing-horizontal);list-style:none}details[role=list] summary+ul li:first-of-type,li[role=list]>ul li:first-of-type{margin-top:calc(var(--form-element-spacing-vertical) * .5)}details[role=list] summary+ul li:last-of-type,li[role=list]>ul li:last-of-type{margin-bottom:calc(var(--form-element-spacing-vertical) * .5)}details[role=list] summary+ul li a,li[role=list]>ul li a{display:block;margin:calc(var(--form-element-spacing-vertical) * -.5) calc(var(--form-element-spacing-horizontal) * -1);padding:calc(var(--form-element-spacing-vertical) * .5) var(--form-element-spacing-horizontal);overflow:hidden;color:var(--dropdown-color);text-decoration:none;text-overflow:ellipsis}details[role=list] summary+ul li a:hover,li[role=list]>ul li a:hover{background-color:var(--dropdown-hover-background-color)}details[role=list] summary::after,li[role=list]>a::after{display:block;width:1rem;height:calc(1rem * var(--line-height,1.5));margin-inline-start:.5rem;float:right;transform:rotate(0);background-image:var(--icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:""}details[role=list]{padding:0;border-bottom:none}details[role=list] summary{margin-bottom:0}details[role=list] summary:not([role]){height:calc(1rem * var(--line-height) + var(--form-element-spacing-vertical) * 2 + var(--border-width) * 2);padding:var(--form-element-spacing-vertical) var(--form-element-spacing-horizontal);border:var(--border-width) solid var(--form-element-border-color);border-radius:var(--border-radius);background-color:var(--form-element-background-color);color:var(--form-element-placeholder-color);line-height:inherit;cursor:pointer;transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}details[role=list] summary:not([role]):active,details[role=list] summary:not([role]):focus{border-color:var(--form-element-active-border-color);background-color:var(--form-element-active-background-color)}details[role=list] summary:not([role]):focus{box-shadow:0 0 0 var(--outline-width) var(--form-element-focus-color)}details[role=list][open] summary{border-bottom-right-radius:0;border-bottom-left-radius:0}details[role=list][open] summary::before{display:block;z-index:1;position:fixed;top:0;right:0;bottom:0;left:0;background:0 0;content:"";cursor:default}nav details[role=list] summary,nav li[role=list] a{display:flex;direction:ltr}nav details[role=list] summary+ul,nav li[role=list]>ul{min-width:-moz-fit-content;min-width:fit-content;border-radius:var(--border-radius)}nav details[role=list] summary+ul li a,nav li[role=list]>ul li a{border-radius:0}nav details[role=list] summary,nav details[role=list] summary:not([role]){height:auto;padding:var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal)}nav details[role=list][open] summary{border-radius:var(--border-radius)}nav details[role=list] summary+ul{margin-top:var(--outline-width);margin-inline-start:0}nav details[role=list] summary[role=link]{margin-bottom:calc(var(--nav-link-spacing-vertical) * -1);line-height:var(--line-height)}nav details[role=list] summary[role=link]+ul{margin-top:calc(var(--nav-link-spacing-vertical) + var(--outline-width));margin-inline-start:calc(var(--nav-link-spacing-horizontal) * -1)}li[role=list] a:active~ul,li[role=list] a:focus~ul,li[role=list]:hover>ul{display:flex}li[role=list]>ul{display:none;margin-top:calc(var(--nav-link-spacing-vertical) + var(--outline-width));margin-inline-start:calc(var(--nav-element-spacing-horizontal) - var(--nav-link-spacing-horizontal))}li[role=list]>a::after{background-image:var(--icon-chevron)}label>details[role=list]{margin-top:calc(var(--spacing) * .25);margin-bottom:var(--spacing)}[aria-busy=true]{cursor:progress}[aria-busy=true]:not(input,select,textarea,html)::before{display:inline-block;width:1em;height:1em;border:.1875em solid currentColor;border-radius:1em;border-right-color:transparent;content:"";vertical-align:text-bottom;vertical-align:-.125em;animation:spinner .75s linear infinite;opacity:var(--loading-spinner-opacity)}[aria-busy=true]:not(input,select,textarea,html):not(:empty)::before{margin-right:calc(var(--spacing) * .5);margin-left:0;margin-inline-start:0;margin-inline-end:calc(var(--spacing) * .5)}[aria-busy=true]:not(input,select,textarea,html):empty{text-align:center}a[aria-busy=true],button[aria-busy=true],input[type=button][aria-busy=true],input[type=reset][aria-busy=true],input[type=submit][aria-busy=true]{pointer-events:none}@keyframes spinner{to{transform:rotate(360deg)}}[data-tooltip]{position:relative}[data-tooltip]:not(a,button,input){border-bottom:1px dotted;text-decoration:none;cursor:help}[data-tooltip]::after,[data-tooltip]::before,[data-tooltip][data-placement=top]::after,[data-tooltip][data-placement=top]::before{display:block;z-index:99;position:absolute;bottom:100%;left:50%;padding:.25rem .5rem;overflow:hidden;transform:translate(-50%,-.25rem);border-radius:var(--border-radius);background:var(--tooltip-background-color);content:attr(data-tooltip);color:var(--tooltip-color);font-style:normal;font-weight:var(--font-weight);font-size:.875rem;text-decoration:none;text-overflow:ellipsis;white-space:nowrap;opacity:0;pointer-events:none}[data-tooltip]::after,[data-tooltip][data-placement=top]::after{padding:0;transform:translate(-50%,0);border-top:.3rem solid;border-right:.3rem solid transparent;border-left:.3rem solid transparent;border-radius:0;background-color:transparent;content:"";color:var(--tooltip-background-color)}[data-tooltip][data-placement=bottom]::after,[data-tooltip][data-placement=bottom]::before{top:100%;bottom:auto;transform:translate(-50%,.25rem)}[data-tooltip][data-placement=bottom]:after{transform:translate(-50%,-.3rem);border:.3rem solid transparent;border-bottom:.3rem solid}[data-tooltip][data-placement=left]::after,[data-tooltip][data-placement=left]::before{top:50%;right:100%;bottom:auto;left:auto;transform:translate(-.25rem,-50%)}[data-tooltip][data-placement=left]:after{transform:translate(.3rem,-50%);border:.3rem solid transparent;border-left:.3rem solid}[data-tooltip][data-placement=right]::after,[data-tooltip][data-placement=right]::before{top:50%;right:auto;bottom:auto;left:100%;transform:translate(.25rem,-50%)}[data-tooltip][data-placement=right]:after{transform:translate(-.3rem,-50%);border:.3rem solid transparent;border-right:.3rem solid}[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{opacity:1}@media (hover:hover) and (pointer:fine){[data-tooltip]:hover::after,[data-tooltip]:hover::before,[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:focus::before,[data-tooltip][data-placement=bottom]:hover [data-tooltip]:focus::after,[data-tooltip][data-placement=bottom]:hover [data-tooltip]:focus::before{animation-duration:.2s;animation-name:tooltip-slide-top}[data-tooltip]:hover::after,[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:hover [data-tooltip]:focus::after{animation-name:tooltip-caret-slide-top}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:focus::before,[data-tooltip][data-placement=bottom]:hover::after,[data-tooltip][data-placement=bottom]:hover::before{animation-duration:.2s;animation-name:tooltip-slide-bottom}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:hover::after{animation-name:tooltip-caret-slide-bottom}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:focus::before,[data-tooltip][data-placement=left]:hover::after,[data-tooltip][data-placement=left]:hover::before{animation-duration:.2s;animation-name:tooltip-slide-left}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:hover::after{animation-name:tooltip-caret-slide-left}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:focus::before,[data-tooltip][data-placement=right]:hover::after,[data-tooltip][data-placement=right]:hover::before{animation-duration:.2s;animation-name:tooltip-slide-right}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:hover::after{animation-name:tooltip-caret-slide-right}}@keyframes tooltip-slide-top{from{transform:translate(-50%,.75rem);opacity:0}to{transform:translate(-50%,-.25rem);opacity:1}}@keyframes tooltip-caret-slide-top{from{opacity:0}50%{transform:translate(-50%,-.25rem);opacity:0}to{transform:translate(-50%,0);opacity:1}}@keyframes tooltip-slide-bottom{from{transform:translate(-50%,-.75rem);opacity:0}to{transform:translate(-50%,.25rem);opacity:1}}@keyframes tooltip-caret-slide-bottom{from{opacity:0}50%{transform:translate(-50%,-.5rem);opacity:0}to{transform:translate(-50%,-.3rem);opacity:1}}@keyframes tooltip-slide-left{from{transform:translate(.75rem,-50%);opacity:0}to{transform:translate(-.25rem,-50%);opacity:1}}@keyframes tooltip-caret-slide-left{from{opacity:0}50%{transform:translate(.05rem,-50%);opacity:0}to{transform:translate(.3rem,-50%);opacity:1}}@keyframes tooltip-slide-right{from{transform:translate(-.75rem,-50%);opacity:0}to{transform:translate(.25rem,-50%);opacity:1}}@keyframes tooltip-caret-slide-right{from{opacity:0}50%{transform:translate(-.05rem,-50%);opacity:0}to{transform:translate(-.3rem,-50%);opacity:1}}[aria-controls]{cursor:pointer}[aria-disabled=true],[disabled]{cursor:not-allowed}[aria-hidden=false][hidden]{display:initial}[aria-hidden=false][hidden]:not(:focus){clip:rect(0,0,0,0);position:absolute}[tabindex],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation}[dir=rtl]{direction:rtl}@media (prefers-reduced-motion:reduce){:not([aria-busy=true]),:not([aria-busy=true])::after,:not([aria-busy=true])::before{background-attachment:initial!important;animation-duration:1ms!important;animation-delay:-1ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-delay:0s!important;transition-duration:0s!important}} +/*# sourceMappingURL=pico.min.css.map */ \ No newline at end of file diff --git a/tests/io/test_formats.py b/tests/io/test_formats.py index 725a00ed3..1a1db4eed 100644 --- a/tests/io/test_formats.py +++ b/tests/io/test_formats.py @@ -7,6 +7,9 @@ import pytest import nixio +# Skip the test for MacOS due to QWebEngineView ImportError +pytestmark = pytest.mark.skipif(os.name == "darwin", reason="ImportError for MacOS") + from sleap.io.video import Video from sleap.instance import Instance, LabeledFrame, PredictedInstance, Track from sleap.io.dataset import Labels From 7859b9338e8ddb91c5b4754c6e5b6902889eaf79 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Thu, 11 Jan 2024 00:14:35 -0800 Subject: [PATCH 20/29] test with different os name --- tests/io/test_formats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/io/test_formats.py b/tests/io/test_formats.py index 1a1db4eed..a493d1417 100644 --- a/tests/io/test_formats.py +++ b/tests/io/test_formats.py @@ -8,7 +8,7 @@ import nixio # Skip the test for MacOS due to QWebEngineView ImportError -pytestmark = pytest.mark.skipif(os.name == "darwin", reason="ImportError for MacOS") +pytestmark = pytest.mark.skipif(os.name == "macos", reason="ImportError for MacOS") from sleap.io.video import Video from sleap.instance import Instance, LabeledFrame, PredictedInstance, Track From ec2b2d7961564d695c0b5a7911966c2929aafd37 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Thu, 11 Jan 2024 00:32:37 -0800 Subject: [PATCH 21/29] correct pytest skip --- tests/io/test_formats.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/io/test_formats.py b/tests/io/test_formats.py index a493d1417..7a7b2bff4 100644 --- a/tests/io/test_formats.py +++ b/tests/io/test_formats.py @@ -1,5 +1,6 @@ import os from pathlib import Path, PurePath +import sys import numpy as np import pandas as pd @@ -8,7 +9,9 @@ import nixio # Skip the test for MacOS due to QWebEngineView ImportError -pytestmark = pytest.mark.skipif(os.name == "macos", reason="ImportError for MacOS") +pytestmark = pytest.mark.skipif( + sys.platform == "darwin", reason="ImportError for MacOS" +) from sleap.io.video import Video from sleap.instance import Instance, LabeledFrame, PredictedInstance, Track From 59f2438858397edf20c84731ea0db6162377ad1f Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Thu, 11 Jan 2024 00:51:07 -0800 Subject: [PATCH 22/29] test skip functionality --- tests/io/test_formats.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/io/test_formats.py b/tests/io/test_formats.py index 7a7b2bff4..859d69b8c 100644 --- a/tests/io/test_formats.py +++ b/tests/io/test_formats.py @@ -9,9 +9,11 @@ import nixio # Skip the test for MacOS due to QWebEngineView ImportError -pytestmark = pytest.mark.skipif( - sys.platform == "darwin", reason="ImportError for MacOS" -) +if sys.platform == 'darwin': + pytestmark = pytest.mark.skip( + reason="ImportError for MacOS" + ) + pytest.skip("Skipping tests and import on macOS") from sleap.io.video import Video from sleap.instance import Instance, LabeledFrame, PredictedInstance, Track From bd8d00415cee3a711af1a8cc8b2719be26f17749 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Thu, 11 Jan 2024 01:17:01 -0800 Subject: [PATCH 23/29] test skipping entire module --- tests/io/test_formats.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/io/test_formats.py b/tests/io/test_formats.py index 859d69b8c..5814ddcdc 100644 --- a/tests/io/test_formats.py +++ b/tests/io/test_formats.py @@ -9,11 +9,12 @@ import nixio # Skip the test for MacOS due to QWebEngineView ImportError +# Need to resolve the ImportError if sys.platform == 'darwin': pytestmark = pytest.mark.skip( reason="ImportError for MacOS" ) - pytest.skip("Skipping tests and import on macOS") + pytest.skip(reason="Skipping tests and import on macOS", allow_module_level=True) from sleap.io.video import Video from sleap.instance import Instance, LabeledFrame, PredictedInstance, Track From ea2111d37d72abfefca0247ffd8a186876477d18 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Thu, 11 Jan 2024 01:34:45 -0800 Subject: [PATCH 24/29] skip macos tests --- tests/gui/learning/test_dialog.py | 7 +++++++ tests/gui/test_app.py | 7 +++++++ tests/gui/test_grid_system.py | 8 ++++++++ tests/gui/widgets/test_docks.py | 7 +++++++ tests/io/test_formats.py | 8 +++----- 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/tests/gui/learning/test_dialog.py b/tests/gui/learning/test_dialog.py index 3d77c891f..ee2140e8f 100644 --- a/tests/gui/learning/test_dialog.py +++ b/tests/gui/learning/test_dialog.py @@ -5,8 +5,15 @@ import cattr import pytest +import sys from qtpy import QtWidgets +# Skip the test for MacOS due to QWebEngineView ImportError +# TODO: Need to resolve the ImportError +if sys.platform == "darwin": + pytestmark = pytest.mark.skip(reason="ImportError for MacOS") + pytest.skip(reason="Skipping tests and import on macOS", allow_module_level=True) + from sleap.gui.learning.dialog import LearningDialog, TrainingEditorWidget from sleap.gui.learning.configs import ( TrainingConfigFilesWidget, diff --git a/tests/gui/test_app.py b/tests/gui/test_app.py index ea48b813f..a8afaa5e5 100644 --- a/tests/gui/test_app.py +++ b/tests/gui/test_app.py @@ -1,10 +1,17 @@ import pytest +import sys from qtpy.QtWidgets import QApplication from sleap.gui.app import MainWindow from sleap.gui.commands import * from sleap.gui.dialogs.bulletin import BulletinWorker +# Skip the test for MacOS due to QWebEngineView ImportError +# TODO: Need to resolve the ImportError +if sys.platform == "darwin": + pytestmark = pytest.mark.skip(reason="ImportError for MacOS") + pytest.skip(reason="Skipping tests and import on macOS", allow_module_level=True) + def test_app_workflow( qtbot, centered_pair_vid, small_robot_mp4_vid, min_tracks_2node_labels: Labels diff --git a/tests/gui/test_grid_system.py b/tests/gui/test_grid_system.py index 85419a467..7517b2f80 100644 --- a/tests/gui/test_grid_system.py +++ b/tests/gui/test_grid_system.py @@ -1,7 +1,15 @@ import numpy as np +import pytest +import sys from sleap.gui.app import MainWindow from sleap.gui.commands import * +# Skip the test for MacOS due to QWebEngineView ImportError +# TODO: Need to resolve the ImportError +if sys.platform == "darwin": + pytestmark = pytest.mark.skip(reason="ImportError for MacOS") + pytest.skip(reason="Skipping tests and import on macOS", allow_module_level=True) + def test_grid_system_midpoint_gui(qtbot, midpoint_grid_labels): app = MainWindow(no_usage_data=True) diff --git a/tests/gui/widgets/test_docks.py b/tests/gui/widgets/test_docks.py index 69fe56a56..196fffe88 100644 --- a/tests/gui/widgets/test_docks.py +++ b/tests/gui/widgets/test_docks.py @@ -2,6 +2,7 @@ from pathlib import Path import pytest +import sys from sleap import Labels, Video from sleap.gui.app import MainWindow from sleap.gui.commands import OpenSkeleton @@ -12,6 +13,12 @@ SkeletonDock, ) +# Skip the test for MacOS due to QWebEngineView ImportError +# TODO: Need to resolve the ImportError +if sys.platform == "darwin": + pytestmark = pytest.mark.skip(reason="ImportError for MacOS") + pytest.skip(reason="Skipping tests and import on macOS", allow_module_level=True) + def test_videos_dock( qtbot, diff --git a/tests/io/test_formats.py b/tests/io/test_formats.py index 5814ddcdc..89d27c386 100644 --- a/tests/io/test_formats.py +++ b/tests/io/test_formats.py @@ -9,11 +9,9 @@ import nixio # Skip the test for MacOS due to QWebEngineView ImportError -# Need to resolve the ImportError -if sys.platform == 'darwin': - pytestmark = pytest.mark.skip( - reason="ImportError for MacOS" - ) +# TODO: Need to resolve the ImportError +if sys.platform == "darwin": + pytestmark = pytest.mark.skip(reason="ImportError for MacOS") pytest.skip(reason="Skipping tests and import on macOS", allow_module_level=True) from sleap.io.video import Video From 7ff49f0e15111e93beae13acb4f898752d822cd0 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Thu, 11 Jan 2024 01:48:06 -0800 Subject: [PATCH 25/29] misplaced skip --- tests/gui/test_app.py | 11 ++++++----- tests/gui/test_grid_system.py | 5 +++-- tests/gui/widgets/test_docks.py | 13 +++++++------ 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/tests/gui/test_app.py b/tests/gui/test_app.py index a8afaa5e5..5a99b9cd2 100644 --- a/tests/gui/test_app.py +++ b/tests/gui/test_app.py @@ -1,10 +1,5 @@ import pytest import sys -from qtpy.QtWidgets import QApplication - -from sleap.gui.app import MainWindow -from sleap.gui.commands import * -from sleap.gui.dialogs.bulletin import BulletinWorker # Skip the test for MacOS due to QWebEngineView ImportError # TODO: Need to resolve the ImportError @@ -12,6 +7,12 @@ pytestmark = pytest.mark.skip(reason="ImportError for MacOS") pytest.skip(reason="Skipping tests and import on macOS", allow_module_level=True) +from qtpy.QtWidgets import QApplication + +from sleap.gui.app import MainWindow +from sleap.gui.commands import * +from sleap.gui.dialogs.bulletin import BulletinWorker + def test_app_workflow( qtbot, centered_pair_vid, small_robot_mp4_vid, min_tracks_2node_labels: Labels diff --git a/tests/gui/test_grid_system.py b/tests/gui/test_grid_system.py index 7517b2f80..3d22b3ec8 100644 --- a/tests/gui/test_grid_system.py +++ b/tests/gui/test_grid_system.py @@ -1,8 +1,6 @@ import numpy as np import pytest import sys -from sleap.gui.app import MainWindow -from sleap.gui.commands import * # Skip the test for MacOS due to QWebEngineView ImportError # TODO: Need to resolve the ImportError @@ -10,6 +8,9 @@ pytestmark = pytest.mark.skip(reason="ImportError for MacOS") pytest.skip(reason="Skipping tests and import on macOS", allow_module_level=True) +from sleap.gui.app import MainWindow +from sleap.gui.commands import * + def test_grid_system_midpoint_gui(qtbot, midpoint_grid_labels): app = MainWindow(no_usage_data=True) diff --git a/tests/gui/widgets/test_docks.py b/tests/gui/widgets/test_docks.py index 196fffe88..3cb67229f 100644 --- a/tests/gui/widgets/test_docks.py +++ b/tests/gui/widgets/test_docks.py @@ -3,6 +3,13 @@ from pathlib import Path import pytest import sys + +# Skip the test for MacOS due to QWebEngineView ImportError +# TODO: Need to resolve the ImportError +if sys.platform == "darwin": + pytestmark = pytest.mark.skip(reason="ImportError for MacOS") + pytest.skip(reason="Skipping tests and import on macOS", allow_module_level=True) + from sleap import Labels, Video from sleap.gui.app import MainWindow from sleap.gui.commands import OpenSkeleton @@ -13,12 +20,6 @@ SkeletonDock, ) -# Skip the test for MacOS due to QWebEngineView ImportError -# TODO: Need to resolve the ImportError -if sys.platform == "darwin": - pytestmark = pytest.mark.skip(reason="ImportError for MacOS") - pytest.skip(reason="Skipping tests and import on macOS", allow_module_level=True) - def test_videos_dock( qtbot, From 6ddb1585790094150c3b4b2fcf083ce3f48e005e Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Fri, 12 Jan 2024 11:42:51 -0800 Subject: [PATCH 26/29] having online bulletin for macos --- sleap/gui/app.py | 28 ++++++++++++++++++++-------- tests/gui/learning/test_dialog.py | 7 ------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/sleap/gui/app.py b/sleap/gui/app.py index 21f97ed80..af42acb5f 100644 --- a/sleap/gui/app.py +++ b/sleap/gui/app.py @@ -54,6 +54,7 @@ from pathlib import Path from typing import Callable, List, Optional, Tuple from datetime import date +import webbrowser from qtpy import QtCore, QtGui from qtpy.QtCore import QEvent, Qt @@ -66,7 +67,14 @@ from sleap.gui.dialogs.formbuilder import FormBuilderModalDialog from sleap.gui.dialogs.metrics import MetricsTableDialog from sleap.gui.dialogs.shortcuts import ShortcutDialog -from sleap.gui.dialogs.bulletin import BulletinWorker + +# Open bulletin online if there is an ImportError (for MacOS) +online_bulletin = False +try: + from sleap.gui.dialogs.bulletin import BulletinWorker +except ImportError: + online_bulletin = True + from sleap.gui.overlays.instance import InstanceOverlay from sleap.gui.overlays.tracks import TrackListOverlay, TrackTrailOverlay from sleap.gui.shortcuts import Shortcuts @@ -206,14 +214,18 @@ def __init__( def bulletin_dialog(self): """Displays bulletin dialog is new announcement is available.""" - # Initialize the bulletin popup worker - popup_worker = BulletinWorker( - "".join(["# What's New? \n", self.state["announcement"]]), self - ) - popup_worker.show_bulletin() + # TODO: Change the URL to the actual SLEAP website before merging to main + if online_bulletin: + webbrowser.open('https://sleap.ai/develop/bulletin.html') + else: + # Initialize the bulletin popup worker + popup_worker = BulletinWorker( + "".join(["# What's New? \n", self.state["announcement"]]), self + ) + popup_worker.show_bulletin() - # Save the bulletin worker so we can close them later - self._child_windows["bulletin_worker"] = popup_worker # Needed! + # Save the bulletin worker so we can close them later + self._child_windows["bulletin_worker"] = popup_worker # Needed! def setWindowTitle(self, value): """Sets window title (if value is not None).""" diff --git a/tests/gui/learning/test_dialog.py b/tests/gui/learning/test_dialog.py index ee2140e8f..3d77c891f 100644 --- a/tests/gui/learning/test_dialog.py +++ b/tests/gui/learning/test_dialog.py @@ -5,15 +5,8 @@ import cattr import pytest -import sys from qtpy import QtWidgets -# Skip the test for MacOS due to QWebEngineView ImportError -# TODO: Need to resolve the ImportError -if sys.platform == "darwin": - pytestmark = pytest.mark.skip(reason="ImportError for MacOS") - pytest.skip(reason="Skipping tests and import on macOS", allow_module_level=True) - from sleap.gui.learning.dialog import LearningDialog, TrainingEditorWidget from sleap.gui.learning.configs import ( TrainingConfigFilesWidget, From 895a074a1acb102e9c7aeebd28c2392a6345d7cb Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Fri, 12 Jan 2024 12:32:58 -0800 Subject: [PATCH 27/29] Adjust test functions to include online bulletin --- environment_mac.yml | 2 +- sleap/gui/app.py | 2 +- tests/gui/test_app.py | 27 +++++++++++++-------------- tests/gui/test_grid_system.py | 9 --------- tests/gui/widgets/test_docks.py | 8 -------- tests/io/test_formats.py | 7 ------- 6 files changed, 15 insertions(+), 40 deletions(-) diff --git a/environment_mac.yml b/environment_mac.yml index 0e4f5d2df..85ef7d3b9 100644 --- a/environment_mac.yml +++ b/environment_mac.yml @@ -23,7 +23,7 @@ dependencies: - conda-forge::pillow - conda-forge::psutil - conda-forge::pykalman - - conda-forge::pyside2 >=5.12 # To ensure application works correctly with QtPy. + - conda-forge::pyside2 >=5.12 # To ensure application works correctly with QtPy. - conda-forge::python ~=3.9 - conda-forge::python-rapidjson - conda-forge::pyyaml diff --git a/sleap/gui/app.py b/sleap/gui/app.py index af42acb5f..4c6ecb1ac 100644 --- a/sleap/gui/app.py +++ b/sleap/gui/app.py @@ -216,7 +216,7 @@ def bulletin_dialog(self): """Displays bulletin dialog is new announcement is available.""" # TODO: Change the URL to the actual SLEAP website before merging to main if online_bulletin: - webbrowser.open('https://sleap.ai/develop/bulletin.html') + webbrowser.open("https://sleap.ai/develop/bulletin.html") else: # Initialize the bulletin popup worker popup_worker = BulletinWorker( diff --git a/tests/gui/test_app.py b/tests/gui/test_app.py index 5a99b9cd2..41b723978 100644 --- a/tests/gui/test_app.py +++ b/tests/gui/test_app.py @@ -1,17 +1,15 @@ import pytest -import sys - -# Skip the test for MacOS due to QWebEngineView ImportError -# TODO: Need to resolve the ImportError -if sys.platform == "darwin": - pytestmark = pytest.mark.skip(reason="ImportError for MacOS") - pytest.skip(reason="Skipping tests and import on macOS", allow_module_level=True) - from qtpy.QtWidgets import QApplication from sleap.gui.app import MainWindow from sleap.gui.commands import * -from sleap.gui.dialogs.bulletin import BulletinWorker + +# Online bulletin if there is an ImportError (for MacOS) +online_bulletin = False +try: + from sleap.gui.dialogs.bulletin import BulletinWorker +except ImportError: + online_bulletin = True def test_app_workflow( @@ -20,11 +18,12 @@ def test_app_workflow( app = MainWindow(no_usage_data=True) # Check if the bulletin is shown or not - bulletin_dialog = app._child_windows.get("bulletin_worker", False) - if app.new_announcement_available: - assert isinstance(bulletin_dialog, BulletinWorker) - else: - assert bulletin_dialog == False + if not online_bulletin: + bulletin_dialog = app._child_windows.get("bulletin_worker", False) + if app.new_announcement_available: + assert isinstance(bulletin_dialog, BulletinWorker) + else: + assert bulletin_dialog == False # Add nodes app.commands.newNode() diff --git a/tests/gui/test_grid_system.py b/tests/gui/test_grid_system.py index 3d22b3ec8..85419a467 100644 --- a/tests/gui/test_grid_system.py +++ b/tests/gui/test_grid_system.py @@ -1,13 +1,4 @@ import numpy as np -import pytest -import sys - -# Skip the test for MacOS due to QWebEngineView ImportError -# TODO: Need to resolve the ImportError -if sys.platform == "darwin": - pytestmark = pytest.mark.skip(reason="ImportError for MacOS") - pytest.skip(reason="Skipping tests and import on macOS", allow_module_level=True) - from sleap.gui.app import MainWindow from sleap.gui.commands import * diff --git a/tests/gui/widgets/test_docks.py b/tests/gui/widgets/test_docks.py index 3cb67229f..69fe56a56 100644 --- a/tests/gui/widgets/test_docks.py +++ b/tests/gui/widgets/test_docks.py @@ -2,14 +2,6 @@ from pathlib import Path import pytest -import sys - -# Skip the test for MacOS due to QWebEngineView ImportError -# TODO: Need to resolve the ImportError -if sys.platform == "darwin": - pytestmark = pytest.mark.skip(reason="ImportError for MacOS") - pytest.skip(reason="Skipping tests and import on macOS", allow_module_level=True) - from sleap import Labels, Video from sleap.gui.app import MainWindow from sleap.gui.commands import OpenSkeleton diff --git a/tests/io/test_formats.py b/tests/io/test_formats.py index 89d27c386..725a00ed3 100644 --- a/tests/io/test_formats.py +++ b/tests/io/test_formats.py @@ -1,6 +1,5 @@ import os from pathlib import Path, PurePath -import sys import numpy as np import pandas as pd @@ -8,12 +7,6 @@ import pytest import nixio -# Skip the test for MacOS due to QWebEngineView ImportError -# TODO: Need to resolve the ImportError -if sys.platform == "darwin": - pytestmark = pytest.mark.skip(reason="ImportError for MacOS") - pytest.skip(reason="Skipping tests and import on macOS", allow_module_level=True) - from sleap.io.video import Video from sleap.instance import Instance, LabeledFrame, PredictedInstance, Track from sleap.io.dataset import Labels From af67d5506ff6a44a2d9cb9cc27f11f7da0b36472 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam Date: Wed, 17 Jan 2024 12:06:00 -0800 Subject: [PATCH 28/29] inlcude review changes --- sleap/gui/app.py | 6 +++--- tests/gui/test_app.py | 11 ++--------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/sleap/gui/app.py b/sleap/gui/app.py index 4c6ecb1ac..7dc21566b 100644 --- a/sleap/gui/app.py +++ b/sleap/gui/app.py @@ -69,11 +69,11 @@ from sleap.gui.dialogs.shortcuts import ShortcutDialog # Open bulletin online if there is an ImportError (for MacOS) -online_bulletin = False +ONLINE_BULLETIN = False try: from sleap.gui.dialogs.bulletin import BulletinWorker except ImportError: - online_bulletin = True + ONLINE_BULLETIN = True from sleap.gui.overlays.instance import InstanceOverlay from sleap.gui.overlays.tracks import TrackListOverlay, TrackTrailOverlay @@ -215,7 +215,7 @@ def __init__( def bulletin_dialog(self): """Displays bulletin dialog is new announcement is available.""" # TODO: Change the URL to the actual SLEAP website before merging to main - if online_bulletin: + if ONLINE_BULLETIN: webbrowser.open("https://sleap.ai/develop/bulletin.html") else: # Initialize the bulletin popup worker diff --git a/tests/gui/test_app.py b/tests/gui/test_app.py index 41b723978..1ffa84bd8 100644 --- a/tests/gui/test_app.py +++ b/tests/gui/test_app.py @@ -1,16 +1,9 @@ import pytest from qtpy.QtWidgets import QApplication -from sleap.gui.app import MainWindow +from sleap.gui.app import * from sleap.gui.commands import * -# Online bulletin if there is an ImportError (for MacOS) -online_bulletin = False -try: - from sleap.gui.dialogs.bulletin import BulletinWorker -except ImportError: - online_bulletin = True - def test_app_workflow( qtbot, centered_pair_vid, small_robot_mp4_vid, min_tracks_2node_labels: Labels @@ -18,7 +11,7 @@ def test_app_workflow( app = MainWindow(no_usage_data=True) # Check if the bulletin is shown or not - if not online_bulletin: + if not ONLINE_BULLETIN: bulletin_dialog = app._child_windows.get("bulletin_worker", False) if app.new_announcement_available: assert isinstance(bulletin_dialog, BulletinWorker) From 150453d02e26e65d489432ddb65604da6a4135a6 Mon Sep 17 00:00:00 2001 From: Shrivaths Shyam <52810689+shrivaths16@users.noreply.github.com> Date: Thu, 18 Jan 2024 22:17:10 -0800 Subject: [PATCH 29/29] PR4: Add helpmenu option (#1628) *Add helpmenu option --- sleap/gui/app.py | 3 +++ sleap/gui/commands.py | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/sleap/gui/app.py b/sleap/gui/app.py index 7dc21566b..5fa5a4d9f 100644 --- a/sleap/gui/app.py +++ b/sleap/gui/app.py @@ -1037,6 +1037,9 @@ def new_instance_menu_action(): helpMenu.addSeparator() helpMenu.addAction("Keyboard Shortcuts", self._show_keyboard_shortcuts_window) + helpMenu.addSeparator() + helpMenu.addAction("What's New?", self.commands.showBulletin) + def process_events_then(self, action: Callable): """Decorates a function with a call to first process events.""" diff --git a/sleap/gui/commands.py b/sleap/gui/commands.py index 90f40397e..81ce7e340 100644 --- a/sleap/gui/commands.py +++ b/sleap/gui/commands.py @@ -623,6 +623,10 @@ def openPrereleaseVersion(self): """Open the current prerelease version.""" self.execute(OpenPrereleaseVersion) + def showBulletin(self): + """Opens the latest bulletin""" + self.execute(ShowBulletin) + # File Commands @@ -3398,6 +3402,12 @@ def do_action(context: CommandContext, params: dict): context.openWebsite(rls.url) +class ShowBulletin(AppCommand): + @staticmethod + def do_action(context: CommandContext, params: dict): + context.app.bulletin_dialog() + + def copy_to_clipboard(text: str): """Copy a string to the system clipboard.