From ffda7eb7d8e92578bdf9e519249a035944c08439 Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Mon, 4 May 2026 22:57:53 -0500 Subject: [PATCH 01/12] two new classes in shortcuts.py. KeyboardShortcutManager stores/handles a user's individual shortcut preferences. ShortcutEditorDialog handles UI interactions and updates a KSM class instance accordingly. base_instrument.py was updated to implement these new classes and add a button to edit keyboard shortcuts. --- src/instrumentserver/gui/base_instrument.py | 53 +++++- src/instrumentserver/gui/shortcuts.py | 191 ++++++++++++++++++++ 2 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 src/instrumentserver/gui/shortcuts.py diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index 5e76af5..7644fd7 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -107,6 +107,7 @@ from typing import Any, Dict, List, Optional, cast from instrumentserver import QtCore, QtGui, QtWidgets +from .shortcuts import KeyboardShortcutManager, ShortcutEditorDialog class ItemBase(QtGui.QStandardItem): @@ -481,7 +482,7 @@ def filterAcceptsRow( item = parent.child(source_row, 0) # The order in which things get constructed seems to impact this. - # When the application is first starting, the proxy model does not have the trash attribute. + # When the application is first starting, the proxy model does not have the trash attribute. if hasattr(self, "trash"): if self.trash: # Assertion is there to satisfy mypy. item can be None, that is why we check before making the assertion @@ -780,6 +781,7 @@ def __init__( proxyModelType: type = InstrumentSortFilterProxyModel, viewType: type = InstrumentTreeViewBase, callSignals: bool = True, + shortcutManager: Optional[KeyboardShortcutManager] = None, parent: Optional[QtWidgets.QWidget] = None, **modelKwargs: Any, ) -> None: @@ -795,6 +797,8 @@ def __init__( self.proxyModel = proxyModelType(self.model) self.view = viewType(self.proxyModel) + self.shortcutManager = shortcutManager if shortcutManager is not None else KeyboardShortcutManager() + self.layout_ = QtWidgets.QVBoxLayout() self.lineEdit = QtWidgets.QLineEdit(self) @@ -830,6 +834,10 @@ def connectSignals(self) -> None: self.proxyModel.onSortingIndicatorChanged ) + self.shortcutManager.register("focus_filter", self.lineEdit.setFocus, self) + self.shortcutManager.register("star_item", self._starCurrentItem, self) + self.shortcutManager.register("trash_item", self._trashCurrentItem, self) + def makeToolbar(self) -> QtWidgets.QToolBar: """ Creates the toolbar, override to add more buttons to the toolbar. @@ -842,6 +850,7 @@ def makeToolbar(self) -> QtWidgets.QToolBar: "refresh all items from the instrument", ) refreshAction.triggered.connect(lambda x: self.refreshAll()) # type: ignore[union-attr] + self.shortcutManager.apply_to_action("refresh_all", refreshAction) # type: ignore[union-attr] toolbar.addSeparator() @@ -850,12 +859,14 @@ def makeToolbar(self) -> QtWidgets.QToolBar: "expand tree", ) expandAction.triggered.connect(lambda x: self.view.expandAll()) # type: ignore[union-attr] + self.shortcutManager.apply_to_action("expand_all", expandAction) # type: ignore[union-attr] collapseAction = toolbar.addAction( QtGui.QIcon(":/icons/collapse.svg"), "collapse tree", ) collapseAction.triggered.connect(lambda x: self.view.collapseAll()) # type: ignore[union-attr] + self.shortcutManager.apply_to_action("collapse_all", collapseAction) # type: ignore[union-attr] toolbar.addSeparator() @@ -864,12 +875,21 @@ def makeToolbar(self) -> QtWidgets.QToolBar: ) starAction.setCheckable(True) # type: ignore[union-attr] starAction.triggered.connect(lambda x: self.promoteStar()) # type: ignore[union-attr] + self.shortcutManager.apply_to_action("toggle_star", starAction) # type: ignore[union-attr] trashAction = toolbar.addAction( QtGui.QIcon(":/icons/trash-crossed.svg"), "Hide trashed items" ) trashAction.setCheckable(True) # type: ignore[union-attr] trashAction.triggered.connect(lambda x: self.hideTrash()) # type: ignore[union-attr] + self.shortcutManager.apply_to_action("toggle_trash", trashAction) # type: ignore[union-attr] + + toolbar.addSeparator() + + shortcutsAction = toolbar.addAction( + QtGui.QIcon(":/icons/code.svg"), "Edit keyboard shortcuts" + ) + shortcutsAction.triggered.connect(self.openShortcutEditor) # type: ignore[union-attr] # Debugging tools keep commented for commits. # printAction = toolbar.addAction( @@ -894,6 +914,37 @@ def promoteStar(self) -> None: def refreshAll(self) -> None: self.model.refreshAll() + @QtCore.Slot() + def _starCurrentItem(self) -> None: + proxy_index = self.view.currentIndex() + if not proxy_index.isValid(): + return + source_index = self.proxyModel.mapToSource(proxy_index) + if source_index.column() != 0: + source_index = source_index.sibling(source_index.row(), 0) + item = self.model.itemFromIndex(source_index) # type: ignore[union-attr] + if isinstance(item, ItemBase): + self.view.lastSelectedItem = item + self.view.itemStarToggle.emit(item) + + @QtCore.Slot() + def _trashCurrentItem(self) -> None: + proxy_index = self.view.currentIndex() + if not proxy_index.isValid(): + return + source_index = self.proxyModel.mapToSource(proxy_index) + if source_index.column() != 0: + source_index = source_index.sibling(source_index.row(), 0) + item = self.model.itemFromIndex(source_index) # type: ignore[union-attr] + if isinstance(item, ItemBase): + self.view.lastSelectedItem = item + self.view.itemTrashToggle.emit(item) + + @QtCore.Slot() + def openShortcutEditor(self) -> None: + dialog = ShortcutEditorDialog(self.shortcutManager, self) + dialog.exec_() + def debuggingMethod(self) -> None: """ This is just a debugging method. diff --git a/src/instrumentserver/gui/shortcuts.py b/src/instrumentserver/gui/shortcuts.py new file mode 100644 index 0000000..3740b68 --- /dev/null +++ b/src/instrumentserver/gui/shortcuts.py @@ -0,0 +1,191 @@ +import json +import logging +from typing import Callable, Optional + +from instrumentserver import QtCore, QtGui, QtWidgets + +from .misc import BaseDialog + +logger = logging.getLogger(__name__) + +class KeyboardShortcutManager: + """ + Manages keyboard shortcut mappings for the instrument GUI. + + Holds a registry of named actions with default key sequences and descriptions. + The active mapping starts from defaults and can be customized by the user and + persisted to a JSON file. + + Qt does not poll for key presses — instead, register() and apply_to_action() + hand each mapping entry to Qt's event system (QShortcut / QAction.setShortcut), + which fires the associated callback when the key is pressed. + """ + + REGISTRY: dict[str, tuple[str, str]] = { + # action_id: (default_key_sequence, description) + "refresh_all": ("Ctrl+R", "Refresh all parameters from instrument"), + "expand_all": ("Ctrl+E", "Expand all tree nodes"), + "collapse_all": ("Ctrl+Shift+E", "Collapse all tree nodes"), + "toggle_star": ("Ctrl+Shift+S", "Toggle star filter"), + "toggle_trash": ("Ctrl+Shift+T", "Toggle trash filter"), + "focus_filter": ("Ctrl+F", "Focus the filter search bar"), + "star_item": ("Ctrl+S", "Star/un-star the selected parameter"), + "trash_item": ("Ctrl+T", "Trash/un-trash the selected parameter"), + } + + def __init__(self) -> None: + self.mapping: dict[str, str] = {k: v[0] for k, v in self.REGISTRY.items()} + self._shortcut_map: dict[str, QtWidgets.QShortcut] = {} + self._action_map: dict[str, QtWidgets.QAction] = {} + + def load(self, path: str) -> None: + """Override the current mapping with entries read from a JSON file.""" + with open(path) as f: + data = json.load(f) + self.mapping.update(data) + + def save(self, path: str) -> None: + """Write the current mapping to a JSON file.""" + with open(path, "w") as f: + json.dump(self.mapping, f, indent=2) + + def apply_to_action(self, action_id: str, qaction: QtWidgets.QAction) -> None: + """Set the shortcut from the current mapping on an existing QAction and retain a reference for live rebinding.""" + key = self.mapping.get(action_id) + if key: + qaction.setShortcut(QtGui.QKeySequence(key)) + self._action_map[action_id] = qaction + + def register( + self, action_id: str, callback: Callable, widget: QtWidgets.QWidget + ) -> None: + """ + Create a QShortcut for action_id on widget and connect it to callback. + + The shortcut fires when widget or any of its children has focus. + The QShortcut object is retained internally so it is not garbage-collected + and can be updated live via rebind(). + """ + key = self.mapping.get(action_id) + if key: + sc = QtWidgets.QShortcut(QtGui.QKeySequence(key), widget) + sc.activated.connect(callback) + self._shortcut_map[action_id] = sc + + def rebind(self, action_id: str, new_key: str) -> None: + """Update a shortcut immediately. Updates the mapping and the live Qt objects.""" + self.mapping[action_id] = new_key + if action_id in self._shortcut_map: + self._shortcut_map[action_id].setKey(QtGui.QKeySequence(new_key)) + if action_id in self._action_map: + self._action_map[action_id].setShortcut(QtGui.QKeySequence(new_key)) + logger.debug(f"Rebound '{action_id}' to '{new_key}'") + + +class ShortcutEditorDialog(BaseDialog): + """ + Dialog for viewing and editing keyboard shortcuts. + + Displays all registered actions in a table. The Shortcut column is editable. + Use 'Save to file' to persist changes; 'Load from file' to restore a saved mapping. + Changes take effect on the next application start. + """ + + def __init__( + self, + manager: KeyboardShortcutManager, + parent: Optional[QtWidgets.QWidget] = None, + ) -> None: + super().__init__(parent) + self.manager = manager + self.setWindowTitle("Keyboard Shortcuts") + + self._table = QtWidgets.QTableWidget(len(manager.REGISTRY), 3, self) + self._table.setHorizontalHeaderLabels(["Action", "Description", "Shortcut"]) + self._table.horizontalHeader().setStretchLastSection(True) # type: ignore[union-attr] + self._table.setSelectionBehavior( + QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows + ) + self._populateTable() + + btnLoad = QtWidgets.QPushButton("Load from file") + btnLoad.clicked.connect(self._loadFromFile) + btnSave = QtWidgets.QPushButton("Save to file") + btnSave.clicked.connect(self._saveToFile) + btnReset = QtWidgets.QPushButton("Reset to defaults") + btnReset.clicked.connect(self._resetDefaults) + btnClose = QtWidgets.QPushButton("Close") + btnClose.clicked.connect(self.accept) + + btnRow = QtWidgets.QHBoxLayout() + btnRow.addWidget(btnLoad) + btnRow.addWidget(btnSave) + btnRow.addStretch() + btnRow.addWidget(btnReset) + btnRow.addWidget(btnClose) + + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self._table) + layout.addLayout(btnRow) + self.setLayout(layout) + self.resize(600, 300) + + def _populateTable(self) -> None: + self._table.clearContents() + for row, (action_id, (_, description)) in enumerate( + self.manager.REGISTRY.items() + ): + current = self.manager.mapping.get(action_id, "") + id_item = QtWidgets.QTableWidgetItem(action_id) + id_item.setFlags(id_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) + desc_item = QtWidgets.QTableWidgetItem(description) + desc_item.setFlags(desc_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) + self._table.setItem(row, 0, id_item) + self._table.setItem(row, 1, desc_item) + key_edit = QtWidgets.QKeySequenceEdit( + QtGui.QKeySequence(current), self._table + ) + self._table.setCellWidget(row, 2, key_edit) + self._table.resizeColumnsToContents() + + def _commitTableToManager(self) -> None: + for row, action_id in enumerate(self.manager.REGISTRY): + widget = self._table.cellWidget(row, 2) + if isinstance(widget, QtWidgets.QKeySequenceEdit): + self.manager.rebind(action_id, widget.keySequence().toString()) + + @QtCore.Slot() + def _loadFromFile(self) -> None: + path, _ = QtWidgets.QFileDialog.getOpenFileName( + self, "Load Shortcuts", ".", "JSON Files (*.json);;All Files (*)" + ) + if path: + try: + self.manager.load(path) + self._populateTable() + logger.info(f"Loaded shortcuts from {path}") + except Exception as e: + logger.warning(f"Failed to load shortcuts from {path} : {e}") + + @QtCore.Slot() + def _saveToFile(self) -> None: + self._commitTableToManager() + path, _ = QtWidgets.QFileDialog.getSaveFileName( + self, "Save Shortcuts", "shortcuts.json", "JSON Files (*.json);;All Files (*)" + ) + if path: + try: + self.manager.save(path) + logger.info(f"Saved shortcuts to {path}") + except Exception as e: + logger.warning(f"Failed to save shortcuts to {path} : {e}") + + @QtCore.Slot() + def _resetDefaults(self) -> None: + for row, (action_id, (default_key, _)) in enumerate( + self.manager.REGISTRY.items() + ): + self.manager.rebind(action_id, default_key) + widget = self._table.cellWidget(row, 2) + if isinstance(widget, QtWidgets.QKeySequenceEdit): + widget.setKeySequence(QtGui.QKeySequence(default_key)) From f531047a357a4936f5ec31aabd7fbcef79a7a896 Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Wed, 6 May 2026 20:25:38 -0500 Subject: [PATCH 02/12] new structure for shortcuts. new permanent tab for shortcuts similar to Station or Log. alert column on right side indicates three states: ok (white), unsaved (orange), and duplicate (red). alert displays tooltip for each tooltip. ServerGui now holds a shortcut editor and shortcut manager, which is passed to an instrument tab when opened. --- src/instrumentserver/gui/base_instrument.py | 14 +- src/instrumentserver/gui/instruments.py | 6 + src/instrumentserver/gui/shortcuts.py | 156 +++++++++++++++--- .../resource/icons/alert-octagon-orange.svg | 21 +++ src/instrumentserver/server/application.py | 7 + 5 files changed, 168 insertions(+), 36 deletions(-) create mode 100644 src/instrumentserver/resource/icons/alert-octagon-orange.svg diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index 7644fd7..ba017ed 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -107,7 +107,7 @@ from typing import Any, Dict, List, Optional, cast from instrumentserver import QtCore, QtGui, QtWidgets -from .shortcuts import KeyboardShortcutManager, ShortcutEditorDialog +from .shortcuts import KeyboardShortcutManager class ItemBase(QtGui.QStandardItem): @@ -884,13 +884,6 @@ def makeToolbar(self) -> QtWidgets.QToolBar: trashAction.triggered.connect(lambda x: self.hideTrash()) # type: ignore[union-attr] self.shortcutManager.apply_to_action("toggle_trash", trashAction) # type: ignore[union-attr] - toolbar.addSeparator() - - shortcutsAction = toolbar.addAction( - QtGui.QIcon(":/icons/code.svg"), "Edit keyboard shortcuts" - ) - shortcutsAction.triggered.connect(self.openShortcutEditor) # type: ignore[union-attr] - # Debugging tools keep commented for commits. # printAction = toolbar.addAction( # QtGui.QIcon(":/icons/code.svg"), @@ -940,11 +933,6 @@ def _trashCurrentItem(self) -> None: self.view.lastSelectedItem = item self.view.itemTrashToggle.emit(item) - @QtCore.Slot() - def openShortcutEditor(self) -> None: - dialog = ShortcutEditorDialog(self.shortcutManager, self) - dialog.exec_() - def debuggingMethod(self) -> None: """ This is just a debugging method. diff --git a/src/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py index 1a41ae0..60ba01f 100644 --- a/src/instrumentserver/gui/instruments.py +++ b/src/instrumentserver/gui/instruments.py @@ -465,6 +465,8 @@ def __init__( if "sub_port" in kwargs: modelKwargs["sub_port"] = kwargs.pop("sub_port") + shortcutManager = kwargs.pop("shortcutManager", None) + super().__init__( instrument=instrument, parent=parent, @@ -473,6 +475,7 @@ def __init__( modelType=ModelParameters, viewType=viewType, callSignals=callSignals, + shortcutManager=shortcutManager, **modelKwargs, ) @@ -767,11 +770,14 @@ def __init__(self, instrument: Any, **kwargs: Any) -> None: if "methods-hide" in kwargs: modelKwargs["itemsHide"] = kwargs.pop("methods-hide") + shortcutManager = kwargs.pop("shortcutManager", None) + super().__init__( instrument=instrument, attr="functions", modelType=MethodsModel, viewType=MethodsTreeView, + shortcutManager=shortcutManager, **modelKwargs, ) diff --git a/src/instrumentserver/gui/shortcuts.py b/src/instrumentserver/gui/shortcuts.py index 3740b68..0804f7a 100644 --- a/src/instrumentserver/gui/shortcuts.py +++ b/src/instrumentserver/gui/shortcuts.py @@ -1,10 +1,13 @@ import json import logging +import os +from collections import defaultdict from typing import Callable, Optional -from instrumentserver import QtCore, QtGui, QtWidgets +from instrumentserver import QtCore, QtGui, QtWidgets, getInstrumentserverPath + +_ICON_DIR = getInstrumentserverPath("resource", "icons") -from .misc import BaseDialog logger = logging.getLogger(__name__) @@ -82,13 +85,23 @@ def rebind(self, action_id: str, new_key: str) -> None: logger.debug(f"Rebound '{action_id}' to '{new_key}'") -class ShortcutEditorDialog(BaseDialog): +class ShortcutEditorWidget(QtWidgets.QWidget): """ - Dialog for viewing and editing keyboard shortcuts. + Permanent widget for viewing and editing keyboard shortcuts. + + Intended to be embedded as a tab in the server window. Changes made in the + table are applied live to the manager (and therefore all registered shortcuts) + when Save is clicked. Use 'Save to file' / 'Load from file' to persist across sessions. - Displays all registered actions in a table. The Shortcut column is editable. - Use 'Save to file' to persist changes; 'Load from file' to restore a saved mapping. - Changes take effect on the next application start. + Each row has a small colored indicator dot in the rightmost column: + - white : saved and unique + - orange: unsaved change (widget value differs from manager.mapping) + - red : duplicate key sequence shared with another action (takes priority) + + QKeySequenceEdit emits a spurious keySequenceChanged after its finishing timeout + resets the internal recording state. _onEditingFinished blocks that widget's signals + for one event-loop tick (swallowing the revert signal at the source), then restores + the display if the widget actually changed its stored sequence during the block. """ def __init__( @@ -98,61 +111,157 @@ def __init__( ) -> None: super().__init__(parent) self.manager = manager - self.setWindowTitle("Keyboard Shortcuts") - self._table = QtWidgets.QTableWidget(len(manager.REGISTRY), 3, self) - self._table.setHorizontalHeaderLabels(["Action", "Description", "Shortcut"]) - self._table.horizontalHeader().setStretchLastSection(True) # type: ignore[union-attr] + self._table = QtWidgets.QTableWidget(len(manager.REGISTRY), 4, self) + self._table.setHorizontalHeaderLabels(["Action", "Description", "Shortcut", ""]) + header = self._table.horizontalHeader() + header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.ResizeToContents) # type: ignore[union-attr] + header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeMode.ResizeToContents) # type: ignore[union-attr] + header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeMode.Stretch) # type: ignore[union-attr] + header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeMode.Fixed) # type: ignore[union-attr] + self._table.setColumnWidth(3, 32) self._table.setSelectionBehavior( QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows ) + + self._indicators: list[QtWidgets.QLabel] = [] self._populateTable() btnLoad = QtWidgets.QPushButton("Load from file") btnLoad.clicked.connect(self._loadFromFile) - btnSave = QtWidgets.QPushButton("Save to file") - btnSave.clicked.connect(self._saveToFile) + btnSaveFile = QtWidgets.QPushButton("Save to file") + btnSaveFile.clicked.connect(self._saveToFile) btnReset = QtWidgets.QPushButton("Reset to defaults") btnReset.clicked.connect(self._resetDefaults) - btnClose = QtWidgets.QPushButton("Close") - btnClose.clicked.connect(self.accept) + btnSave = QtWidgets.QPushButton("Save") + btnSave.clicked.connect(self._save) btnRow = QtWidgets.QHBoxLayout() btnRow.addWidget(btnLoad) - btnRow.addWidget(btnSave) + btnRow.addWidget(btnSaveFile) btnRow.addStretch() btnRow.addWidget(btnReset) - btnRow.addWidget(btnClose) + btnRow.addWidget(btnSave) layout = QtWidgets.QVBoxLayout() layout.addWidget(self._table) layout.addLayout(btnRow) self.setLayout(layout) - self.resize(600, 300) def _populateTable(self) -> None: + self._indicators.clear() self._table.clearContents() for row, (action_id, (_, description)) in enumerate( self.manager.REGISTRY.items() ): current = self.manager.mapping.get(action_id, "") + id_item = QtWidgets.QTableWidgetItem(action_id) id_item.setFlags(id_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) desc_item = QtWidgets.QTableWidgetItem(description) desc_item.setFlags(desc_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) self._table.setItem(row, 0, id_item) self._table.setItem(row, 1, desc_item) + key_edit = QtWidgets.QKeySequenceEdit( QtGui.QKeySequence(current), self._table ) + key_edit.keySequenceChanged.connect(self._onUnsavedChange) + key_edit.editingFinished.connect( + lambda w=key_edit: self._onEditingFinished(w) + ) self._table.setCellWidget(row, 2, key_edit) - self._table.resizeColumnsToContents() - def _commitTableToManager(self) -> None: + dot = QtWidgets.QLabel() + dot.setFixedSize(20, 20) + dot.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + dot.setStyleSheet( + "QToolTip { color: black; background-color: white;" + " border: 1px solid #cccccc; }" + ) + container = QtWidgets.QWidget() + cl = QtWidgets.QHBoxLayout(container) + cl.addWidget(dot) + cl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + cl.setContentsMargins(0, 0, 0, 0) + self._table.setCellWidget(row, 3, container) + self._indicators.append(dot) + + self._updateAllIndicators() + + def _collectDuplicates(self) -> dict[str, list[str]]: + """Return {key_sequence: [action_ids]} for every key bound to more than one action.""" + seen: dict[str, list[str]] = defaultdict(list) + for row, action_id in enumerate(self.manager.REGISTRY): + widget = self._table.cellWidget(row, 2) + if isinstance(widget, QtWidgets.QKeySequenceEdit): + key = widget.keySequence().toString() + if key: + seen[key].append(action_id) + return {k: v for k, v in seen.items() if len(v) > 1} + + def _updateAllIndicators(self) -> None: + duplicates = self._collectDuplicates() + for row, action_id in enumerate(self.manager.REGISTRY): + if row >= len(self._indicators): + break + dot = self._indicators[row] + widget = self._table.cellWidget(row, 2) + if not isinstance(widget, QtWidgets.QKeySequenceEdit): + continue + current = widget.keySequence().toString() + if current in duplicates: + others = [a for a in duplicates[current] if a != action_id] + self._applyIndicator(dot, "duplicate", + f"Duplicate: also bound to {', '.join(others)}") + elif current != self.manager.mapping.get(action_id, ""): + self._applyIndicator(dot, "unsaved", "Unsaved change") + else: + self._applyIndicator(dot, "ok", "") + + @staticmethod + def _applyIndicator(dot: QtWidgets.QLabel, state: str, tooltip: str) -> None: + dot.setToolTip(tooltip) + if state == "ok": + icon_file = "alert-octagon.svg" + elif state == "unsaved": + icon_file = "alert-octagon-orange.svg" + else: # duplicate + icon_file = "alert-octagon-red.svg" + pix = QtGui.QIcon(os.path.join(_ICON_DIR, icon_file)).pixmap(20, 20) + dot.setPixmap(pix) + + @QtCore.Slot() + def _onUnsavedChange(self) -> None: + self._updateAllIndicators() + + def _onEditingFinished(self, widget: QtWidgets.QKeySequenceEdit) -> None: + # Capture the intended value before Qt resets the recording state. + # Block signals for one event-loop tick so the spurious keySequenceChanged + # that follows the internal reset never reaches _onUnsavedChange. + intended = widget.keySequence().toString() + widget.blockSignals(True) + QtCore.QTimer.singleShot( + 0, lambda: self._restoreAfterRevert(intended, widget) + ) + + def _restoreAfterRevert( + self, intended: str, widget: QtWidgets.QKeySequenceEdit + ) -> None: + # If the widget reverted its stored sequence during the block window, + # put it back so the display and _save() read the correct value. + if widget.keySequence().toString() != intended: + widget.setKeySequence(QtGui.QKeySequence(intended)) + widget.blockSignals(False) + + @QtCore.Slot() + def _save(self) -> None: for row, action_id in enumerate(self.manager.REGISTRY): widget = self._table.cellWidget(row, 2) if isinstance(widget, QtWidgets.QKeySequenceEdit): self.manager.rebind(action_id, widget.keySequence().toString()) + self._updateAllIndicators() + logger.info("Shortcuts saved locally") @QtCore.Slot() def _loadFromFile(self) -> None: @@ -165,11 +274,11 @@ def _loadFromFile(self) -> None: self._populateTable() logger.info(f"Loaded shortcuts from {path}") except Exception as e: - logger.warning(f"Failed to load shortcuts from {path} : {e}") + logger.warning(f"Failed to load shortcuts from {path}: {e}") @QtCore.Slot() def _saveToFile(self) -> None: - self._commitTableToManager() + self._save() path, _ = QtWidgets.QFileDialog.getSaveFileName( self, "Save Shortcuts", "shortcuts.json", "JSON Files (*.json);;All Files (*)" ) @@ -178,7 +287,7 @@ def _saveToFile(self) -> None: self.manager.save(path) logger.info(f"Saved shortcuts to {path}") except Exception as e: - logger.warning(f"Failed to save shortcuts to {path} : {e}") + logger.warning(f"Failed to save shortcuts to {path}: {e}") @QtCore.Slot() def _resetDefaults(self) -> None: @@ -189,3 +298,4 @@ def _resetDefaults(self) -> None: widget = self._table.cellWidget(row, 2) if isinstance(widget, QtWidgets.QKeySequenceEdit): widget.setKeySequence(QtGui.QKeySequence(default_key)) + self._updateAllIndicators() diff --git a/src/instrumentserver/resource/icons/alert-octagon-orange.svg b/src/instrumentserver/resource/icons/alert-octagon-orange.svg new file mode 100644 index 0000000..061888c --- /dev/null +++ b/src/instrumentserver/resource/icons/alert-octagon-orange.svg @@ -0,0 +1,21 @@ + + + + + + diff --git a/src/instrumentserver/server/application.py b/src/instrumentserver/server/application.py index 83fcc0a..8cc2854 100644 --- a/src/instrumentserver/server/application.py +++ b/src/instrumentserver/server/application.py @@ -14,8 +14,10 @@ from ..gui.instruments import GenericInstrument from ..gui.misc import BaseDialog, DetachableTabWidget from ..gui.parameters import AnyInputForMethod +from ..gui.shortcuts import KeyboardShortcutManager, ShortcutEditorWidget from .core import InstrumentModuleBluePrint, ParameterBluePrint, StationServer + logger = logging.getLogger(__name__) @@ -659,6 +661,10 @@ def __init__( self.serverStatus = ServerStatus() self.tabs.addUnclosableTab(self.serverStatus, "Server") + self.shortcutManager = KeyboardShortcutManager() + self.shortcutEditor = ShortcutEditorWidget(self.shortcutManager) + self.tabs.addUnclosableTab(self.shortcutEditor, "Shortcuts") + # Toolbar. self.toolBar = self.addToolBar("Tools") self.toolBar.setIconSize(QtCore.QSize(16, 16)) # type: ignore[union-attr] @@ -889,6 +895,7 @@ def addInstrumentTab(self, item: QtWidgets.QTreeWidgetItem, index: int) -> None: kwargs = self._guiConfig[name]["gui"]["kwargs"] kwargs["sub_port"] = kwargs.get("sub_port", self.stationServer.port + 1) # type: ignore[union-attr] + kwargs["shortcutManager"] = self.shortcutManager insWidget = widgetClass(ins, parent=self, **kwargs) index = self.tabs.addTab(insWidget, ins.name) self.instrumentTabsOpen[ins.name] = insWidget From 1c205a4f0115848d7b783b3887611b8b09273b66 Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Wed, 6 May 2026 21:29:13 -0500 Subject: [PATCH 03/12] added more shortcuts and implemented there backend actions --- src/instrumentserver/gui/base_instrument.py | 23 +++++++++- src/instrumentserver/gui/instruments.py | 51 ++++++++++++++++++++- src/instrumentserver/gui/shortcuts.py | 25 ++++++---- 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index ba017ed..7157e4b 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -834,9 +834,11 @@ def connectSignals(self) -> None: self.proxyModel.onSortingIndicatorChanged ) - self.shortcutManager.register("focus_filter", self.lineEdit.setFocus, self) + self.shortcutManager.register("jump_filter", self.lineEdit.setFocus, self) self.shortcutManager.register("star_item", self._starCurrentItem, self) self.shortcutManager.register("trash_item", self._trashCurrentItem, self) + self.shortcutManager.register("fit_column", self._fitCurrentColumn, self) + self.shortcutManager.register("sort_column", self._sortCurrentColumn, self) def makeToolbar(self) -> QtWidgets.QToolBar: """ @@ -933,6 +935,25 @@ def _trashCurrentItem(self) -> None: self.view.lastSelectedItem = item self.view.itemTrashToggle.emit(item) + @QtCore.Slot() + def _fitCurrentColumn(self) -> None: + col = self.view.currentIndex().column() + self.view.resizeColumnToContents(col if col >= 0 else 0) + + @QtCore.Slot() + def _sortCurrentColumn(self) -> None: + header = self.view.header() + col = self.view.currentIndex().column() + if col < 0: + col = header.sortIndicatorSection() + current_order = header.sortIndicatorOrder() + new_order = ( + QtCore.Qt.SortOrder.AscendingOrder + if current_order == QtCore.Qt.SortOrder.DescendingOrder + else QtCore.Qt.SortOrder.DescendingOrder + ) + header.setSortIndicator(col, new_order) + def debuggingMethod(self) -> None: """ This is just a debugging method. diff --git a/src/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py index 60ba01f..9b62444 100644 --- a/src/instrumentserver/gui/instruments.py +++ b/src/instrumentserver/gui/instruments.py @@ -19,7 +19,7 @@ InstrumentTreeViewBase, ItemBase, ) -from .parameters import AnyInputForMethod, ParameterWidget +from .parameters import AnyInput, AnyInputForMethod, ParameterWidget # TODO: all styles set through a global style sheet. # TODO: [maybe] add a column for information on valid input values? @@ -482,6 +482,36 @@ def __init__( def connectSignals(self) -> None: super().connectSignals() self.model.itemNewValue.connect(self.view.onItemNewValue) + self.shortcutManager.register("refresh_item", self._refreshCurrentItem, self) + self.shortcutManager.register("toggle_python", self._togglePythonCurrentItem, self) + + @QtCore.Slot() + def _refreshCurrentItem(self) -> None: + proxy_index = self.view.currentIndex() + if not proxy_index.isValid(): + return + source_index = self.proxyModel.mapToSource(proxy_index) + if source_index.column() != 0: + source_index = source_index.sibling(source_index.row(), 0) + item = self.model.itemFromIndex(source_index) + if isinstance(item, ItemBase): + widget = self.view.delegate.parameters.get(item.name) + if widget is not None: + widget.setWidgetFromParameter() + + @QtCore.Slot() + def _togglePythonCurrentItem(self) -> None: + proxy_index = self.view.currentIndex() + if not proxy_index.isValid(): + return + source_index = self.proxyModel.mapToSource(proxy_index) + if source_index.column() != 0: + source_index = source_index.sibling(source_index.row(), 0) + item = self.model.itemFromIndex(source_index) + if isinstance(item, ItemBase): + widget = self.view.delegate.parameters.get(item.name) + if widget is not None and isinstance(widget.paramWidget, AnyInput): + widget.paramWidget.doEval.toggle() # ----------------- Parameters Display Classes - Ending -------------------------------- @@ -618,6 +648,23 @@ def connectSignals(self) -> None: self.parameterCreationError.connect(self.addParam.setError) self.parameterCreated.connect(self.addParam.clear) self.profileManager.indexChanged.connect(self.loadProfile) + self.shortcutManager.register("delete_item", self._deleteCurrentItem, self) + self.shortcutManager.register("clear_add", self.addParam.clear, self) + self.shortcutManager.register("add_item", self.addParam.nameEdit.setFocus, self) + self.shortcutManager.register("load_items", self.loadFromFile, self) + self.shortcutManager.register("save_items", self.saveToFile, self) + + @QtCore.Slot() + def _deleteCurrentItem(self) -> None: + proxy_index = self.view.currentIndex() + if not proxy_index.isValid(): + return + source_index = self.proxyModel.mapToSource(proxy_index) + if source_index.column() != 0: + source_index = source_index.sibling(source_index.row(), 0) + item = self.model.itemFromIndex(source_index) + if isinstance(item, ItemBase): + self.removeParameter(item.name) def makeToolbar(self) -> QtWidgets.QToolBar: toolbar = super().makeToolbar() @@ -629,12 +676,14 @@ def makeToolbar(self) -> QtWidgets.QToolBar: "Load parameters from file", ) loadParamAction.triggered.connect(lambda x: self.loadFromFile()) # type: ignore[union-attr] + self.shortcutManager.apply_to_action("load_items", loadParamAction) # type: ignore[union-attr] saveParamAction = toolbar.addAction( QtGui.QIcon(":/icons/save.svg"), "Save parameters to file", ) saveParamAction.triggered.connect(lambda x: self.saveToFile()) # type: ignore[union-attr] + self.shortcutManager.apply_to_action("save_items", saveParamAction) # type: ignore[union-attr] return toolbar diff --git a/src/instrumentserver/gui/shortcuts.py b/src/instrumentserver/gui/shortcuts.py index 0804f7a..5912f8b 100644 --- a/src/instrumentserver/gui/shortcuts.py +++ b/src/instrumentserver/gui/shortcuts.py @@ -26,14 +26,23 @@ class KeyboardShortcutManager: REGISTRY: dict[str, tuple[str, str]] = { # action_id: (default_key_sequence, description) - "refresh_all": ("Ctrl+R", "Refresh all parameters from instrument"), - "expand_all": ("Ctrl+E", "Expand all tree nodes"), - "collapse_all": ("Ctrl+Shift+E", "Collapse all tree nodes"), - "toggle_star": ("Ctrl+Shift+S", "Toggle star filter"), - "toggle_trash": ("Ctrl+Shift+T", "Toggle trash filter"), - "focus_filter": ("Ctrl+F", "Focus the filter search bar"), - "star_item": ("Ctrl+S", "Star/un-star the selected parameter"), - "trash_item": ("Ctrl+T", "Trash/un-trash the selected parameter"), + "jump_filter": ("Ctrl+F", "Jump cursor to the filter search bar"), + "collapse_all": ("Ctrl+Shift+E", "Collapse all tree nodes"), + "expand_all": ("Ctrl+E", "Expand all tree nodes"), + "toggle_star": ("Ctrl+Shift+A", "Toggle star filter"), + "star_item": ("Ctrl+A", "Star/un-star the selected parameter"), + "toggle_trash": ("Ctrl+Shift+T", "Toggle trash filter"), + "trash_item": ("Ctrl+T", "Trash/un-trash the selected parameter"), + "refresh_all": ("Ctrl+Shift+R", "Refresh all parameters from instrument"), + "refresh_item": ("Ctrl+R", "Refresh the selected parameter"), + "toggle_python": ("Ctrl+P", "Toggle Python eval for selected parameter"), + "delete_item": ("Ctrl+Backspace", "Delete the selected parameter"), + "clear_add": ("Ctrl+Shift+N", "Clear regions of add parameter bar"), + "add_item": ("Ctrl+N", "Jump cursor to the add parameter bar"), + "load_items": ("Ctrl+O", "Load parameters from JSON file"), + "save_items": ("Ctrl+S", "Save parameters to JSON file"), + "fit_column": ("Ctrl+Shift+D", "Fits column width"), + "sort_column": ("Ctrl+D", "Toggle sorting of selected column") } def __init__(self) -> None: From c64a4897a809683298d348f15d9d2c193d03116c Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Wed, 6 May 2026 23:01:15 -0500 Subject: [PATCH 04/12] ruff edits v2 --- src/instrumentserver/gui/base_instrument.py | 152 ++++++++++---------- src/instrumentserver/gui/instruments.py | 48 ++++--- src/instrumentserver/gui/shortcuts.py | 82 ++++++----- src/instrumentserver/server/application.py | 99 +++++++------ 4 files changed, 205 insertions(+), 176 deletions(-) diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index 7157e4b..fc15dcd 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -107,6 +107,7 @@ from typing import Any, Dict, List, Optional, cast from instrumentserver import QtCore, QtGui, QtWidgets + from .shortcuts import KeyboardShortcutManager @@ -150,8 +151,8 @@ class DelegateBase(QtWidgets.QStyledItemDelegate): def getItem(cls, QModelIndex: QtCore.QModelIndex) -> QtGui.QStandardItem: proxyModel = QModelIndex.model() - model = proxyModel.sourceModel() # type: ignore[union-attr] - item = model.itemFromIndex(proxyModel.mapToSource(QModelIndex)) # type: ignore[union-attr] + model = proxyModel.sourceModel() + item = model.itemFromIndex(proxyModel.mapToSource(QModelIndex)) if item.column != 0: parent = item.parent() row = item.row() @@ -224,7 +225,7 @@ def __init__( self.loadingItems = False @staticmethod - def _matches_any_pattern(name: str, patterns: List[str]) -> bool: + def _matches_any_pattern(name: str, patterns: Optional[List[str]]) -> bool: """ Check if a name matches any glob pattern in the list. @@ -238,6 +239,8 @@ def _matches_any_pattern(name: str, patterns: List[str]) -> bool: :param patterns: List of glob patterns to match against (e.g., 'power_*', '*_frequency') :return: True if name matches any pattern, False otherwise """ + if not patterns: + return False for pattern in patterns: if fnmatch.fnmatch(name, pattern): return True @@ -259,13 +262,13 @@ def loadItems(self, module: Any = None, prefix: Optional[str] = None) -> None: # constructor if prefix is not None: objectName = ".".join([prefix, objectName]) - if not self._matches_any_pattern(objectName, self.itemsHide): # type: ignore[arg-type] + if not self._matches_any_pattern(objectName, self.itemsHide): item = self.addItem( fullName=objectName, star=False, trash=False, element=obj ) - if self._matches_any_pattern(objectName, self.itemsTrash): # type: ignore[arg-type] + if self._matches_any_pattern(objectName, self.itemsTrash): self.onItemTrashToggle(item) - if self._matches_any_pattern(objectName, self.itemsStar): # type: ignore[arg-type] + if self._matches_any_pattern(objectName, self.itemsStar): self.onItemStarToggle(item) for submodName, submod in module.submodules.items(): @@ -332,20 +335,20 @@ def addItem(self, fullName: str, **kwargs: Any) -> "ItemBase": ) # submodules get directly added here and not in the load function, so need to have it here too. if self.loadingItems: - if not self._matches_any_pattern(smName, self.itemsHide): # type: ignore[arg-type] - self.insertItemTo(parent, subModItem) # type: ignore[arg-type] - if self._matches_any_pattern(smName, self.itemsTrash): # type: ignore[arg-type] + if not self._matches_any_pattern(smName, self.itemsHide): + self.insertItemTo(parent, subModItem) + if self._matches_any_pattern(smName, self.itemsTrash): self.onItemTrashToggle(subModItem) - if self._matches_any_pattern(smName, self.itemsStar): # type: ignore[arg-type] + if self._matches_any_pattern(smName, self.itemsStar): self.onItemStarToggle(subModItem) else: - self.insertItemTo(parent, subModItem) # type: ignore[arg-type] - parent = subModItem # type: ignore[assignment] + self.insertItemTo(parent, subModItem) + parent = subModItem else: - parent = items[0] # type: ignore[assignment] + parent = items[0] newItem = self.itemClass(name=fullName, **kwargs) - self.insertItemTo(parent, newItem) # type: ignore[arg-type] + self.insertItemTo(parent, newItem) return newItem @@ -465,7 +468,7 @@ def _isParentTrash(self, parent: Optional["ItemBase"]) -> bool: if parent.trash: return True - return self._isParentTrash(parent.parent()) # type: ignore[arg-type] + return self._isParentTrash(parent.parent()) def filterAcceptsRow( self, source_row: int, source_parent: QtCore.QModelIndex @@ -488,7 +491,7 @@ def filterAcceptsRow( # Assertion is there to satisfy mypy. item can be None, that is why we check before making the assertion if item is not None: assert isinstance(item, ItemBase) - if self._isParentTrash(parent) or getattr( # type: ignore[arg-type] + if self._isParentTrash(parent) or getattr( item, "trash", False ): # item could be None when it's trashed and hidden return False @@ -510,15 +513,15 @@ def lessThan(self, left: QtCore.QModelIndex, right: QtCore.QModelIndex) -> bool: rightItem = model.itemFromIndex(right) if hasattr(leftItem, "star") and hasattr(rightItem, "star"): if self.sortOrder() == QtCore.Qt.SortOrder.DescendingOrder: - if rightItem.star and not leftItem.star: # type: ignore[union-attr] + if rightItem.star and not leftItem.star: return True - elif not rightItem.star and leftItem.star: # type: ignore[union-attr] + elif not rightItem.star and leftItem.star: return False elif self.sortOrder() == QtCore.Qt.SortOrder.AscendingOrder: - if rightItem.star and not leftItem.star: # type: ignore[union-attr] + if rightItem.star and not leftItem.star: return False - elif not rightItem.star and leftItem.star: # type: ignore[union-attr] + elif not rightItem.star and leftItem.star: return True return super().lessThan(left, right) @@ -542,7 +545,7 @@ def __init__( super().__init__(parent=parent) # Indicates if a column is using delegates. - self.delegateColumns = delegateColumns + self.delegateColumns: List[int] = delegateColumns or [] self.lastSelectedItem = None # Stores the last collapsed state before a change in filtering to restore it afterwards. # The keys are persistent indexes from the original model (not the proxy one) and the values a bool @@ -563,8 +566,8 @@ def __init__( self.setSortingEnabled(False) # The tree should not have anything to do with filtering itself since that is left for the proxy model. - self.header().setSortIndicatorShown(True) # type: ignore[union-attr] - self.header().setSectionsClickable(True) # type: ignore[union-attr] + self.header().setSortIndicatorShown(True) + self.header().setSectionsClickable(True) self.setAlternatingRowColors(True) @@ -601,8 +604,8 @@ def fillCollapsedDict(self, parentItem: Optional[ItemBase] = None) -> None: proxyIndex = m.mapFromSource(index) if proxyIndex.isValid(): self.collapsedState[persistentIndex] = self.isExpanded(proxyIndex) - if item.hasChildren(): # type: ignore[union-attr] - self.fillCollapsedDict(item) # type: ignore[arg-type] + if item.hasChildren(): + self.fillCollapsedDict(item) else: for i in range(parentItem.rowCount()): child = parentItem.child(i, 0) @@ -613,8 +616,8 @@ def fillCollapsedDict(self, parentItem: Optional[ItemBase] = None) -> None: proxyIndex = m.mapFromSource(childIndex) if proxyIndex.isValid(): self.collapsedState[persistentIndex] = self.isExpanded(proxyIndex) - if child.hasChildren(): # type: ignore[union-attr] - self.fillCollapsedDict(child) # type: ignore[arg-type] + if child.hasChildren(): + self.fillCollapsedDict(child) @QtCore.Slot() def restoreCollapsedDict(self) -> None: @@ -623,24 +626,23 @@ def restoreCollapsedDict(self) -> None: the persistent editors and triggers a resizing of delegates. """ for persistentIndex, state in self.collapsedState.items(): - modelIndex = self.modelActual.index( # type: ignore[union-attr] + modelIndex = self.modelActual.index( persistentIndex.row(), persistentIndex.column(), persistentIndex.parent(), ) - item = self.modelActual.itemFromIndex(modelIndex) # type: ignore[union-attr] - proxyIndex = self.model().mapFromSource(modelIndex) # type: ignore[union-attr] + item = self.modelActual.itemFromIndex(modelIndex) + proxyIndex = self.model().mapFromSource(modelIndex) self.setExpanded(proxyIndex, state) if item.showDelegate: delegateIndexes = [ - self.modelActual.index( # type: ignore[union-attr] + self.modelActual.index( persistentIndex.row(), x, persistentIndex.parent() ) - for x in self.delegateColumns # type: ignore[union-attr] + for x in self.delegateColumns ] proxyDelegateIndexes = [ - self.model().mapFromSource(index) # type: ignore[union-attr] - for index in delegateIndexes + self.model().mapFromSource(index) for index in delegateIndexes ] for delegateIndex in proxyDelegateIndexes: self.openPersistentEditor(delegateIndex) @@ -655,14 +657,14 @@ def setAllDelegatesPersistent( :param parentIndex: If None, start the process. if it's an item, it will go through the children """ if parentIndex is None: - for i in range(self.model().rowCount()): # type: ignore[union-attr] - for column in self.delegateColumns: # type: ignore[union-attr] - index = self.model().index(i, column) # type: ignore[union-attr] - index0 = self.model().index( # type: ignore[union-attr] + for i in range(self.model().rowCount()): + for column in self.delegateColumns: + index = self.model().index(i, column) + index0 = self.model().index( i, 0 ) # Only items at column 0 hold children and model info - item0 = self.modelActual.itemFromIndex( # type: ignore[union-attr] - self.model().mapToSource(index0) # type: ignore[union-attr] + item0 = self.modelActual.itemFromIndex( + self.model().mapToSource(index0) ) if item0.showDelegate: self.openPersistentEditor(index) @@ -670,18 +672,18 @@ def setAllDelegatesPersistent( self.setAllDelegatesPersistent(index0) else: - parentItem = self.modelActual.itemFromIndex( # type: ignore[union-attr] - self.model().mapToSource(parentIndex) # type: ignore[union-attr] + parentItem = self.modelActual.itemFromIndex( + self.model().mapToSource(parentIndex) ) for i in range(parentItem.rowCount()): - for column in self.delegateColumns: # type: ignore[union-attr] + for column in self.delegateColumns: item = parentItem.child(i, column) item0 = parentItem.child(i, 0) - index = self.model().mapFromSource( # type: ignore[union-attr] - self.modelActual.indexFromItem(item) # type: ignore[union-attr] + index = self.model().mapFromSource( + self.modelActual.indexFromItem(item) ) - index0 = self.model().mapFromSource( # type: ignore[union-attr] - self.modelActual.indexFromItem(item0) # type: ignore[union-attr] + index0 = self.model().mapFromSource( + self.modelActual.indexFromItem(item0) ) if item0.showDelegate: self.openPersistentEditor(index) @@ -699,13 +701,13 @@ def onCheckDelegate(self, item: Optional["ItemBase"]) -> None: if item.showDelegate: row = item.row() parent = item.parent() - for column in self.delegateColumns: # type: ignore[union-attr] + for column in self.delegateColumns: if parent is None: - sibling = self.modelActual.item(row, column) # type: ignore[union-attr] + sibling = self.modelActual.item(row, column) else: sibling = parent.child(row, column) - index = self.model().mapFromSource( # type: ignore[union-attr] - self.modelActual.indexFromItem(sibling) # type: ignore[union-attr] + index = self.model().mapFromSource( + self.modelActual.indexFromItem(sibling) ) self.openPersistentEditor(index) self.scheduleDelayedItemsLayout() @@ -714,9 +716,9 @@ def onCheckDelegate(self, item: Optional["ItemBase"]) -> None: def onContextMenuRequested(self, pos: QtCore.QPoint) -> None: # We get the item from the real model, not the proxy model - originalModel = self.model().sourceModel() # type: ignore[union-attr] + originalModel = self.model().sourceModel() proxyIndex = self.indexAt(pos) - index = self.model().mapToSource(proxyIndex) # type: ignore[union-attr] + index = self.model().mapToSource(proxyIndex) # catch the case if the user rightcliks on any other column if index.column() != 0: @@ -797,7 +799,11 @@ def __init__( self.proxyModel = proxyModelType(self.model) self.view = viewType(self.proxyModel) - self.shortcutManager = shortcutManager if shortcutManager is not None else KeyboardShortcutManager() + self.shortcutManager = ( + shortcutManager + if shortcutManager is not None + else KeyboardShortcutManager() + ) self.layout_ = QtWidgets.QVBoxLayout() @@ -851,8 +857,8 @@ def makeToolbar(self) -> QtWidgets.QToolBar: QtGui.QIcon(":/icons/refresh.svg"), "refresh all items from the instrument", ) - refreshAction.triggered.connect(lambda x: self.refreshAll()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("refresh_all", refreshAction) # type: ignore[union-attr] + refreshAction.triggered.connect(lambda x: self.refreshAll()) + self.shortcutManager.apply_to_action("refresh_all", refreshAction) toolbar.addSeparator() @@ -860,31 +866,31 @@ def makeToolbar(self) -> QtWidgets.QToolBar: QtGui.QIcon(":/icons/expand.svg"), "expand tree", ) - expandAction.triggered.connect(lambda x: self.view.expandAll()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("expand_all", expandAction) # type: ignore[union-attr] + expandAction.triggered.connect(lambda x: self.view.expandAll()) + self.shortcutManager.apply_to_action("expand_all", expandAction) collapseAction = toolbar.addAction( QtGui.QIcon(":/icons/collapse.svg"), "collapse tree", ) - collapseAction.triggered.connect(lambda x: self.view.collapseAll()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("collapse_all", collapseAction) # type: ignore[union-attr] + collapseAction.triggered.connect(lambda x: self.view.collapseAll()) + self.shortcutManager.apply_to_action("collapse_all", collapseAction) toolbar.addSeparator() starAction = toolbar.addAction( QtGui.QIcon(":/icons/star.svg"), "Move Starred items to the top" ) - starAction.setCheckable(True) # type: ignore[union-attr] - starAction.triggered.connect(lambda x: self.promoteStar()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("toggle_star", starAction) # type: ignore[union-attr] + starAction.setCheckable(True) + starAction.triggered.connect(lambda x: self.promoteStar()) + self.shortcutManager.apply_to_action("toggle_star", starAction) trashAction = toolbar.addAction( QtGui.QIcon(":/icons/trash-crossed.svg"), "Hide trashed items" ) - trashAction.setCheckable(True) # type: ignore[union-attr] - trashAction.triggered.connect(lambda x: self.hideTrash()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("toggle_trash", trashAction) # type: ignore[union-attr] + trashAction.setCheckable(True) + trashAction.triggered.connect(lambda x: self.hideTrash()) + self.shortcutManager.apply_to_action("toggle_trash", trashAction) # Debugging tools keep commented for commits. # printAction = toolbar.addAction( @@ -917,7 +923,7 @@ def _starCurrentItem(self) -> None: source_index = self.proxyModel.mapToSource(proxy_index) if source_index.column() != 0: source_index = source_index.sibling(source_index.row(), 0) - item = self.model.itemFromIndex(source_index) # type: ignore[union-attr] + item = self.model.itemFromIndex(source_index) if isinstance(item, ItemBase): self.view.lastSelectedItem = item self.view.itemStarToggle.emit(item) @@ -930,7 +936,7 @@ def _trashCurrentItem(self) -> None: source_index = self.proxyModel.mapToSource(proxy_index) if source_index.column() != 0: source_index = source_index.sibling(source_index.row(), 0) - item = self.model.itemFromIndex(source_index) # type: ignore[union-attr] + item = self.model.itemFromIndex(source_index) if isinstance(item, ItemBase): self.view.lastSelectedItem = item self.view.itemTrashToggle.emit(item) @@ -963,13 +969,13 @@ def debuggingMethod(self) -> None: def fillChildren(parent: QtGui.QStandardItem) -> None: for i in range(parent.rowCount()): item = parent.child(i, 0) - items[item.name] = { # type: ignore[union-attr] + items[item.name] = { "item": item, - "star": item.star, # type: ignore[union-attr] - "trash": item.trash, # type: ignore[union-attr] + "star": item.star, + "trash": item.trash, } - if item.hasChildren(): # type: ignore[union-attr] - fillChildren(item) # type: ignore[arg-type] + if item.hasChildren(): + fillChildren(item) for i in range(self.model.rowCount()): item = self.model.item(i, 0) diff --git a/src/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py index 9b62444..6f2918c 100644 --- a/src/instrumentserver/gui/instruments.py +++ b/src/instrumentserver/gui/instruments.py @@ -158,7 +158,7 @@ def clear(self) -> None: self.unitEdit.setText("") if self.typeInput: self.typeSelect.setCurrentText( - parameterTypes[ParameterTypes.numeric]["name"] # type: ignore[arg-type] + parameterTypes[ParameterTypes.numeric]["name"] ) self.valsArgsEdit.setText("") @@ -287,7 +287,7 @@ def __init__(self, parent: Optional[QtCore.QObject] = None) -> None: # used to keep a reference to the widget. self.parameters: Dict[str, QtWidgets.QWidget] = {} - def createEditor( # type: ignore[override] + def createEditor( self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, @@ -297,10 +297,10 @@ def createEditor( # type: ignore[override] This is the function that is supposed to create the widget. It should return it. """ item = self.getItem(index) - element = item.element # type: ignore[attr-defined] + element = item.element ret = ParameterWidget(element, widget) - self.parameters[item.name] = ret # type: ignore[attr-defined] + self.parameters[item.name] = ret # Try to fetch and display current value immediately # ---- Chao: removed because the constructor of ParameterWidget object already calls parameter get ---- # if element.gettable: @@ -333,7 +333,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.subClient = SubClient([self.instrument.name], **subClientArgs) self.subClient.moveToThread(self.cliThread) - self.cliThread.started.connect(self.subClient.connect) # type: ignore[arg-type] + self.cliThread.started.connect(self.subClient.connect) self.subClient.update.connect(self.updateParameter) self.subClient.finished.connect(self.cliThread.quit) @@ -398,8 +398,8 @@ def insertItemTo( if item is not None: # A parameter might not have a unit unit = "" - if item.element is not None: # type: ignore[attr-defined] - unit = item.element.unit # type: ignore[attr-defined] + if item.element is not None: + unit = item.element.unit unitItem = QtGui.QStandardItem(unit) extraItem = QtGui.QStandardItem() @@ -483,7 +483,9 @@ def connectSignals(self) -> None: super().connectSignals() self.model.itemNewValue.connect(self.view.onItemNewValue) self.shortcutManager.register("refresh_item", self._refreshCurrentItem, self) - self.shortcutManager.register("toggle_python", self._togglePythonCurrentItem, self) + self.shortcutManager.register( + "toggle_python", self._togglePythonCurrentItem, self + ) @QtCore.Slot() def _refreshCurrentItem(self) -> None: @@ -524,18 +526,18 @@ class ParameterDeleteDelegate(ParameterDelegate): #: Emits the name of the parameter to be deleted when the user presses the delete button. removeParameter = QtCore.Signal(str) - def createEditor( # type: ignore[override] + def createEditor( self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex, ) -> QtWidgets.QWidget: item = self.getItem(index) - element = item.element # type: ignore[attr-defined] - rw = self.makeRemoveWidget(item.name, widget) # type: ignore[attr-defined] + element = item.element + rw = self.makeRemoveWidget(item.name, widget) ret = ParameterWidget(parameter=element, parent=widget, additionalWidgets=[rw]) - self.parameters[item.name] = ret # type: ignore[attr-defined] + self.parameters[item.name] = ret return ret @@ -583,7 +585,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.setEditable(False) - self.params = self.parent().instrument # type: ignore[union-attr] + self.params = self.parent().instrument self.refreshing = False loadingProfile = None @@ -675,15 +677,15 @@ def makeToolbar(self) -> QtWidgets.QToolBar: QtGui.QIcon(":/icons/load.svg"), "Load parameters from file", ) - loadParamAction.triggered.connect(lambda x: self.loadFromFile()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("load_items", loadParamAction) # type: ignore[union-attr] + loadParamAction.triggered.connect(lambda x: self.loadFromFile()) + self.shortcutManager.apply_to_action("load_items", loadParamAction) saveParamAction = toolbar.addAction( QtGui.QIcon(":/icons/save.svg"), "Save parameters to file", ) - saveParamAction.triggered.connect(lambda x: self.saveToFile()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("save_items", saveParamAction) # type: ignore[union-attr] + saveParamAction.triggered.connect(lambda x: self.saveToFile()) + self.shortcutManager.apply_to_action("save_items", saveParamAction) return toolbar @@ -768,22 +770,22 @@ def __init__(self, parent: Optional[QtCore.QObject] = None) -> None: self.methods: Dict[str, "MethodDisplay"] = {} - def createEditor( # type: ignore[override] + def createEditor( self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex, ) -> QtWidgets.QWidget: item = self.getItem(index) - element = item.element # type: ignore[attr-defined] - ret = MethodDisplay(element, item.name, parent=widget) # type: ignore[attr-defined] + element = item.element + ret = MethodDisplay(element, item.name, parent=widget) parent = self.parent() assert hasattr(parent, "clearAlertsAction") # connecting the widget with the clear alert signal - parent.clearAlertsAction.triggered.connect(ret.alertLabel.clearAlert) # type: ignore[union-attr] + parent.clearAlertsAction.triggered.connect(ret.alertLabel.clearAlert) - self.methods[item.name] = ret # type: ignore[attr-defined] + self.methods[item.name] = ret return ret @@ -883,7 +885,7 @@ def __init__( self.parametersList.view.resizeColumnToContents(1) self.methodsList.view.resizeColumnToContents(0) - def closeEvent(self, event: QtGui.QCloseEvent) -> None: # type: ignore[override] + def closeEvent(self, event: QtGui.QCloseEvent) -> None: """Stop the parameter subscriber thread before destruction.""" model = getattr(self.parametersList, "model", None) if model is not None and hasattr(model, "stopListener"): diff --git a/src/instrumentserver/gui/shortcuts.py b/src/instrumentserver/gui/shortcuts.py index 5912f8b..376f1cc 100644 --- a/src/instrumentserver/gui/shortcuts.py +++ b/src/instrumentserver/gui/shortcuts.py @@ -11,6 +11,7 @@ logger = logging.getLogger(__name__) + class KeyboardShortcutManager: """ Manages keyboard shortcut mappings for the instrument GUI. @@ -26,23 +27,23 @@ class KeyboardShortcutManager: REGISTRY: dict[str, tuple[str, str]] = { # action_id: (default_key_sequence, description) - "jump_filter": ("Ctrl+F", "Jump cursor to the filter search bar"), - "collapse_all": ("Ctrl+Shift+E", "Collapse all tree nodes"), - "expand_all": ("Ctrl+E", "Expand all tree nodes"), - "toggle_star": ("Ctrl+Shift+A", "Toggle star filter"), - "star_item": ("Ctrl+A", "Star/un-star the selected parameter"), - "toggle_trash": ("Ctrl+Shift+T", "Toggle trash filter"), - "trash_item": ("Ctrl+T", "Trash/un-trash the selected parameter"), - "refresh_all": ("Ctrl+Shift+R", "Refresh all parameters from instrument"), - "refresh_item": ("Ctrl+R", "Refresh the selected parameter"), - "toggle_python": ("Ctrl+P", "Toggle Python eval for selected parameter"), - "delete_item": ("Ctrl+Backspace", "Delete the selected parameter"), - "clear_add": ("Ctrl+Shift+N", "Clear regions of add parameter bar"), - "add_item": ("Ctrl+N", "Jump cursor to the add parameter bar"), - "load_items": ("Ctrl+O", "Load parameters from JSON file"), - "save_items": ("Ctrl+S", "Save parameters to JSON file"), - "fit_column": ("Ctrl+Shift+D", "Fits column width"), - "sort_column": ("Ctrl+D", "Toggle sorting of selected column") + "jump_filter": ("Ctrl+F", "Jump cursor to the filter search bar"), + "collapse_all": ("Ctrl+Shift+E", "Collapse all tree nodes"), + "expand_all": ("Ctrl+E", "Expand all tree nodes"), + "toggle_star": ("Ctrl+Shift+A", "Toggle star filter"), + "star_item": ("Ctrl+A", "Star/un-star the selected parameter"), + "toggle_trash": ("Ctrl+Shift+T", "Toggle trash filter"), + "trash_item": ("Ctrl+T", "Trash/un-trash the selected parameter"), + "refresh_all": ("Ctrl+Shift+R", "Refresh all parameters from instrument"), + "refresh_item": ("Ctrl+R", "Refresh the selected parameter"), + "toggle_python": ("Ctrl+P", "Toggle Python eval for selected parameter"), + "delete_item": ("Ctrl+Backspace", "Delete the selected parameter"), + "clear_add": ("Ctrl+Shift+N", "Clear regions of add parameter bar"), + "add_item": ("Ctrl+N", "Jump cursor to the add parameter bar"), + "load_items": ("Ctrl+O", "Load parameters from JSON file"), + "save_items": ("Ctrl+S", "Save parameters to JSON file"), + "fit_column": ("Ctrl+Shift+D", "Fits column width"), + "sort_column": ("Ctrl+D", "Toggle sorting of selected column"), } def __init__(self) -> None: @@ -61,8 +62,12 @@ def save(self, path: str) -> None: with open(path, "w") as f: json.dump(self.mapping, f, indent=2) - def apply_to_action(self, action_id: str, qaction: QtWidgets.QAction) -> None: + def apply_to_action( + self, action_id: str, qaction: Optional[QtWidgets.QAction] + ) -> None: """Set the shortcut from the current mapping on an existing QAction and retain a reference for live rebinding.""" + if qaction is None: + return key = self.mapping.get(action_id) if key: qaction.setShortcut(QtGui.QKeySequence(key)) @@ -124,10 +129,14 @@ def __init__( self._table = QtWidgets.QTableWidget(len(manager.REGISTRY), 4, self) self._table.setHorizontalHeaderLabels(["Action", "Description", "Shortcut", ""]) header = self._table.horizontalHeader() - header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.ResizeToContents) # type: ignore[union-attr] - header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeMode.ResizeToContents) # type: ignore[union-attr] - header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeMode.Stretch) # type: ignore[union-attr] - header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeMode.Fixed) # type: ignore[union-attr] + header.setSectionResizeMode( + 0, QtWidgets.QHeaderView.ResizeMode.ResizeToContents + ) + header.setSectionResizeMode( + 1, QtWidgets.QHeaderView.ResizeMode.ResizeToContents + ) + header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeMode.Fixed) self._table.setColumnWidth(3, 32) self._table.setSelectionBehavior( QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows @@ -166,9 +175,15 @@ def _populateTable(self) -> None: current = self.manager.mapping.get(action_id, "") id_item = QtWidgets.QTableWidgetItem(action_id) - id_item.setFlags(id_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) + id_item.setFlags( + QtCore.Qt.ItemFlags(id_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) + ) desc_item = QtWidgets.QTableWidgetItem(description) - desc_item.setFlags(desc_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) + desc_item.setFlags( + QtCore.Qt.ItemFlags( + desc_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable + ) + ) self._table.setItem(row, 0, id_item) self._table.setItem(row, 1, desc_item) @@ -221,8 +236,9 @@ def _updateAllIndicators(self) -> None: current = widget.keySequence().toString() if current in duplicates: others = [a for a in duplicates[current] if a != action_id] - self._applyIndicator(dot, "duplicate", - f"Duplicate: also bound to {', '.join(others)}") + self._applyIndicator( + dot, "duplicate", f"Duplicate: also bound to {', '.join(others)}" + ) elif current != self.manager.mapping.get(action_id, ""): self._applyIndicator(dot, "unsaved", "Unsaved change") else: @@ -245,20 +261,13 @@ def _onUnsavedChange(self) -> None: self._updateAllIndicators() def _onEditingFinished(self, widget: QtWidgets.QKeySequenceEdit) -> None: - # Capture the intended value before Qt resets the recording state. - # Block signals for one event-loop tick so the spurious keySequenceChanged - # that follows the internal reset never reaches _onUnsavedChange. intended = widget.keySequence().toString() widget.blockSignals(True) - QtCore.QTimer.singleShot( - 0, lambda: self._restoreAfterRevert(intended, widget) - ) + QtCore.QTimer.singleShot(0, lambda: self._restoreAfterRevert(intended, widget)) def _restoreAfterRevert( self, intended: str, widget: QtWidgets.QKeySequenceEdit ) -> None: - # If the widget reverted its stored sequence during the block window, - # put it back so the display and _save() read the correct value. if widget.keySequence().toString() != intended: widget.setKeySequence(QtGui.QKeySequence(intended)) widget.blockSignals(False) @@ -289,7 +298,10 @@ def _loadFromFile(self) -> None: def _saveToFile(self) -> None: self._save() path, _ = QtWidgets.QFileDialog.getSaveFileName( - self, "Save Shortcuts", "shortcuts.json", "JSON Files (*.json);;All Files (*)" + self, + "Save Shortcuts", + "shortcuts.json", + "JSON Files (*.json);;All Files (*)", ) if path: try: diff --git a/src/instrumentserver/server/application.py b/src/instrumentserver/server/application.py index 8cc2854..4bf2a42 100644 --- a/src/instrumentserver/server/application.py +++ b/src/instrumentserver/server/application.py @@ -17,7 +17,6 @@ from ..gui.shortcuts import KeyboardShortcutManager, ShortcutEditorWidget from .core import InstrumentModuleBluePrint, ParameterBluePrint, StationServer - logger = logging.getLogger(__name__) @@ -62,7 +61,7 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None: self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) self.customContextMenuRequested.connect( - lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) # type: ignore[arg-type] + lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) ) self.deleteAction.triggered.connect(self.onDeleteAction) self.itemSelectionChanged.connect(self._processSelection) @@ -131,7 +130,7 @@ class ServerStatus(QtWidgets.QWidget): def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None: super().__init__(parent) - self.layout = QtWidgets.QVBoxLayout(self) # type: ignore[assignment,method-assign] + self.layout = QtWidgets.QVBoxLayout(self) # At the top: a status label, and a button for emitting a test message self.addressLabel = QtWidgets.QLabel() @@ -145,13 +144,13 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None: ) ) - self.layout.addLayout(self.statusLayout) # type: ignore[attr-defined] + self.layout.addLayout(self.statusLayout) # next row: a window for displaying the incoming messages. - self.layout.addWidget(QtWidgets.QLabel("Messages:")) # type: ignore[attr-defined] + self.layout.addWidget(QtWidgets.QLabel("Messages:")) self.messages = QtWidgets.QTextEdit() self.messages.setReadOnly(True) - self.layout.addWidget(self.messages) # type: ignore[attr-defined] + self.layout.addWidget(self.messages) @QtCore.Slot(str) def setListeningAddress(self, addr: str) -> None: @@ -329,7 +328,7 @@ def __init__(self, guiConfig: Optional[dict] = None, *args: Any) -> None: self.contextMenu.addSeparator() self.contextMenu.addAction(self.deletePossibleInstrumentAction) self.customContextMenuRequested.connect( - lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) # type: ignore[arg-type] + lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) ) self.basedInstrumentAction.triggered.connect(self.onBasedInstrumentAction) @@ -433,16 +432,16 @@ def onRemoveInstrumentFromTree(self) -> None: for item in items: if item.childCount() == 0: parent = item.parent() - if item.configName is not None and item.configName in self.config: # type: ignore[attr-defined] - del self.config[item.configName] # type: ignore[attr-defined] - parent.removeChild(item) # type: ignore[union-attr] - if parent.childCount() == 0: # type: ignore[union-attr] + if item.configName is not None and item.configName in self.config: + del self.config[item.configName] + parent.removeChild(item) + if parent.childCount() == 0: self.takeTopLevelItem((self.indexOfTopLevelItem(parent))) else: for i in range(item.childCount()): child = item.child(i) - if child.configName in self.config: # type: ignore[union-attr] - del self.config[child.configName] # type: ignore[union-attr] + if child.configName in self.config: + del self.config[child.configName] self.takeTopLevelItem(self.indexOfTopLevelItem(item)) @@ -610,8 +609,8 @@ def __init__( else: self._guiConfig = guiConfig - self.stationServer = None - self.stationServerThread = None + self.stationServer: Optional[StationServer] = None + self.stationServerThread: Optional[QtCore.QThread] = None self.instrumentTabsOpen: dict[str, GenericInstrument] = {} @@ -667,31 +666,31 @@ def __init__( # Toolbar. self.toolBar = self.addToolBar("Tools") - self.toolBar.setIconSize(QtCore.QSize(16, 16)) # type: ignore[union-attr] + self.toolBar.setIconSize(QtCore.QSize(16, 16)) # Station tools. - self.toolBar.addWidget(QtWidgets.QLabel("Station:")) # type: ignore[union-attr] + self.toolBar.addWidget(QtWidgets.QLabel("Station:")) self.refreshStationAction = QtWidgets.QAction( QtGui.QIcon(":/icons/refresh.svg"), "Refresh", self ) self.refreshStationAction.triggered.connect(self.refreshStationComponents) - self.toolBar.addAction(self.refreshStationAction) # type: ignore[union-attr] + self.toolBar.addAction(self.refreshStationAction) # Parameter tools. - self.toolBar.addSeparator() # type: ignore[union-attr] - self.toolBar.addWidget(QtWidgets.QLabel("Params:")) # type: ignore[union-attr] + self.toolBar.addSeparator() + self.toolBar.addWidget(QtWidgets.QLabel("Params:")) self.loadParamsAction = QtWidgets.QAction( QtGui.QIcon(":/icons/load.svg"), "Load from file", self ) self.loadParamsAction.triggered.connect(self.loadParamsFromFile) - self.toolBar.addAction(self.loadParamsAction) # type: ignore[union-attr] + self.toolBar.addAction(self.loadParamsAction) self.saveParamsAction = QtWidgets.QAction( QtGui.QIcon(":/icons/save.svg"), "Save to file", self ) self.saveParamsAction.triggered.connect(self.saveParamsToFile) - self.toolBar.addAction(self.saveParamsAction) # type: ignore[union-attr] + self.toolBar.addAction(self.saveParamsAction) self.serverStatus.testButton.clicked.connect( lambda x: self.client.ask("Ping server.") @@ -710,7 +709,7 @@ def __init__( def log(self, message: str, level: LogLevels = LogLevels.info) -> None: log(logger, message, level) - def closeEvent(self, event: Optional[QtGui.QCloseEvent]) -> None: + def closeEvent(self, event: QtGui.QCloseEvent) -> None: for name, widget in list(self.instrumentTabsOpen.items()): try: widget.close() @@ -721,6 +720,7 @@ def closeEvent(self, event: Optional[QtGui.QCloseEvent]) -> None: if ( hasattr(self, "stationServerThread") and self.stationServerThread is not None + and self.stationServer is not None ): if self.stationServerThread.isRunning(): try: @@ -732,33 +732,39 @@ def closeEvent(self, event: Optional[QtGui.QCloseEvent]) -> None: self.client.disconnect() except Exception: pass - event.accept() # type: ignore[union-attr] + event.accept() def startServer(self) -> None: """Start the instrument server in a separate thread.""" - self.stationServer = StationServer(**self._serverKwargs) # type: ignore[assignment] - self.stationServerThread = QtCore.QThread() # type: ignore[assignment] - self.stationServer.moveToThread(self.stationServerThread) # type: ignore[attr-defined] - self.stationServerThread.started.connect(self.stationServer.startServer) # type: ignore[arg-type,attr-defined] - self.stationServer.finished.connect(lambda: self.log("ZMQ server closed.")) # type: ignore[attr-defined] - self.stationServer.finished.connect(self.stationServerThread.quit) # type: ignore[attr-defined] - self.stationServer.finished.connect(self.stationServer.deleteLater) # type: ignore[attr-defined] + self.stationServer = StationServer(**self._serverKwargs) + self.stationServerThread = QtCore.QThread() + assert self.stationServer is not None + assert self.stationServerThread is not None + self.stationServer.moveToThread(self.stationServerThread) + self.stationServerThread.started.connect(self.stationServer.startServer) + self.stationServer.finished.connect(lambda: self.log("ZMQ server closed.")) + self.stationServer.finished.connect(self.stationServerThread.quit) + self.stationServer.finished.connect(self.stationServer.deleteLater) # Connecting some additional things for messages. - self.stationServer.serverStarted.connect(self.serverStatus.setListeningAddress) # type: ignore[attr-defined] - self.stationServer.serverStarted.connect(self.client.start) # type: ignore[attr-defined] - self.stationServer.serverStarted.connect(self.refreshStationComponents) # type: ignore[attr-defined] - self.stationServer.finished.connect( # type: ignore[attr-defined] + self.stationServer.serverStarted.connect(self.serverStatus.setListeningAddress) + self.stationServer.serverStarted.connect(self.client.start) + self.stationServer.serverStarted.connect(self.refreshStationComponents) + self.stationServer.finished.connect( lambda: self.log("Server thread finished.", LogLevels.info) ) - self.stationServer.messageReceived.connect(self._messageReceived) # type: ignore[attr-defined] - self.stationServer.instrumentCreated.connect(self.addInstrumentToGui) # type: ignore[attr-defined] - self.stationServer.funcCalled.connect(self.onFuncCalled) # type: ignore[attr-defined] + self.stationServer.messageReceived.connect(self._messageReceived) + self.stationServer.instrumentCreated.connect(self.addInstrumentToGui) + self.stationServer.funcCalled.connect(self.onFuncCalled) - self.stationServerThread.start() # type: ignore[attr-defined] + self.stationServerThread.start() def getServerIfRunning(self) -> Optional["StationServer"]: - if self.stationServer is not None and self.stationServerThread.isRunning(): # type: ignore[union-attr] + if ( + self.stationServer is not None + and self.stationServerThread is not None + and self.stationServerThread.isRunning() + ): return self.stationServer else: return None @@ -866,7 +872,7 @@ def displayComponentInfo(self, name: Union[str, None]) -> None: bp = self._bluePrints[name] else: bp = None - self.stationObjInfo.setObject(bp) # type: ignore[arg-type] + self.stationObjInfo.setObject(bp) @QtCore.Slot(QtWidgets.QTreeWidgetItem, int) def addInstrumentTab(self, item: QtWidgets.QTreeWidgetItem, index: int) -> None: @@ -894,7 +900,10 @@ def addInstrumentTab(self, item: QtWidgets.QTreeWidgetItem, index: int) -> None: if "kwargs" in self._guiConfig[name]["gui"]: kwargs = self._guiConfig[name]["gui"]["kwargs"] - kwargs["sub_port"] = kwargs.get("sub_port", self.stationServer.port + 1) # type: ignore[union-attr] + station_server = self.stationServer + if station_server is None: + raise RuntimeError("addInstrumentToGui called before server started") + kwargs["sub_port"] = kwargs.get("sub_port", station_server.port + 1) kwargs["shortcutManager"] = self.shortcutManager insWidget = widgetClass(ins, parent=self, **kwargs) index = self.tabs.addTab(insWidget, ins.name) @@ -953,15 +962,15 @@ def __init__(self, host: str = "localhost", port: int = 5555) -> None: # Toolbar. self.toolBar = self.addToolBar("Tools") - self.toolBar.setIconSize(QtCore.QSize(16, 16)) # type: ignore[union-attr] + self.toolBar.setIconSize(QtCore.QSize(16, 16)) # Station tools. - self.toolBar.addWidget(QtWidgets.QLabel("Station:")) # type: ignore[union-attr] + self.toolBar.addWidget(QtWidgets.QLabel("Station:")) self.refreshStationAction = QtWidgets.QAction( QtGui.QIcon(":/icons/refresh.svg"), "Refresh", self ) self.refreshStationAction.triggered.connect(self.refreshStationComponents) - self.toolBar.addAction(self.refreshStationAction) # type: ignore[union-attr] + self.toolBar.addAction(self.refreshStationAction) self.refreshStationComponents() From b618362ae8cae300d15e6c2c7848a9a4cb6e4234 Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Mon, 18 May 2026 17:25:39 -0400 Subject: [PATCH 05/12] shortcuts now stored in server config file instead of separate JSON file --- src/instrumentserver/apps.py | 9 +++- src/instrumentserver/client/proxy.py | 4 +- src/instrumentserver/config.py | 10 +++- src/instrumentserver/gui/base_instrument.py | 2 +- src/instrumentserver/gui/shortcuts.py | 57 ++++++++------------- src/instrumentserver/server/application.py | 8 ++- 6 files changed, 47 insertions(+), 43 deletions(-) diff --git a/src/instrumentserver/apps.py b/src/instrumentserver/apps.py index 0d881d1..e624e42 100644 --- a/src/instrumentserver/apps.py +++ b/src/instrumentserver/apps.py @@ -63,14 +63,15 @@ def serverScript() -> None: stationConfig, serverConfig, guiConfig, + shortcutConfig, tempFile, pollingRates, pollingThread, ipAddresses, - ) = None, None, None, None, None, None, None + ) = None, None, None, None, None, None, None, None if configPath != "": # Separates the corresponding settings into the 5 necessary parts - stationConfig, serverConfig, guiConfig, tempFile, pollingRates, ipAddresses = ( + stationConfig, serverConfig, guiConfig, shortcutConfig, tempFile, pollingRates, ipAddresses = ( loadConfig(configPath) ) if pollingRates is not None and pollingRates != {}: @@ -89,8 +90,10 @@ def serverScript() -> None: serverConfig=serverConfig, stationConfig=stationConfig, guiConfig=guiConfig, + shortcutConfig=shortcutConfig, pollingThread=pollingThread, ipAddresses=ipAddresses, + configPath=configPath ) else: serverWithGui( @@ -100,8 +103,10 @@ def serverScript() -> None: serverConfig=serverConfig, stationConfig=stationConfig, guiConfig=guiConfig, + shortcutConfig=shortcutConfig, pollingThread=pollingThread, ipAddresses=ipAddresses, + configPath=configPath ) # Close and delete the temporary files diff --git a/src/instrumentserver/client/proxy.py b/src/instrumentserver/client/proxy.py index 3c43d74..391542c 100644 --- a/src/instrumentserver/client/proxy.py +++ b/src/instrumentserver/client/proxy.py @@ -18,7 +18,7 @@ import qcodes as qc import zmq from qcodes import Instrument, Parameter -from qcodes.instrument.base import InstrumentBase +from qcodes.instrument import InstrumentBase from instrumentserver import DEFAULT_PORT, QtCore from instrumentserver.helpers import flat_to_nested_dict, flatten_dict, is_flat_dict @@ -859,7 +859,7 @@ def __init__( # Use config.py to parse server config format from instrumentserver.config import loadConfig - _, serverConfig, fullConfig, tempFile, _, _ = loadConfig(config_path) + _, serverConfig, fullConfig, _, tempFile, _, _ = loadConfig(config_path) tempFile.close() # Clean up temp file self.full_config = fullConfig diff --git a/src/instrumentserver/config.py b/src/instrumentserver/config.py index d177c87..2f8a2a2 100644 --- a/src/instrumentserver/config.py +++ b/src/instrumentserver/config.py @@ -18,7 +18,7 @@ GUIFIELD = {"type": "instrumentserver.gui.instruments.GenericInstrument", "kwargs": {}} -def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, IO[bytes], dict, dict]: +def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, dict, IO[bytes], dict, dict]: """ Loads the config for the instrumentserver. From 1 config file it splits the respective fields into 3 different objects: a serverConfig (the configurations for the server), a stationConfig(the qcodes station config file clean @@ -36,6 +36,7 @@ def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, IO[bytes], dict serverConfig: dict = {} # Config for the server guiConfig = {} # Individual gui config of each instrument fullConfig = {} # serverConfig + guiConfig + any unfilled fields. Used for creating instruments from the gui + shortcutConfig = {} # Preferences for keyboard shortcuts pollingRates = {} # Polling rates for each parameter ipAddresses = {} # Dictionary of IP Addresses to send broadcasts to: # externalBroadcast: where to externally send parameter change broadcasts to, formatted like "tcp://address:port" @@ -149,6 +150,11 @@ def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, IO[bytes], dict # Update fullConfig with merged GUI config fullConfig[instrumentName]["gui"] = guiConfig[instrumentName] + + # Gets all shortcuts different to REGISTRY defaults from the config file + if "shortcuts" in rawConfig: + shortcutConfig = rawConfig["shortcuts"] + rawConfig.pop("shortcuts") # Gets all of the broadcasting and listening addresses from the config file if "networking" in rawConfig: @@ -170,4 +176,4 @@ def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, IO[bytes], dict tempFilePath = tempFile.name # You need to return the tempFile itself so that the garbage collector doesn't touch it - return tempFilePath, serverConfig, fullConfig, tempFile, pollingRates, ipAddresses + return tempFilePath, serverConfig, fullConfig, shortcutConfig, tempFile, pollingRates, ipAddresses diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index fc15dcd..7b38b42 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -108,7 +108,7 @@ from instrumentserver import QtCore, QtGui, QtWidgets -from .shortcuts import KeyboardShortcutManager +from instrumentserver.gui.shortcuts import KeyboardShortcutManager class ItemBase(QtGui.QStandardItem): diff --git a/src/instrumentserver/gui/shortcuts.py b/src/instrumentserver/gui/shortcuts.py index 376f1cc..8d33eb5 100644 --- a/src/instrumentserver/gui/shortcuts.py +++ b/src/instrumentserver/gui/shortcuts.py @@ -1,4 +1,4 @@ -import json +import yaml import logging import os from collections import defaultdict @@ -51,16 +51,22 @@ def __init__(self) -> None: self._shortcut_map: dict[str, QtWidgets.QShortcut] = {} self._action_map: dict[str, QtWidgets.QAction] = {} - def load(self, path: str) -> None: - """Override the current mapping with entries read from a JSON file.""" - with open(path) as f: - data = json.load(f) - self.mapping.update(data) + def load_from_dict(self, config) -> None: + """Override the current mapping with entries read from serverConfig file.""" + self.mapping.update(config) def save(self, path: str) -> None: - """Write the current mapping to a JSON file.""" + """Write the current mapping to the serverConfig file.""" + with open(path, "r") as f: + data = yaml.safe_load(f) or {} + + diffs = {k: v for k, v in self.mapping.items() if v != self.REGISTRY[k][0]} + if diffs: + data["shortcuts"] = diffs + elif "shortcuts" in data: + del data["shortcuts"] with open(path, "w") as f: - json.dump(self.mapping, f, indent=2) + yaml.dump(data, f, indent=2) def apply_to_action( self, action_id: str, qaction: Optional[QtWidgets.QAction] @@ -105,7 +111,7 @@ class ShortcutEditorWidget(QtWidgets.QWidget): Intended to be embedded as a tab in the server window. Changes made in the table are applied live to the manager (and therefore all registered shortcuts) - when Save is clicked. Use 'Save to file' / 'Load from file' to persist across sessions. + when Save is clicked. Use 'Save to file' to persist across sessions. Each row has a small colored indicator dot in the rightmost column: - white : saved and unique @@ -121,6 +127,7 @@ class ShortcutEditorWidget(QtWidgets.QWidget): def __init__( self, manager: KeyboardShortcutManager, + configPath: str, parent: Optional[QtWidgets.QWidget] = None, ) -> None: super().__init__(parent) @@ -145,8 +152,6 @@ def __init__( self._indicators: list[QtWidgets.QLabel] = [] self._populateTable() - btnLoad = QtWidgets.QPushButton("Load from file") - btnLoad.clicked.connect(self._loadFromFile) btnSaveFile = QtWidgets.QPushButton("Save to file") btnSaveFile.clicked.connect(self._saveToFile) btnReset = QtWidgets.QPushButton("Reset to defaults") @@ -155,7 +160,6 @@ def __init__( btnSave.clicked.connect(self._save) btnRow = QtWidgets.QHBoxLayout() - btnRow.addWidget(btnLoad) btnRow.addWidget(btnSaveFile) btnRow.addStretch() btnRow.addWidget(btnReset) @@ -166,6 +170,8 @@ def __init__( layout.addLayout(btnRow) self.setLayout(layout) + self.configPath = configPath + def _populateTable(self) -> None: self._indicators.clear() self._table.clearContents() @@ -281,34 +287,15 @@ def _save(self) -> None: self._updateAllIndicators() logger.info("Shortcuts saved locally") - @QtCore.Slot() - def _loadFromFile(self) -> None: - path, _ = QtWidgets.QFileDialog.getOpenFileName( - self, "Load Shortcuts", ".", "JSON Files (*.json);;All Files (*)" - ) - if path: - try: - self.manager.load(path) - self._populateTable() - logger.info(f"Loaded shortcuts from {path}") - except Exception as e: - logger.warning(f"Failed to load shortcuts from {path}: {e}") - @QtCore.Slot() def _saveToFile(self) -> None: self._save() - path, _ = QtWidgets.QFileDialog.getSaveFileName( - self, - "Save Shortcuts", - "shortcuts.json", - "JSON Files (*.json);;All Files (*)", - ) - if path: + if self.configPath: try: - self.manager.save(path) - logger.info(f"Saved shortcuts to {path}") + self.manager.save(self.configPath) + logger.info(f"Saved shortcuts to {self.configPath}") except Exception as e: - logger.warning(f"Failed to save shortcuts to {path}: {e}") + logger.warning(f"Failed to save shortcuts to {self.configPath}: {e}") @QtCore.Slot() def _resetDefaults(self) -> None: diff --git a/src/instrumentserver/server/application.py b/src/instrumentserver/server/application.py index 4bf2a42..3bd56ce 100644 --- a/src/instrumentserver/server/application.py +++ b/src/instrumentserver/server/application.py @@ -609,6 +609,9 @@ def __init__( else: self._guiConfig = guiConfig + shortcutConfig = serverKwargs.pop("shortcutConfig", {}) + configPath = serverKwargs.pop("configPath", None) + self.stationServer: Optional[StationServer] = None self.stationServerThread: Optional[QtCore.QThread] = None @@ -661,7 +664,10 @@ def __init__( self.tabs.addUnclosableTab(self.serverStatus, "Server") self.shortcutManager = KeyboardShortcutManager() - self.shortcutEditor = ShortcutEditorWidget(self.shortcutManager) + if shortcutConfig: + self.shortcutManager.load_from_dict(shortcutConfig) + + self.shortcutEditor = ShortcutEditorWidget(self.shortcutManager, configPath) self.tabs.addUnclosableTab(self.shortcutEditor, "Shortcuts") # Toolbar. From d0ffee545759f904787340b7a14dca6f5761bdb8 Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Tue, 19 May 2026 21:20:09 -0400 Subject: [PATCH 06/12] can now click right to jump focus to parameters value. addressed feedback --- src/instrumentserver/gui/base_instrument.py | 26 ++++++--------------- src/instrumentserver/gui/instruments.py | 25 ++++++++++++++++++++ src/instrumentserver/gui/shortcuts.py | 10 +++++--- src/instrumentserver/server/application.py | 6 ++--- 4 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index 7b38b42..aab971a 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -107,7 +107,6 @@ from typing import Any, Dict, List, Optional, cast from instrumentserver import QtCore, QtGui, QtWidgets - from instrumentserver.gui.shortcuts import KeyboardShortcutManager @@ -225,7 +224,7 @@ def __init__( self.loadingItems = False @staticmethod - def _matches_any_pattern(name: str, patterns: Optional[List[str]]) -> bool: + def _matches_any_pattern(name: str, patterns: List[str]) -> bool: """ Check if a name matches any glob pattern in the list. @@ -545,7 +544,7 @@ def __init__( super().__init__(parent=parent) # Indicates if a column is using delegates. - self.delegateColumns: List[int] = delegateColumns or [] + self.delegateColumns: List[int] = delegateColumns self.lastSelectedItem = None # Stores the last collapsed state before a change in filtering to restore it afterwards. # The keys are persistent indexes from the original model (not the proxy one) and the values a bool @@ -772,6 +771,7 @@ class InstrumentDisplayBase(QtWidgets.QWidget): :param proxyModelType: The type of proxy model that should be used. :param viewType: The type of view that should be used. :param callSignals: If False, the constructor will not call the method connectSignals + :param shortcutManager: Manager shared across the application so actions can be registered to shortcuts """ def __init__( @@ -783,7 +783,7 @@ def __init__( proxyModelType: type = InstrumentSortFilterProxyModel, viewType: type = InstrumentTreeViewBase, callSignals: bool = True, - shortcutManager: Optional[KeyboardShortcutManager] = None, + shortcutManager: type = KeyboardShortcutManager, parent: Optional[QtWidgets.QWidget] = None, **modelKwargs: Any, ) -> None: @@ -799,11 +799,7 @@ def __init__( self.proxyModel = proxyModelType(self.model) self.view = viewType(self.proxyModel) - self.shortcutManager = ( - shortcutManager - if shortcutManager is not None - else KeyboardShortcutManager() - ) + self.shortcutManager = shortcutManager self.layout_ = QtWidgets.QVBoxLayout() @@ -882,14 +878,14 @@ def makeToolbar(self) -> QtWidgets.QToolBar: QtGui.QIcon(":/icons/star.svg"), "Move Starred items to the top" ) starAction.setCheckable(True) - starAction.triggered.connect(lambda x: self.promoteStar()) + starAction.triggered.connect(lambda x: self.proxyModel.onToggleStar()) self.shortcutManager.apply_to_action("toggle_star", starAction) trashAction = toolbar.addAction( QtGui.QIcon(":/icons/trash-crossed.svg"), "Hide trashed items" ) trashAction.setCheckable(True) - trashAction.triggered.connect(lambda x: self.hideTrash()) + trashAction.triggered.connect(lambda x: self.proxyModel.onToggleTrash()) self.shortcutManager.apply_to_action("toggle_trash", trashAction) # Debugging tools keep commented for commits. @@ -903,14 +899,6 @@ def makeToolbar(self) -> QtWidgets.QToolBar: return toolbar - @QtCore.Slot() - def hideTrash(self) -> None: - self.proxyModel.onToggleTrash() - - @QtCore.Slot() - def promoteStar(self) -> None: - self.proxyModel.onToggleStar() - @QtCore.Slot() def refreshAll(self) -> None: self.model.refreshAll() diff --git a/src/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py index 6f2918c..6dbc151 100644 --- a/src/instrumentserver/gui/instruments.py +++ b/src/instrumentserver/gui/instruments.py @@ -466,6 +466,7 @@ def __init__( modelKwargs["sub_port"] = kwargs.pop("sub_port") shortcutManager = kwargs.pop("shortcutManager", None) + print(shortcutManager) super().__init__( instrument=instrument, @@ -486,6 +487,11 @@ def connectSignals(self) -> None: self.shortcutManager.register( "toggle_python", self._togglePythonCurrentItem, self ) + self.shortcutManager.register("edit_value", self._focusToParameterValue, self) + print( + "edit_value registered", + self.shortcutManager._shortcut_map.get("edit_value"), + ) @QtCore.Slot() def _refreshCurrentItem(self) -> None: @@ -515,6 +521,25 @@ def _togglePythonCurrentItem(self) -> None: if widget is not None and isinstance(widget.paramWidget, AnyInput): widget.paramWidget.doEval.toggle() + @QtCore.Slot() + def _focusToParameterValue(self) -> None: + logger.debug("test") + proxy_index = self.view.currentIndex() + if not proxy_index.isValid(): + return + source_index = self.proxyModel.mapToSource(proxy_index) + if source_index.column() != 0: + source_index = source_index.sibling(source_index.row(), 0) + item = self.model.itemFromIndex(source_index) + if isinstance(item, ItemBase): + widget = self.view.delegate.parameters.get(item.name) + if widget and hasattr(widget, "paramWidget"): + pw = widget.paramWidget + if isinstance(pw, AnyInput): + pw.input.setFocus() + else: + pw.setFocus() + # ----------------- Parameters Display Classes - Ending -------------------------------- diff --git a/src/instrumentserver/gui/shortcuts.py b/src/instrumentserver/gui/shortcuts.py index 8d33eb5..163011c 100644 --- a/src/instrumentserver/gui/shortcuts.py +++ b/src/instrumentserver/gui/shortcuts.py @@ -1,9 +1,10 @@ -import yaml import logging import os from collections import defaultdict from typing import Callable, Optional +import yaml + from instrumentserver import QtCore, QtGui, QtWidgets, getInstrumentserverPath _ICON_DIR = getInstrumentserverPath("resource", "icons") @@ -44,6 +45,7 @@ class KeyboardShortcutManager: "save_items": ("Ctrl+S", "Save parameters to JSON file"), "fit_column": ("Ctrl+Shift+D", "Fits column width"), "sort_column": ("Ctrl+D", "Toggle sorting of selected column"), + "edit_value": ("Right", "Jump cursor to value field for selected parameter"), } def __init__(self) -> None: @@ -111,7 +113,7 @@ class ShortcutEditorWidget(QtWidgets.QWidget): Intended to be embedded as a tab in the server window. Changes made in the table are applied live to the manager (and therefore all registered shortcuts) - when Save is clicked. Use 'Save to file' to persist across sessions. + when Save is clicked. Use 'Save to file' to persist across sessions. Each row has a small colored indicator dot in the rightmost column: - white : saved and unique @@ -182,7 +184,9 @@ def _populateTable(self) -> None: id_item = QtWidgets.QTableWidgetItem(action_id) id_item.setFlags( - QtCore.Qt.ItemFlags(id_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) + QtCore.Qt.ItemFlags( + id_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable + ) ) desc_item = QtWidgets.QTableWidgetItem(description) desc_item.setFlags( diff --git a/src/instrumentserver/server/application.py b/src/instrumentserver/server/application.py index 3bd56ce..d574d52 100644 --- a/src/instrumentserver/server/application.py +++ b/src/instrumentserver/server/application.py @@ -906,11 +906,9 @@ def addInstrumentTab(self, item: QtWidgets.QTreeWidgetItem, index: int) -> None: if "kwargs" in self._guiConfig[name]["gui"]: kwargs = self._guiConfig[name]["gui"]["kwargs"] - station_server = self.stationServer - if station_server is None: - raise RuntimeError("addInstrumentToGui called before server started") - kwargs["sub_port"] = kwargs.get("sub_port", station_server.port + 1) + kwargs["sub_port"] = kwargs.get("sub_port", self.stationServer.port + 1) # type: ignore[union-attr] kwargs["shortcutManager"] = self.shortcutManager + insWidget = widgetClass(ins, parent=self, **kwargs) index = self.tabs.addTab(insWidget, ins.name) self.instrumentTabsOpen[ins.name] = insWidget From e87e19ceea60674e76125f624b8b0151b54859c7 Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Wed, 20 May 2026 21:57:02 -0400 Subject: [PATCH 07/12] getting tests to pass part 1 --- src/instrumentserver/gui/base_instrument.py | 130 ++++++++++---------- src/instrumentserver/gui/instruments.py | 45 +++---- src/instrumentserver/gui/shortcuts.py | 17 +-- src/instrumentserver/server/application.py | 82 ++++++------ 4 files changed, 131 insertions(+), 143 deletions(-) diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index aab971a..9dad22c 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -150,8 +150,8 @@ class DelegateBase(QtWidgets.QStyledItemDelegate): def getItem(cls, QModelIndex: QtCore.QModelIndex) -> QtGui.QStandardItem: proxyModel = QModelIndex.model() - model = proxyModel.sourceModel() - item = model.itemFromIndex(proxyModel.mapToSource(QModelIndex)) + model = proxyModel.sourceModel() # type: ignore[union-attr] + item = model.itemFromIndex(proxyModel.mapToSource(QModelIndex)) # type: ignore[union-attr] if item.column != 0: parent = item.parent() row = item.row() @@ -261,13 +261,13 @@ def loadItems(self, module: Any = None, prefix: Optional[str] = None) -> None: # constructor if prefix is not None: objectName = ".".join([prefix, objectName]) - if not self._matches_any_pattern(objectName, self.itemsHide): + if not self._matches_any_pattern(objectName, self.itemsHide): # type: ignore[arg-type] item = self.addItem( fullName=objectName, star=False, trash=False, element=obj ) - if self._matches_any_pattern(objectName, self.itemsTrash): + if self._matches_any_pattern(objectName, self.itemsTrash): # type: ignore[arg-type] self.onItemTrashToggle(item) - if self._matches_any_pattern(objectName, self.itemsStar): + if self._matches_any_pattern(objectName, self.itemsStar): # type: ignore[arg-type] self.onItemStarToggle(item) for submodName, submod in module.submodules.items(): @@ -334,20 +334,20 @@ def addItem(self, fullName: str, **kwargs: Any) -> "ItemBase": ) # submodules get directly added here and not in the load function, so need to have it here too. if self.loadingItems: - if not self._matches_any_pattern(smName, self.itemsHide): - self.insertItemTo(parent, subModItem) - if self._matches_any_pattern(smName, self.itemsTrash): + if not self._matches_any_pattern(smName, self.itemsHide): # type: ignore[arg-type] + self.insertItemTo(parent, subModItem) # type: ignore[arg-type] + if self._matches_any_pattern(smName, self.itemsTrash): # type: ignore[arg-type] self.onItemTrashToggle(subModItem) - if self._matches_any_pattern(smName, self.itemsStar): + if self._matches_any_pattern(smName, self.itemsStar): # type: ignore[arg-type] self.onItemStarToggle(subModItem) else: - self.insertItemTo(parent, subModItem) - parent = subModItem + self.insertItemTo(parent, subModItem) # type: ignore[arg-type] + parent = subModItem # type: ignore[assignment] else: - parent = items[0] + parent = items[0] # type: ignore[assignment] newItem = self.itemClass(name=fullName, **kwargs) - self.insertItemTo(parent, newItem) + self.insertItemTo(parent, newItem) # type: ignore[arg-type] return newItem @@ -467,7 +467,7 @@ def _isParentTrash(self, parent: Optional["ItemBase"]) -> bool: if parent.trash: return True - return self._isParentTrash(parent.parent()) + return self._isParentTrash(parent.parent()) # type: ignore[arg-type] def filterAcceptsRow( self, source_row: int, source_parent: QtCore.QModelIndex @@ -490,7 +490,7 @@ def filterAcceptsRow( # Assertion is there to satisfy mypy. item can be None, that is why we check before making the assertion if item is not None: assert isinstance(item, ItemBase) - if self._isParentTrash(parent) or getattr( + if self._isParentTrash(parent) or getattr( # type: ignore[arg-type] item, "trash", False ): # item could be None when it's trashed and hidden return False @@ -512,15 +512,15 @@ def lessThan(self, left: QtCore.QModelIndex, right: QtCore.QModelIndex) -> bool: rightItem = model.itemFromIndex(right) if hasattr(leftItem, "star") and hasattr(rightItem, "star"): if self.sortOrder() == QtCore.Qt.SortOrder.DescendingOrder: - if rightItem.star and not leftItem.star: + if rightItem.star and not leftItem.star: # type: ignore[union-attr] return True - elif not rightItem.star and leftItem.star: + elif not rightItem.star and leftItem.star: # type: ignore[union-attr] return False elif self.sortOrder() == QtCore.Qt.SortOrder.AscendingOrder: - if rightItem.star and not leftItem.star: + if rightItem.star and not leftItem.star: # type: ignore[union-attr] return False - elif not rightItem.star and leftItem.star: + elif not rightItem.star and leftItem.star: # type: ignore[union-attr] return True return super().lessThan(left, right) @@ -544,7 +544,7 @@ def __init__( super().__init__(parent=parent) # Indicates if a column is using delegates. - self.delegateColumns: List[int] = delegateColumns + self.delegateColumns = delegateColumns self.lastSelectedItem = None # Stores the last collapsed state before a change in filtering to restore it afterwards. # The keys are persistent indexes from the original model (not the proxy one) and the values a bool @@ -565,8 +565,8 @@ def __init__( self.setSortingEnabled(False) # The tree should not have anything to do with filtering itself since that is left for the proxy model. - self.header().setSortIndicatorShown(True) - self.header().setSectionsClickable(True) + self.header().setSortIndicatorShown(True) # type: ignore[union-attr] + self.header().setSectionsClickable(True) # type: ignore[union-attr] self.setAlternatingRowColors(True) @@ -603,8 +603,8 @@ def fillCollapsedDict(self, parentItem: Optional[ItemBase] = None) -> None: proxyIndex = m.mapFromSource(index) if proxyIndex.isValid(): self.collapsedState[persistentIndex] = self.isExpanded(proxyIndex) - if item.hasChildren(): - self.fillCollapsedDict(item) + if item.hasChildren(): # type: ignore[union-attr] + self.fillCollapsedDict(item) # type: ignore[arg-type] else: for i in range(parentItem.rowCount()): child = parentItem.child(i, 0) @@ -615,8 +615,8 @@ def fillCollapsedDict(self, parentItem: Optional[ItemBase] = None) -> None: proxyIndex = m.mapFromSource(childIndex) if proxyIndex.isValid(): self.collapsedState[persistentIndex] = self.isExpanded(proxyIndex) - if child.hasChildren(): - self.fillCollapsedDict(child) + if child.hasChildren(): # type: ignore[union-attr] + self.fillCollapsedDict(child) # type: ignore[arg-type] @QtCore.Slot() def restoreCollapsedDict(self) -> None: @@ -625,23 +625,23 @@ def restoreCollapsedDict(self) -> None: the persistent editors and triggers a resizing of delegates. """ for persistentIndex, state in self.collapsedState.items(): - modelIndex = self.modelActual.index( + modelIndex = self.modelActual.index( # type: ignore[union-attr] persistentIndex.row(), persistentIndex.column(), persistentIndex.parent(), ) - item = self.modelActual.itemFromIndex(modelIndex) - proxyIndex = self.model().mapFromSource(modelIndex) + item = self.modelActual.itemFromIndex(modelIndex) # type: ignore[union-attr] + proxyIndex = self.model().mapFromSource(modelIndex) # type: ignore[union-attr] self.setExpanded(proxyIndex, state) if item.showDelegate: delegateIndexes = [ - self.modelActual.index( + self.modelActual.index( # type: ignore[union-attr] persistentIndex.row(), x, persistentIndex.parent() ) - for x in self.delegateColumns + for x in self.delegateColumns # type: ignore[union-attr] ] proxyDelegateIndexes = [ - self.model().mapFromSource(index) for index in delegateIndexes + self.model().mapFromSource(index) for index in delegateIndexes # type: ignore[union-attr] ] for delegateIndex in proxyDelegateIndexes: self.openPersistentEditor(delegateIndex) @@ -656,14 +656,14 @@ def setAllDelegatesPersistent( :param parentIndex: If None, start the process. if it's an item, it will go through the children """ if parentIndex is None: - for i in range(self.model().rowCount()): - for column in self.delegateColumns: - index = self.model().index(i, column) - index0 = self.model().index( + for i in range(self.model().rowCount()): # type: ignore[union-attr] + for column in self.delegateColumns: # type: ignore[union-attr] + index = self.model().index(i, column) # type: ignore[union-attr] + index0 = self.model().index( # type: ignore[union-attr] i, 0 ) # Only items at column 0 hold children and model info - item0 = self.modelActual.itemFromIndex( - self.model().mapToSource(index0) + item0 = self.modelActual.itemFromIndex( # type: ignore[union-attr] + self.model().mapToSource(index0) # type: ignore[union-attr] ) if item0.showDelegate: self.openPersistentEditor(index) @@ -671,18 +671,18 @@ def setAllDelegatesPersistent( self.setAllDelegatesPersistent(index0) else: - parentItem = self.modelActual.itemFromIndex( - self.model().mapToSource(parentIndex) + parentItem = self.modelActual.itemFromIndex( # type: ignore[union-attr] + self.model().mapToSource(parentIndex) # type: ignore[union-attr] ) for i in range(parentItem.rowCount()): - for column in self.delegateColumns: + for column in self.delegateColumns: # type: ignore[union-attr] item = parentItem.child(i, column) item0 = parentItem.child(i, 0) - index = self.model().mapFromSource( - self.modelActual.indexFromItem(item) + index = self.model().mapFromSource( # type: ignore[union-attr] + self.modelActual.indexFromItem(item) # type: ignore[union-attr] ) - index0 = self.model().mapFromSource( - self.modelActual.indexFromItem(item0) + index0 = self.model().mapFromSource( # type: ignore[union-attr] + self.modelActual.indexFromItem(item0) # type: ignore[union-attr] ) if item0.showDelegate: self.openPersistentEditor(index) @@ -700,13 +700,13 @@ def onCheckDelegate(self, item: Optional["ItemBase"]) -> None: if item.showDelegate: row = item.row() parent = item.parent() - for column in self.delegateColumns: + for column in self.delegateColumns: # type: ignore[union-attr] if parent is None: - sibling = self.modelActual.item(row, column) + sibling = self.modelActual.item(row, column) # type: ignore[union-attr] else: sibling = parent.child(row, column) - index = self.model().mapFromSource( - self.modelActual.indexFromItem(sibling) + index = self.model().mapFromSource( # type: ignore[union-attr] + self.modelActual.indexFromItem(sibling) # type: ignore[union-attr] ) self.openPersistentEditor(index) self.scheduleDelayedItemsLayout() @@ -715,9 +715,9 @@ def onCheckDelegate(self, item: Optional["ItemBase"]) -> None: def onContextMenuRequested(self, pos: QtCore.QPoint) -> None: # We get the item from the real model, not the proxy model - originalModel = self.model().sourceModel() + originalModel = self.model().sourceModel() # type: ignore[union-attr] proxyIndex = self.indexAt(pos) - index = self.model().mapToSource(proxyIndex) + index = self.model().mapToSource(proxyIndex) # type: ignore[union-attr] # catch the case if the user rightcliks on any other column if index.column() != 0: @@ -783,7 +783,7 @@ def __init__( proxyModelType: type = InstrumentSortFilterProxyModel, viewType: type = InstrumentTreeViewBase, callSignals: bool = True, - shortcutManager: type = KeyboardShortcutManager, + shortcutManager: Optional[KeyboardShortcutManager] = None, parent: Optional[QtWidgets.QWidget] = None, **modelKwargs: Any, ) -> None: @@ -799,7 +799,7 @@ def __init__( self.proxyModel = proxyModelType(self.model) self.view = viewType(self.proxyModel) - self.shortcutManager = shortcutManager + self.shortcutManager = shortcutManager if shortcutManager is not None else KeyboardShortcutManager() self.layout_ = QtWidgets.QVBoxLayout() @@ -853,7 +853,7 @@ def makeToolbar(self) -> QtWidgets.QToolBar: QtGui.QIcon(":/icons/refresh.svg"), "refresh all items from the instrument", ) - refreshAction.triggered.connect(lambda x: self.refreshAll()) + refreshAction.triggered.connect(lambda x: self.refreshAll()) # type: ignore[union-attr] self.shortcutManager.apply_to_action("refresh_all", refreshAction) toolbar.addSeparator() @@ -862,14 +862,14 @@ def makeToolbar(self) -> QtWidgets.QToolBar: QtGui.QIcon(":/icons/expand.svg"), "expand tree", ) - expandAction.triggered.connect(lambda x: self.view.expandAll()) + expandAction.triggered.connect(lambda x: self.view.expandAll()) # type: ignore[union-attr] self.shortcutManager.apply_to_action("expand_all", expandAction) collapseAction = toolbar.addAction( QtGui.QIcon(":/icons/collapse.svg"), "collapse tree", ) - collapseAction.triggered.connect(lambda x: self.view.collapseAll()) + collapseAction.triggered.connect(lambda x: self.view.collapseAll()) # type: ignore[union-attr] self.shortcutManager.apply_to_action("collapse_all", collapseAction) toolbar.addSeparator() @@ -877,15 +877,15 @@ def makeToolbar(self) -> QtWidgets.QToolBar: starAction = toolbar.addAction( QtGui.QIcon(":/icons/star.svg"), "Move Starred items to the top" ) - starAction.setCheckable(True) - starAction.triggered.connect(lambda x: self.proxyModel.onToggleStar()) + starAction.setCheckable(True) # type: ignore[union-attr] + starAction.triggered.connect(lambda x: self.proxyModel.onToggleStar()) # type: ignore[union-attr] self.shortcutManager.apply_to_action("toggle_star", starAction) trashAction = toolbar.addAction( QtGui.QIcon(":/icons/trash-crossed.svg"), "Hide trashed items" ) - trashAction.setCheckable(True) - trashAction.triggered.connect(lambda x: self.proxyModel.onToggleTrash()) + trashAction.setCheckable(True) # type: ignore[union-attr] + trashAction.triggered.connect(lambda x: self.proxyModel.onToggleTrash()) # type: ignore[union-attr] self.shortcutManager.apply_to_action("toggle_trash", trashAction) # Debugging tools keep commented for commits. @@ -957,13 +957,13 @@ def debuggingMethod(self) -> None: def fillChildren(parent: QtGui.QStandardItem) -> None: for i in range(parent.rowCount()): item = parent.child(i, 0) - items[item.name] = { + items[item.name] = { # type: ignore[union-attr] "item": item, - "star": item.star, - "trash": item.trash, + "star": item.star, # type: ignore[union-attr] + "trash": item.trash, # type: ignore[union-attr] } - if item.hasChildren(): - fillChildren(item) + if item.hasChildren(): # type: ignore[union-attr] + fillChildren(item) # type: ignore[arg-type] for i in range(self.model.rowCount()): item = self.model.item(i, 0) diff --git a/src/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py index 6dbc151..7c5f545 100644 --- a/src/instrumentserver/gui/instruments.py +++ b/src/instrumentserver/gui/instruments.py @@ -158,7 +158,7 @@ def clear(self) -> None: self.unitEdit.setText("") if self.typeInput: self.typeSelect.setCurrentText( - parameterTypes[ParameterTypes.numeric]["name"] + parameterTypes[ParameterTypes.numeric]["name"] # type: ignore[arg-type] ) self.valsArgsEdit.setText("") @@ -287,7 +287,7 @@ def __init__(self, parent: Optional[QtCore.QObject] = None) -> None: # used to keep a reference to the widget. self.parameters: Dict[str, QtWidgets.QWidget] = {} - def createEditor( + def createEditor( # type: ignore[override] self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, @@ -297,10 +297,10 @@ def createEditor( This is the function that is supposed to create the widget. It should return it. """ item = self.getItem(index) - element = item.element + element = item.element # type: ignore[attr-defined] ret = ParameterWidget(element, widget) - self.parameters[item.name] = ret + self.parameters[item.name] = ret # type: ignore[attr-defined] # Try to fetch and display current value immediately # ---- Chao: removed because the constructor of ParameterWidget object already calls parameter get ---- # if element.gettable: @@ -333,7 +333,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.subClient = SubClient([self.instrument.name], **subClientArgs) self.subClient.moveToThread(self.cliThread) - self.cliThread.started.connect(self.subClient.connect) + self.cliThread.started.connect(self.subClient.connect) # type: ignore[arg-type] self.subClient.update.connect(self.updateParameter) self.subClient.finished.connect(self.cliThread.quit) @@ -398,8 +398,8 @@ def insertItemTo( if item is not None: # A parameter might not have a unit unit = "" - if item.element is not None: - unit = item.element.unit + if item.element is not None: # type: ignore[attr-defined] + unit = item.element.unit # type: ignore[attr-defined] unitItem = QtGui.QStandardItem(unit) extraItem = QtGui.QStandardItem() @@ -488,10 +488,6 @@ def connectSignals(self) -> None: "toggle_python", self._togglePythonCurrentItem, self ) self.shortcutManager.register("edit_value", self._focusToParameterValue, self) - print( - "edit_value registered", - self.shortcutManager._shortcut_map.get("edit_value"), - ) @QtCore.Slot() def _refreshCurrentItem(self) -> None: @@ -523,7 +519,6 @@ def _togglePythonCurrentItem(self) -> None: @QtCore.Slot() def _focusToParameterValue(self) -> None: - logger.debug("test") proxy_index = self.view.currentIndex() if not proxy_index.isValid(): return @@ -551,18 +546,18 @@ class ParameterDeleteDelegate(ParameterDelegate): #: Emits the name of the parameter to be deleted when the user presses the delete button. removeParameter = QtCore.Signal(str) - def createEditor( + def createEditor( # type: ignore[override] self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex, ) -> QtWidgets.QWidget: item = self.getItem(index) - element = item.element - rw = self.makeRemoveWidget(item.name, widget) + element = item.element # type: ignore[attr-defined] + rw = self.makeRemoveWidget(item.name, widget) # type: ignore[attr-defined] ret = ParameterWidget(parameter=element, parent=widget, additionalWidgets=[rw]) - self.parameters[item.name] = ret + self.parameters[item.name] = ret # type: ignore[attr-defined] return ret @@ -610,7 +605,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.setEditable(False) - self.params = self.parent().instrument + self.params = self.parent().instrument # type: ignore[union-attr] self.refreshing = False loadingProfile = None @@ -702,14 +697,14 @@ def makeToolbar(self) -> QtWidgets.QToolBar: QtGui.QIcon(":/icons/load.svg"), "Load parameters from file", ) - loadParamAction.triggered.connect(lambda x: self.loadFromFile()) + loadParamAction.triggered.connect(lambda x: self.loadFromFile()) # type: ignore[union-attr] self.shortcutManager.apply_to_action("load_items", loadParamAction) saveParamAction = toolbar.addAction( QtGui.QIcon(":/icons/save.svg"), "Save parameters to file", ) - saveParamAction.triggered.connect(lambda x: self.saveToFile()) + saveParamAction.triggered.connect(lambda x: self.saveToFile()) # type: ignore[union-attr] self.shortcutManager.apply_to_action("save_items", saveParamAction) return toolbar @@ -795,22 +790,22 @@ def __init__(self, parent: Optional[QtCore.QObject] = None) -> None: self.methods: Dict[str, "MethodDisplay"] = {} - def createEditor( + def createEditor( # type: ignore[override] self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex, ) -> QtWidgets.QWidget: item = self.getItem(index) - element = item.element - ret = MethodDisplay(element, item.name, parent=widget) + element = item.element # type: ignore[attr-defined] + ret = MethodDisplay(element, item.name, parent=widget) # type: ignore[attr-defined] parent = self.parent() assert hasattr(parent, "clearAlertsAction") # connecting the widget with the clear alert signal - parent.clearAlertsAction.triggered.connect(ret.alertLabel.clearAlert) + parent.clearAlertsAction.triggered.connect(ret.alertLabel.clearAlert) # type: ignore[union-attr] - self.methods[item.name] = ret + self.methods[item.name] = ret # type: ignore[attr-defined] return ret @@ -910,7 +905,7 @@ def __init__( self.parametersList.view.resizeColumnToContents(1) self.methodsList.view.resizeColumnToContents(0) - def closeEvent(self, event: QtGui.QCloseEvent) -> None: + def closeEvent(self, event: QtGui.QCloseEvent) -> None: # type: ignore[override] """Stop the parameter subscriber thread before destruction.""" model = getattr(self.parametersList, "model", None) if model is not None and hasattr(model, "stopListener"): diff --git a/src/instrumentserver/gui/shortcuts.py b/src/instrumentserver/gui/shortcuts.py index 163011c..7af1a6e 100644 --- a/src/instrumentserver/gui/shortcuts.py +++ b/src/instrumentserver/gui/shortcuts.py @@ -53,7 +53,7 @@ def __init__(self) -> None: self._shortcut_map: dict[str, QtWidgets.QShortcut] = {} self._action_map: dict[str, QtWidgets.QAction] = {} - def load_from_dict(self, config) -> None: + def load_from_dict(self, config: dict[str, str]) -> None: """Override the current mapping with entries read from serverConfig file.""" self.mapping.update(config) @@ -138,6 +138,7 @@ def __init__( self._table = QtWidgets.QTableWidget(len(manager.REGISTRY), 4, self) self._table.setHorizontalHeaderLabels(["Action", "Description", "Shortcut", ""]) header = self._table.horizontalHeader() + assert header is not None header.setSectionResizeMode( 0, QtWidgets.QHeaderView.ResizeMode.ResizeToContents ) @@ -183,17 +184,11 @@ def _populateTable(self) -> None: current = self.manager.mapping.get(action_id, "") id_item = QtWidgets.QTableWidgetItem(action_id) - id_item.setFlags( - QtCore.Qt.ItemFlags( - id_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable - ) - ) + id_item.setFlags(id_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) # type: ignore[arg-type] + desc_item = QtWidgets.QTableWidgetItem(description) - desc_item.setFlags( - QtCore.Qt.ItemFlags( - desc_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable - ) - ) + desc_item.setFlags(desc_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) # type: ignore[arg-type] + self._table.setItem(row, 0, id_item) self._table.setItem(row, 1, desc_item) diff --git a/src/instrumentserver/server/application.py b/src/instrumentserver/server/application.py index d574d52..c47caa8 100644 --- a/src/instrumentserver/server/application.py +++ b/src/instrumentserver/server/application.py @@ -61,7 +61,7 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None: self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) self.customContextMenuRequested.connect( - lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) + lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) # type: ignore[arg-type] ) self.deleteAction.triggered.connect(self.onDeleteAction) self.itemSelectionChanged.connect(self._processSelection) @@ -130,7 +130,7 @@ class ServerStatus(QtWidgets.QWidget): def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None: super().__init__(parent) - self.layout = QtWidgets.QVBoxLayout(self) + self.layout = QtWidgets.QVBoxLayout(self) # type: ignore[assignment,method-assign] # At the top: a status label, and a button for emitting a test message self.addressLabel = QtWidgets.QLabel() @@ -144,13 +144,13 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None: ) ) - self.layout.addLayout(self.statusLayout) + self.layout.addLayout(self.statusLayout) # type: ignore[attr-defined] # next row: a window for displaying the incoming messages. - self.layout.addWidget(QtWidgets.QLabel("Messages:")) + self.layout.addWidget(QtWidgets.QLabel("Messages:")) # type: ignore[attr-defined] self.messages = QtWidgets.QTextEdit() self.messages.setReadOnly(True) - self.layout.addWidget(self.messages) + self.layout.addWidget(self.messages) # type: ignore[attr-defined] @QtCore.Slot(str) def setListeningAddress(self, addr: str) -> None: @@ -328,7 +328,7 @@ def __init__(self, guiConfig: Optional[dict] = None, *args: Any) -> None: self.contextMenu.addSeparator() self.contextMenu.addAction(self.deletePossibleInstrumentAction) self.customContextMenuRequested.connect( - lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) + lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) # type: ignore[arg-type] ) self.basedInstrumentAction.triggered.connect(self.onBasedInstrumentAction) @@ -432,16 +432,16 @@ def onRemoveInstrumentFromTree(self) -> None: for item in items: if item.childCount() == 0: parent = item.parent() - if item.configName is not None and item.configName in self.config: - del self.config[item.configName] - parent.removeChild(item) - if parent.childCount() == 0: + if item.configName is not None and item.configName in self.config: # type: ignore[attr-defined] + del self.config[item.configName] # type: ignore[attr-defined] + parent.removeChild(item) # type: ignore[union-attr] + if parent.childCount() == 0: # type: ignore[union-attr] self.takeTopLevelItem((self.indexOfTopLevelItem(parent))) else: for i in range(item.childCount()): child = item.child(i) - if child.configName in self.config: - del self.config[child.configName] + if child.configName in self.config: # type: ignore[union-attr] + del self.config[child.configName] # type: ignore[union-attr] self.takeTopLevelItem(self.indexOfTopLevelItem(item)) @@ -672,31 +672,31 @@ def __init__( # Toolbar. self.toolBar = self.addToolBar("Tools") - self.toolBar.setIconSize(QtCore.QSize(16, 16)) + self.toolBar.setIconSize(QtCore.QSize(16, 16)) # type: ignore[union-attr] # Station tools. - self.toolBar.addWidget(QtWidgets.QLabel("Station:")) + self.toolBar.addWidget(QtWidgets.QLabel("Station:")) # type: ignore[union-attr] self.refreshStationAction = QtWidgets.QAction( QtGui.QIcon(":/icons/refresh.svg"), "Refresh", self ) self.refreshStationAction.triggered.connect(self.refreshStationComponents) - self.toolBar.addAction(self.refreshStationAction) + self.toolBar.addAction(self.refreshStationAction) # type: ignore[union-attr] # Parameter tools. - self.toolBar.addSeparator() - self.toolBar.addWidget(QtWidgets.QLabel("Params:")) + self.toolBar.addSeparator() # type: ignore[union-attr] + self.toolBar.addWidget(QtWidgets.QLabel("Params:")) # type: ignore[union-attr] self.loadParamsAction = QtWidgets.QAction( QtGui.QIcon(":/icons/load.svg"), "Load from file", self ) self.loadParamsAction.triggered.connect(self.loadParamsFromFile) - self.toolBar.addAction(self.loadParamsAction) + self.toolBar.addAction(self.loadParamsAction) # type: ignore[union-attr] self.saveParamsAction = QtWidgets.QAction( QtGui.QIcon(":/icons/save.svg"), "Save to file", self ) self.saveParamsAction.triggered.connect(self.saveParamsToFile) - self.toolBar.addAction(self.saveParamsAction) + self.toolBar.addAction(self.saveParamsAction) # type: ignore[union-attr] self.serverStatus.testButton.clicked.connect( lambda x: self.client.ask("Ping server.") @@ -715,7 +715,7 @@ def __init__( def log(self, message: str, level: LogLevels = LogLevels.info) -> None: log(logger, message, level) - def closeEvent(self, event: QtGui.QCloseEvent) -> None: + def closeEvent(self, event: Optional[QtGui.QCloseEvent]) -> None: for name, widget in list(self.instrumentTabsOpen.items()): try: widget.close() @@ -738,32 +738,30 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: self.client.disconnect() except Exception: pass - event.accept() + event.accept() # type: ignore[union-attr] def startServer(self) -> None: """Start the instrument server in a separate thread.""" - self.stationServer = StationServer(**self._serverKwargs) - self.stationServerThread = QtCore.QThread() - assert self.stationServer is not None - assert self.stationServerThread is not None - self.stationServer.moveToThread(self.stationServerThread) - self.stationServerThread.started.connect(self.stationServer.startServer) - self.stationServer.finished.connect(lambda: self.log("ZMQ server closed.")) - self.stationServer.finished.connect(self.stationServerThread.quit) - self.stationServer.finished.connect(self.stationServer.deleteLater) + self.stationServer = StationServer(**self._serverKwargs) # type: ignore[assignment] + self.stationServerThread = QtCore.QThread() # type: ignore[assignment] + self.stationServer.moveToThread(self.stationServerThread) # type: ignore[attr-defined] + self.stationServerThread.started.connect(self.stationServer.startServer) # type: ignore[arg-type,attr-defined] + self.stationServer.finished.connect(lambda: self.log("ZMQ server closed.")) # type: ignore[attr-defined] + self.stationServer.finished.connect(self.stationServerThread.quit) # type: ignore[attr-defined] + self.stationServer.finished.connect(self.stationServer.deleteLater) # type: ignore[attr-defined] # Connecting some additional things for messages. - self.stationServer.serverStarted.connect(self.serverStatus.setListeningAddress) - self.stationServer.serverStarted.connect(self.client.start) - self.stationServer.serverStarted.connect(self.refreshStationComponents) - self.stationServer.finished.connect( + self.stationServer.serverStarted.connect(self.serverStatus.setListeningAddress) # type: ignore[attr-defined] + self.stationServer.serverStarted.connect(self.client.start) # type: ignore[attr-defined] + self.stationServer.serverStarted.connect(self.refreshStationComponents) # type: ignore[attr-defined] + self.stationServer.finished.connect( # type: ignore[attr-defined] lambda: self.log("Server thread finished.", LogLevels.info) ) - self.stationServer.messageReceived.connect(self._messageReceived) - self.stationServer.instrumentCreated.connect(self.addInstrumentToGui) - self.stationServer.funcCalled.connect(self.onFuncCalled) + self.stationServer.messageReceived.connect(self._messageReceived) # type: ignore[attr-defined] + self.stationServer.instrumentCreated.connect(self.addInstrumentToGui) # type: ignore[attr-defined] + self.stationServer.funcCalled.connect(self.onFuncCalled) # type: ignore[attr-defined] - self.stationServerThread.start() + self.stationServerThread.start() # type: ignore[attr-defined] def getServerIfRunning(self) -> Optional["StationServer"]: if ( @@ -878,7 +876,7 @@ def displayComponentInfo(self, name: Union[str, None]) -> None: bp = self._bluePrints[name] else: bp = None - self.stationObjInfo.setObject(bp) + self.stationObjInfo.setObject(bp) # type: ignore[arg-type] @QtCore.Slot(QtWidgets.QTreeWidgetItem, int) def addInstrumentTab(self, item: QtWidgets.QTreeWidgetItem, index: int) -> None: @@ -966,15 +964,15 @@ def __init__(self, host: str = "localhost", port: int = 5555) -> None: # Toolbar. self.toolBar = self.addToolBar("Tools") - self.toolBar.setIconSize(QtCore.QSize(16, 16)) + self.toolBar.setIconSize(QtCore.QSize(16, 16)) # type: ignore[union-attr] # Station tools. - self.toolBar.addWidget(QtWidgets.QLabel("Station:")) + self.toolBar.addWidget(QtWidgets.QLabel("Station:")) # type: ignore[union-attr] self.refreshStationAction = QtWidgets.QAction( QtGui.QIcon(":/icons/refresh.svg"), "Refresh", self ) self.refreshStationAction.triggered.connect(self.refreshStationComponents) - self.toolBar.addAction(self.refreshStationAction) + self.toolBar.addAction(self.refreshStationAction) # type: ignore[union-attr] self.refreshStationComponents() From 741af7af0d6755e6cfba069429857e24f35cf741 Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Wed, 20 May 2026 22:11:26 -0400 Subject: [PATCH 08/12] getting tests to pass part 2 --- src/instrumentserver/gui/base_instrument.py | 57 +++++++++++---------- src/instrumentserver/gui/instruments.py | 4 +- src/instrumentserver/gui/shortcuts.py | 4 +- src/instrumentserver/server/application.py | 32 ++++++------ 4 files changed, 51 insertions(+), 46 deletions(-) diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index 9dad22c..8f302ef 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -150,8 +150,8 @@ class DelegateBase(QtWidgets.QStyledItemDelegate): def getItem(cls, QModelIndex: QtCore.QModelIndex) -> QtGui.QStandardItem: proxyModel = QModelIndex.model() - model = proxyModel.sourceModel() # type: ignore[union-attr] - item = model.itemFromIndex(proxyModel.mapToSource(QModelIndex)) # type: ignore[union-attr] + model = proxyModel.sourceModel() # type: ignore[union-attr] + item = model.itemFromIndex(proxyModel.mapToSource(QModelIndex)) # type: ignore[union-attr] if item.column != 0: parent = item.parent() row = item.row() @@ -261,13 +261,13 @@ def loadItems(self, module: Any = None, prefix: Optional[str] = None) -> None: # constructor if prefix is not None: objectName = ".".join([prefix, objectName]) - if not self._matches_any_pattern(objectName, self.itemsHide): # type: ignore[arg-type] + if not self._matches_any_pattern(objectName, self.itemsHide): # type: ignore[arg-type] item = self.addItem( fullName=objectName, star=False, trash=False, element=obj ) - if self._matches_any_pattern(objectName, self.itemsTrash): # type: ignore[arg-type] + if self._matches_any_pattern(objectName, self.itemsTrash): # type: ignore[arg-type] self.onItemTrashToggle(item) - if self._matches_any_pattern(objectName, self.itemsStar): # type: ignore[arg-type] + if self._matches_any_pattern(objectName, self.itemsStar): # type: ignore[arg-type] self.onItemStarToggle(item) for submodName, submod in module.submodules.items(): @@ -334,20 +334,20 @@ def addItem(self, fullName: str, **kwargs: Any) -> "ItemBase": ) # submodules get directly added here and not in the load function, so need to have it here too. if self.loadingItems: - if not self._matches_any_pattern(smName, self.itemsHide): # type: ignore[arg-type] - self.insertItemTo(parent, subModItem) # type: ignore[arg-type] - if self._matches_any_pattern(smName, self.itemsTrash): # type: ignore[arg-type] + if not self._matches_any_pattern(smName, self.itemsHide): # type: ignore[arg-type] + self.insertItemTo(parent, subModItem) # type: ignore[arg-type] + if self._matches_any_pattern(smName, self.itemsTrash): # type: ignore[arg-type] self.onItemTrashToggle(subModItem) - if self._matches_any_pattern(smName, self.itemsStar): # type: ignore[arg-type] + if self._matches_any_pattern(smName, self.itemsStar): # type: ignore[arg-type] self.onItemStarToggle(subModItem) else: - self.insertItemTo(parent, subModItem) # type: ignore[arg-type] - parent = subModItem # type: ignore[assignment] + self.insertItemTo(parent, subModItem) # type: ignore[arg-type] + parent = subModItem # type: ignore[assignment] else: - parent = items[0] # type: ignore[assignment] + parent = items[0] # type: ignore[assignment] newItem = self.itemClass(name=fullName, **kwargs) - self.insertItemTo(parent, newItem) # type: ignore[arg-type] + self.insertItemTo(parent, newItem) # type: ignore[arg-type] return newItem @@ -467,7 +467,7 @@ def _isParentTrash(self, parent: Optional["ItemBase"]) -> bool: if parent.trash: return True - return self._isParentTrash(parent.parent()) # type: ignore[arg-type] + return self._isParentTrash(parent.parent()) # type: ignore[arg-type] def filterAcceptsRow( self, source_row: int, source_parent: QtCore.QModelIndex @@ -490,7 +490,7 @@ def filterAcceptsRow( # Assertion is there to satisfy mypy. item can be None, that is why we check before making the assertion if item is not None: assert isinstance(item, ItemBase) - if self._isParentTrash(parent) or getattr( # type: ignore[arg-type] + if self._isParentTrash(parent) or getattr( # type: ignore[arg-type] item, "trash", False ): # item could be None when it's trashed and hidden return False @@ -512,15 +512,15 @@ def lessThan(self, left: QtCore.QModelIndex, right: QtCore.QModelIndex) -> bool: rightItem = model.itemFromIndex(right) if hasattr(leftItem, "star") and hasattr(rightItem, "star"): if self.sortOrder() == QtCore.Qt.SortOrder.DescendingOrder: - if rightItem.star and not leftItem.star: # type: ignore[union-attr] + if rightItem.star and not leftItem.star: # type: ignore[union-attr] return True - elif not rightItem.star and leftItem.star: # type: ignore[union-attr] + elif not rightItem.star and leftItem.star: # type: ignore[union-attr] return False elif self.sortOrder() == QtCore.Qt.SortOrder.AscendingOrder: - if rightItem.star and not leftItem.star: # type: ignore[union-attr] + if rightItem.star and not leftItem.star: # type: ignore[union-attr] return False - elif not rightItem.star and leftItem.star: # type: ignore[union-attr] + elif not rightItem.star and leftItem.star: # type: ignore[union-attr] return True return super().lessThan(left, right) @@ -565,8 +565,8 @@ def __init__( self.setSortingEnabled(False) # The tree should not have anything to do with filtering itself since that is left for the proxy model. - self.header().setSortIndicatorShown(True) # type: ignore[union-attr] - self.header().setSectionsClickable(True) # type: ignore[union-attr] + self.header().setSortIndicatorShown(True) # type: ignore[union-attr] + self.header().setSectionsClickable(True) # type: ignore[union-attr] self.setAlternatingRowColors(True) @@ -603,8 +603,8 @@ def fillCollapsedDict(self, parentItem: Optional[ItemBase] = None) -> None: proxyIndex = m.mapFromSource(index) if proxyIndex.isValid(): self.collapsedState[persistentIndex] = self.isExpanded(proxyIndex) - if item.hasChildren(): # type: ignore[union-attr] - self.fillCollapsedDict(item) # type: ignore[arg-type] + if item.hasChildren(): # type: ignore[union-attr] + self.fillCollapsedDict(item) # type: ignore[arg-type] else: for i in range(parentItem.rowCount()): child = parentItem.child(i, 0) @@ -615,7 +615,7 @@ def fillCollapsedDict(self, parentItem: Optional[ItemBase] = None) -> None: proxyIndex = m.mapFromSource(childIndex) if proxyIndex.isValid(): self.collapsedState[persistentIndex] = self.isExpanded(proxyIndex) - if child.hasChildren(): # type: ignore[union-attr] + if child.hasChildren(): # type: ignore[union-attr] self.fillCollapsedDict(child) # type: ignore[arg-type] @QtCore.Slot() @@ -641,7 +641,8 @@ def restoreCollapsedDict(self) -> None: for x in self.delegateColumns # type: ignore[union-attr] ] proxyDelegateIndexes = [ - self.model().mapFromSource(index) for index in delegateIndexes # type: ignore[union-attr] + self.model().mapFromSource(index) + for index in delegateIndexes # type: ignore[union-attr] ] for delegateIndex in proxyDelegateIndexes: self.openPersistentEditor(delegateIndex) @@ -799,7 +800,11 @@ def __init__( self.proxyModel = proxyModelType(self.model) self.view = viewType(self.proxyModel) - self.shortcutManager = shortcutManager if shortcutManager is not None else KeyboardShortcutManager() + self.shortcutManager = ( + shortcutManager + if shortcutManager is not None + else KeyboardShortcutManager() + ) self.layout_ = QtWidgets.QVBoxLayout() diff --git a/src/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py index 7c5f545..ab387c4 100644 --- a/src/instrumentserver/gui/instruments.py +++ b/src/instrumentserver/gui/instruments.py @@ -158,7 +158,7 @@ def clear(self) -> None: self.unitEdit.setText("") if self.typeInput: self.typeSelect.setCurrentText( - parameterTypes[ParameterTypes.numeric]["name"] # type: ignore[arg-type] + parameterTypes[ParameterTypes.numeric]["name"] # type: ignore[arg-type] ) self.valsArgsEdit.setText("") @@ -287,7 +287,7 @@ def __init__(self, parent: Optional[QtCore.QObject] = None) -> None: # used to keep a reference to the widget. self.parameters: Dict[str, QtWidgets.QWidget] = {} - def createEditor( # type: ignore[override] + def createEditor( # type: ignore[override] self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, diff --git a/src/instrumentserver/gui/shortcuts.py b/src/instrumentserver/gui/shortcuts.py index 7af1a6e..4b7a294 100644 --- a/src/instrumentserver/gui/shortcuts.py +++ b/src/instrumentserver/gui/shortcuts.py @@ -184,10 +184,10 @@ def _populateTable(self) -> None: current = self.manager.mapping.get(action_id, "") id_item = QtWidgets.QTableWidgetItem(action_id) - id_item.setFlags(id_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) # type: ignore[arg-type] + id_item.setFlags(id_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) # type: ignore[arg-type] desc_item = QtWidgets.QTableWidgetItem(description) - desc_item.setFlags(desc_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) # type: ignore[arg-type] + desc_item.setFlags(desc_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) # type: ignore[arg-type] self._table.setItem(row, 0, id_item) self._table.setItem(row, 1, desc_item) diff --git a/src/instrumentserver/server/application.py b/src/instrumentserver/server/application.py index c47caa8..608082b 100644 --- a/src/instrumentserver/server/application.py +++ b/src/instrumentserver/server/application.py @@ -738,30 +738,30 @@ def closeEvent(self, event: Optional[QtGui.QCloseEvent]) -> None: self.client.disconnect() except Exception: pass - event.accept() # type: ignore[union-attr] + event.accept() # type: ignore[union-attr] def startServer(self) -> None: """Start the instrument server in a separate thread.""" - self.stationServer = StationServer(**self._serverKwargs) # type: ignore[assignment] - self.stationServerThread = QtCore.QThread() # type: ignore[assignment] - self.stationServer.moveToThread(self.stationServerThread) # type: ignore[attr-defined] - self.stationServerThread.started.connect(self.stationServer.startServer) # type: ignore[arg-type,attr-defined] - self.stationServer.finished.connect(lambda: self.log("ZMQ server closed.")) # type: ignore[attr-defined] - self.stationServer.finished.connect(self.stationServerThread.quit) # type: ignore[attr-defined] - self.stationServer.finished.connect(self.stationServer.deleteLater) # type: ignore[attr-defined] + self.stationServer = StationServer(**self._serverKwargs) + self.stationServerThread = QtCore.QThread() + self.stationServer.moveToThread(self.stationServerThread) + self.stationServerThread.started.connect(self.stationServer.startServer) # type: ignore[arg-type] + self.stationServer.finished.connect(lambda: self.log("ZMQ server closed.")) + self.stationServer.finished.connect(self.stationServerThread.quit) + self.stationServer.finished.connect(self.stationServer.deleteLater) # Connecting some additional things for messages. - self.stationServer.serverStarted.connect(self.serverStatus.setListeningAddress) # type: ignore[attr-defined] - self.stationServer.serverStarted.connect(self.client.start) # type: ignore[attr-defined] - self.stationServer.serverStarted.connect(self.refreshStationComponents) # type: ignore[attr-defined] - self.stationServer.finished.connect( # type: ignore[attr-defined] + self.stationServer.serverStarted.connect(self.serverStatus.setListeningAddress) + self.stationServer.serverStarted.connect(self.client.start) + self.stationServer.serverStarted.connect(self.refreshStationComponents) + self.stationServer.finished.connect( lambda: self.log("Server thread finished.", LogLevels.info) ) - self.stationServer.messageReceived.connect(self._messageReceived) # type: ignore[attr-defined] - self.stationServer.instrumentCreated.connect(self.addInstrumentToGui) # type: ignore[attr-defined] - self.stationServer.funcCalled.connect(self.onFuncCalled) # type: ignore[attr-defined] + self.stationServer.messageReceived.connect(self._messageReceived) + self.stationServer.instrumentCreated.connect(self.addInstrumentToGui) + self.stationServer.funcCalled.connect(self.onFuncCalled) - self.stationServerThread.start() # type: ignore[attr-defined] + self.stationServerThread.start() def getServerIfRunning(self) -> Optional["StationServer"]: if ( From aaa8386aee0669843d7bac7616c3436ae5e97793 Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Wed, 20 May 2026 22:18:38 -0400 Subject: [PATCH 09/12] getting tests to pass part 3 --- src/instrumentserver/apps.py | 16 +++++++++++----- src/instrumentserver/config.py | 18 ++++++++++++++---- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/instrumentserver/apps.py b/src/instrumentserver/apps.py index e624e42..b3680e5 100644 --- a/src/instrumentserver/apps.py +++ b/src/instrumentserver/apps.py @@ -71,9 +71,15 @@ def serverScript() -> None: ) = None, None, None, None, None, None, None, None if configPath != "": # Separates the corresponding settings into the 5 necessary parts - stationConfig, serverConfig, guiConfig, shortcutConfig, tempFile, pollingRates, ipAddresses = ( - loadConfig(configPath) - ) + ( + stationConfig, + serverConfig, + guiConfig, + shortcutConfig, + tempFile, + pollingRates, + ipAddresses, + ) = loadConfig(configPath) if pollingRates is not None and pollingRates != {}: pollingThread = QtCore.QThread() pollWorker = PollingWorker(pollingRates=pollingRates) @@ -93,7 +99,7 @@ def serverScript() -> None: shortcutConfig=shortcutConfig, pollingThread=pollingThread, ipAddresses=ipAddresses, - configPath=configPath + configPath=configPath, ) else: serverWithGui( @@ -106,7 +112,7 @@ def serverScript() -> None: shortcutConfig=shortcutConfig, pollingThread=pollingThread, ipAddresses=ipAddresses, - configPath=configPath + configPath=configPath, ) # Close and delete the temporary files diff --git a/src/instrumentserver/config.py b/src/instrumentserver/config.py index 2f8a2a2..f30d874 100644 --- a/src/instrumentserver/config.py +++ b/src/instrumentserver/config.py @@ -18,7 +18,9 @@ GUIFIELD = {"type": "instrumentserver.gui.instruments.GenericInstrument", "kwargs": {}} -def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, dict, IO[bytes], dict, dict]: +def loadConfig( + configPath: str | Path, +) -> tuple[str, dict, dict, dict, IO[bytes], dict, dict]: """ Loads the config for the instrumentserver. From 1 config file it splits the respective fields into 3 different objects: a serverConfig (the configurations for the server), a stationConfig(the qcodes station config file clean @@ -36,7 +38,7 @@ def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, dict, IO[bytes] serverConfig: dict = {} # Config for the server guiConfig = {} # Individual gui config of each instrument fullConfig = {} # serverConfig + guiConfig + any unfilled fields. Used for creating instruments from the gui - shortcutConfig = {} # Preferences for keyboard shortcuts + shortcutConfig = {} # Preferences for keyboard shortcuts pollingRates = {} # Polling rates for each parameter ipAddresses = {} # Dictionary of IP Addresses to send broadcasts to: # externalBroadcast: where to externally send parameter change broadcasts to, formatted like "tcp://address:port" @@ -150,7 +152,7 @@ def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, dict, IO[bytes] # Update fullConfig with merged GUI config fullConfig[instrumentName]["gui"] = guiConfig[instrumentName] - + # Gets all shortcuts different to REGISTRY defaults from the config file if "shortcuts" in rawConfig: shortcutConfig = rawConfig["shortcuts"] @@ -176,4 +178,12 @@ def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, dict, IO[bytes] tempFilePath = tempFile.name # You need to return the tempFile itself so that the garbage collector doesn't touch it - return tempFilePath, serverConfig, fullConfig, shortcutConfig, tempFile, pollingRates, ipAddresses + return ( + tempFilePath, + serverConfig, + fullConfig, + shortcutConfig, + tempFile, + pollingRates, + ipAddresses, + ) From b087eece0906de63bc5f00b45499498c5497c8e5 Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Wed, 20 May 2026 22:23:02 -0400 Subject: [PATCH 10/12] forgot to commit base instrument. part 4 --- src/instrumentserver/gui/base_instrument.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index 8f302ef..4057ef1 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -641,8 +641,8 @@ def restoreCollapsedDict(self) -> None: for x in self.delegateColumns # type: ignore[union-attr] ] proxyDelegateIndexes = [ - self.model().mapFromSource(index) - for index in delegateIndexes # type: ignore[union-attr] + self.model().mapFromSource(index) # type: ignore[union-attr] + for index in delegateIndexes ] for delegateIndex in proxyDelegateIndexes: self.openPersistentEditor(delegateIndex) From f9180104a49a37b2d904cd6ce7fec20536e3c92b Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Sat, 23 May 2026 17:54:14 -0400 Subject: [PATCH 11/12] rewrote tests because loadConfig now returns 7 values instead of 6. also, removed 'Save' button, so only can save to file --- src/instrumentserver/gui/shortcuts.py | 11 ++--- test/pytest/test_config.py | 67 +++++++++++++++++++++------ 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/src/instrumentserver/gui/shortcuts.py b/src/instrumentserver/gui/shortcuts.py index 4b7a294..bfe0231 100644 --- a/src/instrumentserver/gui/shortcuts.py +++ b/src/instrumentserver/gui/shortcuts.py @@ -155,18 +155,15 @@ def __init__( self._indicators: list[QtWidgets.QLabel] = [] self._populateTable() - btnSaveFile = QtWidgets.QPushButton("Save to file") - btnSaveFile.clicked.connect(self._saveToFile) btnReset = QtWidgets.QPushButton("Reset to defaults") btnReset.clicked.connect(self._resetDefaults) - btnSave = QtWidgets.QPushButton("Save") - btnSave.clicked.connect(self._save) + btnSaveFile = QtWidgets.QPushButton("Save to file") + btnSaveFile.clicked.connect(self._saveToFile) btnRow = QtWidgets.QHBoxLayout() - btnRow.addWidget(btnSaveFile) btnRow.addStretch() btnRow.addWidget(btnReset) - btnRow.addWidget(btnSave) + btnRow.addWidget(btnSaveFile) layout = QtWidgets.QVBoxLayout() layout.addWidget(self._table) @@ -277,14 +274,12 @@ def _restoreAfterRevert( widget.setKeySequence(QtGui.QKeySequence(intended)) widget.blockSignals(False) - @QtCore.Slot() def _save(self) -> None: for row, action_id in enumerate(self.manager.REGISTRY): widget = self._table.cellWidget(row, 2) if isinstance(widget, QtWidgets.QKeySequenceEdit): self.manager.rebind(action_id, widget.keySequence().toString()) self._updateAllIndicators() - logger.info("Shortcuts saved locally") @QtCore.Slot() def _saveToFile(self) -> None: diff --git a/test/pytest/test_config.py b/test/pytest/test_config.py index fdbe570..1a4e6e1 100644 --- a/test/pytest/test_config.py +++ b/test/pytest/test_config.py @@ -27,13 +27,20 @@ def test_minimal_config(tmp_path): type: instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule """, ) - path, serverConfig, fullConfig, tempFile, pollingRates, ipAddresses = loadConfig( - cfg - ) + ( + path, + serverConfig, + fullConfig, + shortcutConfig, + tempFile, + pollingRates, + ipAddresses, + ) = loadConfig(cfg) tempFile.close() assert "my_ins" in serverConfig assert "my_ins" in fullConfig + assert shortcutConfig == {} assert pollingRates == {} assert ipAddresses == {} # returned path is a string @@ -49,7 +56,7 @@ def test_temp_file_is_readable(tmp_path): type: some.Type """, ) - tempFilePath, _, _, tempFile, _, _ = loadConfig(cfg) + tempFilePath, _, _, _, tempFile, _, _ = loadConfig(cfg) tempFile.seek(0) content = tempFile.read() assert len(content) > 0 @@ -70,7 +77,7 @@ def test_initialize_defaults_to_true(tmp_path): type: some.Type """, ) - _, serverConfig, _, tempFile, _, _ = loadConfig(cfg) + _, serverConfig, _, _, tempFile, _, _ = loadConfig(cfg) tempFile.close() assert serverConfig["my_ins"]["initialize"] is True @@ -85,7 +92,7 @@ def test_initialize_explicit_false(tmp_path): initialize: false """, ) - _, serverConfig, _, tempFile, _, _ = loadConfig(cfg) + _, serverConfig, _, _, tempFile, _, _ = loadConfig(cfg) tempFile.close() assert serverConfig["my_ins"]["initialize"] is False @@ -118,7 +125,7 @@ def test_gui_defaults_to_generic_instrument(tmp_path): type: some.Type """, ) - _, _, fullConfig, tempFile, _, _ = loadConfig(cfg) + _, _, fullConfig, _, tempFile, _, _ = loadConfig(cfg) tempFile.close() assert fullConfig["my_ins"]["gui"]["type"] == GUIFIELD["type"] @@ -134,7 +141,7 @@ def test_gui_generic_alias_maps_to_full_path(tmp_path): type: generic """, ) - _, _, fullConfig, tempFile, _, _ = loadConfig(cfg) + _, _, fullConfig, _, tempFile, _, _ = loadConfig(cfg) tempFile.close() assert fullConfig["my_ins"]["gui"]["type"] == GUIFIELD["type"] @@ -187,7 +194,7 @@ def test_polling_rate_parsed(tmp_path): param2: 200 """, ) - _, _, _, tempFile, pollingRates, _ = loadConfig(cfg) + _, _, _, _, tempFile, pollingRates, _ = loadConfig(cfg) tempFile.close() assert pollingRates == {"my_ins.param1": 100, "my_ins.param2": 200} @@ -202,7 +209,7 @@ def test_polling_rate_empty_is_ignored(tmp_path): pollingRate: """, ) - _, _, _, tempFile, pollingRates, _ = loadConfig(cfg) + _, _, _, _, tempFile, pollingRates, _ = loadConfig(cfg) tempFile.close() assert pollingRates == {} @@ -224,7 +231,7 @@ def test_networking_parsed(tmp_path): listeningAddress: 192.168.1.1 """, ) - _, _, _, tempFile, _, ipAddresses = loadConfig(cfg) + _, _, _, _, tempFile, _, ipAddresses = loadConfig(cfg) tempFile.close() assert ipAddresses["externalBroadcast"] == "tcp://192.168.1.1:5556" assert ipAddresses["listeningAddress"] == "192.168.1.1" @@ -239,7 +246,7 @@ def test_no_networking_section_gives_empty_dict(tmp_path): type: some.Type """, ) - _, _, _, tempFile, _, ipAddresses = loadConfig(cfg) + _, _, _, _, tempFile, _, ipAddresses = loadConfig(cfg) tempFile.close() assert ipAddresses == {} @@ -262,7 +269,7 @@ def test_gui_defaults_default_section(tmp_path): - IDN """, ) - _, _, fullConfig, tempFile, _, _ = loadConfig(cfg) + _, _, fullConfig, _, tempFile, _, _ = loadConfig(cfg) tempFile.close() kwargs = fullConfig["my_ins"]["gui"].get("kwargs", {}) assert "parameters-hide" in kwargs @@ -282,7 +289,7 @@ def test_gui_defaults_class_section(tmp_path): - power_level """, ) - _, _, fullConfig, tempFile, _, _ = loadConfig(cfg) + _, _, fullConfig, _, tempFile, _, _ = loadConfig(cfg) tempFile.close() kwargs = fullConfig["my_ins"]["gui"].get("kwargs", {}) assert "parameters-hide" in kwargs @@ -310,9 +317,39 @@ def test_gui_defaults_merging_order(tmp_path): - class_param """, ) - _, _, fullConfig, tempFile, _, _ = loadConfig(cfg) + _, _, fullConfig, _, tempFile, _, _ = loadConfig(cfg) tempFile.close() hide = fullConfig["my_ins"]["gui"]["kwargs"]["parameters-hide"] assert "default_param" in hide assert "class_param" in hide assert "instance_param" in hide + + +def test_shortcuts_parsed(tmp_path): + cfg = _write_config( + tmp_path, + """\ +instruments: + my_ins: + type: some.Type +shortcuts: + jump_filter: "Ctrl+G" +""", + ) + _, _, _, shortcutConfig, tempFile, _, _ = loadConfig(cfg) + tempFile.close() + assert shortcutConfig == {"jump_filter": "Ctrl+G"} + + +def test_no_shortcuts_gives_empty_dict(tmp_path): + cfg = _write_config( + tmp_path, + """\ +instruments: + my_ins: + type: some.Type +""", + ) + _, _, _, shortcutConfig, tempFile, _, _ = loadConfig(cfg) + tempFile.close() + assert shortcutConfig == {} From 3d89514b7795fc9691689e84d80ad178b7f9778d Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Sun, 31 May 2026 18:33:16 -0400 Subject: [PATCH 12/12] fixed issues with shortcuts not working when multiple tabs open. some shortcuts also fixed/implemented for methods. shortcut tooltips added for toolbar --- src/instrumentserver/gui/base_instrument.py | 58 +- src/instrumentserver/gui/instruments.py | 103 +- src/instrumentserver/gui/shortcuts.py | 95 +- src/instrumentserver/resource.py | 1924 ++++++++++--------- src/instrumentserver/resource.qrc | 1 + 5 files changed, 1141 insertions(+), 1040 deletions(-) diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index 4057ef1..2e28793 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -238,8 +238,7 @@ def _matches_any_pattern(name: str, patterns: List[str]) -> bool: :param patterns: List of glob patterns to match against (e.g., 'power_*', '*_frequency') :return: True if name matches any pattern, False otherwise """ - if not patterns: - return False + for pattern in patterns: if fnmatch.fnmatch(name, pattern): return True @@ -846,6 +845,11 @@ def connectSignals(self) -> None: self.shortcutManager.register("trash_item", self._trashCurrentItem, self) self.shortcutManager.register("fit_column", self._fitCurrentColumn, self) self.shortcutManager.register("sort_column", self._sortCurrentColumn, self) + self.shortcutManager.register("refresh_all", self.refreshAll, self) + self.shortcutManager.register("expand_all", self.view.expandAll, self) + self.shortcutManager.register("collapse_all", self.view.collapseAll, self) + self.shortcutManager.register("toggle_star", self._starAction.trigger, self) # type: ignore[union-attr] + self.shortcutManager.register("toggle_trash", self._trashAction.trigger, self) # type: ignore[union-attr] def makeToolbar(self) -> QtWidgets.QToolBar: """ @@ -859,7 +863,7 @@ def makeToolbar(self) -> QtWidgets.QToolBar: "refresh all items from the instrument", ) refreshAction.triggered.connect(lambda x: self.refreshAll()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("refresh_all", refreshAction) + self.shortcutManager.register_tooltip("refresh_all", refreshAction) toolbar.addSeparator() @@ -868,30 +872,30 @@ def makeToolbar(self) -> QtWidgets.QToolBar: "expand tree", ) expandAction.triggered.connect(lambda x: self.view.expandAll()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("expand_all", expandAction) + self.shortcutManager.register_tooltip("expand_all", expandAction) collapseAction = toolbar.addAction( QtGui.QIcon(":/icons/collapse.svg"), "collapse tree", ) collapseAction.triggered.connect(lambda x: self.view.collapseAll()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("collapse_all", collapseAction) + self.shortcutManager.register_tooltip("collapse_all", collapseAction) toolbar.addSeparator() - starAction = toolbar.addAction( + self._starAction = toolbar.addAction( QtGui.QIcon(":/icons/star.svg"), "Move Starred items to the top" ) - starAction.setCheckable(True) # type: ignore[union-attr] - starAction.triggered.connect(lambda x: self.proxyModel.onToggleStar()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("toggle_star", starAction) + self._starAction.setCheckable(True) # type: ignore[union-attr] + self._starAction.triggered.connect(lambda x: self.proxyModel.onToggleStar()) # type: ignore[union-attr] + self.shortcutManager.register_tooltip("toggle_star", self._starAction) - trashAction = toolbar.addAction( + self._trashAction = toolbar.addAction( QtGui.QIcon(":/icons/trash-crossed.svg"), "Hide trashed items" ) - trashAction.setCheckable(True) # type: ignore[union-attr] - trashAction.triggered.connect(lambda x: self.proxyModel.onToggleTrash()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("toggle_trash", trashAction) + self._trashAction.setCheckable(True) # type: ignore[union-attr] + self._trashAction.triggered.connect(lambda x: self.proxyModel.onToggleTrash()) # type: ignore[union-attr] + self.shortcutManager.register_tooltip("toggle_trash", self._trashAction) # Debugging tools keep commented for commits. # printAction = toolbar.addAction( @@ -908,31 +912,29 @@ def makeToolbar(self) -> QtWidgets.QToolBar: def refreshAll(self) -> None: self.model.refreshAll() - @QtCore.Slot() - def _starCurrentItem(self) -> None: + def _getCurrentItem(self) -> Optional[ItemBase]: proxy_index = self.view.currentIndex() if not proxy_index.isValid(): - return + return None source_index = self.proxyModel.mapToSource(proxy_index) if source_index.column() != 0: source_index = source_index.sibling(source_index.row(), 0) item = self.model.itemFromIndex(source_index) - if isinstance(item, ItemBase): + return item if isinstance(item, ItemBase) else None + + def _toggleCurrentItem(self, signal: QtCore.SignalInstance) -> None: + item = self._getCurrentItem() + if item is not None: self.view.lastSelectedItem = item - self.view.itemStarToggle.emit(item) + signal.emit(item) + + @QtCore.Slot() + def _starCurrentItem(self) -> None: + self._toggleCurrentItem(self.view.itemStarToggle) @QtCore.Slot() def _trashCurrentItem(self) -> None: - proxy_index = self.view.currentIndex() - if not proxy_index.isValid(): - return - source_index = self.proxyModel.mapToSource(proxy_index) - if source_index.column() != 0: - source_index = source_index.sibling(source_index.row(), 0) - item = self.model.itemFromIndex(source_index) - if isinstance(item, ItemBase): - self.view.lastSelectedItem = item - self.view.itemTrashToggle.emit(item) + self._toggleCurrentItem(self.view.itemTrashToggle) @QtCore.Slot() def _fitCurrentColumn(self) -> None: diff --git a/src/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py index ab387c4..b96805a 100644 --- a/src/instrumentserver/gui/instruments.py +++ b/src/instrumentserver/gui/instruments.py @@ -466,7 +466,6 @@ def __init__( modelKwargs["sub_port"] = kwargs.pop("sub_port") shortcutManager = kwargs.pop("shortcutManager", None) - print(shortcutManager) super().__init__( instrument=instrument, @@ -489,51 +488,38 @@ def connectSignals(self) -> None: ) self.shortcutManager.register("edit_value", self._focusToParameterValue, self) - @QtCore.Slot() - def _refreshCurrentItem(self) -> None: - proxy_index = self.view.currentIndex() - if not proxy_index.isValid(): - return - source_index = self.proxyModel.mapToSource(proxy_index) - if source_index.column() != 0: - source_index = source_index.sibling(source_index.row(), 0) - item = self.model.itemFromIndex(source_index) - if isinstance(item, ItemBase): + def _withCurrentParameter( + self, callback: Callable[["ParameterWidget"], None] + ) -> None: + item = self._getCurrentItem() + if item is not None: widget = self.view.delegate.parameters.get(item.name) if widget is not None: - widget.setWidgetFromParameter() + callback(widget) + + @QtCore.Slot() + def _refreshCurrentItem(self) -> None: + self._withCurrentParameter(lambda w: w.setWidgetFromParameter()) @QtCore.Slot() def _togglePythonCurrentItem(self) -> None: - proxy_index = self.view.currentIndex() - if not proxy_index.isValid(): - return - source_index = self.proxyModel.mapToSource(proxy_index) - if source_index.column() != 0: - source_index = source_index.sibling(source_index.row(), 0) - item = self.model.itemFromIndex(source_index) - if isinstance(item, ItemBase): - widget = self.view.delegate.parameters.get(item.name) - if widget is not None and isinstance(widget.paramWidget, AnyInput): - widget.paramWidget.doEval.toggle() + self._withCurrentParameter( + lambda w: ( + w.paramWidget.doEval.toggle() + if isinstance(w.paramWidget, AnyInput) + else None + ) + ) @QtCore.Slot() def _focusToParameterValue(self) -> None: - proxy_index = self.view.currentIndex() - if not proxy_index.isValid(): - return - source_index = self.proxyModel.mapToSource(proxy_index) - if source_index.column() != 0: - source_index = source_index.sibling(source_index.row(), 0) - item = self.model.itemFromIndex(source_index) - if isinstance(item, ItemBase): - widget = self.view.delegate.parameters.get(item.name) - if widget and hasattr(widget, "paramWidget"): - pw = widget.paramWidget - if isinstance(pw, AnyInput): - pw.input.setFocus() - else: - pw.setFocus() + self._withCurrentParameter( + lambda w: ( + w.paramWidget.input.setFocus() + if isinstance(w.paramWidget, AnyInput) + else w.paramWidget.setFocus() + ) + ) # ----------------- Parameters Display Classes - Ending -------------------------------- @@ -678,14 +664,8 @@ def connectSignals(self) -> None: @QtCore.Slot() def _deleteCurrentItem(self) -> None: - proxy_index = self.view.currentIndex() - if not proxy_index.isValid(): - return - source_index = self.proxyModel.mapToSource(proxy_index) - if source_index.column() != 0: - source_index = source_index.sibling(source_index.row(), 0) - item = self.model.itemFromIndex(source_index) - if isinstance(item, ItemBase): + item = self._getCurrentItem() + if item is not None: self.removeParameter(item.name) def makeToolbar(self) -> QtWidgets.QToolBar: @@ -698,14 +678,14 @@ def makeToolbar(self) -> QtWidgets.QToolBar: "Load parameters from file", ) loadParamAction.triggered.connect(lambda x: self.loadFromFile()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("load_items", loadParamAction) + self.shortcutManager.register_tooltip("load_items", loadParamAction) saveParamAction = toolbar.addAction( QtGui.QIcon(":/icons/save.svg"), "Save parameters to file", ) saveParamAction.triggered.connect(lambda x: self.saveToFile()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("save_items", saveParamAction) + self.shortcutManager.register_tooltip("save_items", saveParamAction) return toolbar @@ -852,6 +832,33 @@ def __init__(self, instrument: Any, **kwargs: Any) -> None: **modelKwargs, ) + def connectSignals(self) -> None: + super().connectSignals() + self.shortcutManager.register( + "toggle_python", self._togglePythonCurrentItem, self + ) + self.shortcutManager.register("edit_value", self._focusToMethodValue, self) + self.shortcutManager.register("run_method", self._runCurrentMethod, self) + + def _withCurrentMethod(self, callback: Callable[["MethodDisplay"], None]) -> None: + item = self._getCurrentItem() + if item is not None: + widget = self.view.delegate.methods.get(item.name) + if widget is not None: + callback(widget) + + @QtCore.Slot() + def _togglePythonCurrentItem(self) -> None: + self._withCurrentMethod(lambda w: w.anyInput.doEval.toggle()) + + @QtCore.Slot() + def _focusToMethodValue(self) -> None: + self._withCurrentMethod(lambda w: w.anyInput.input.setFocus()) + + @QtCore.Slot() + def _runCurrentMethod(self) -> None: + self._withCurrentMethod(lambda w: w.runFun()) + # ----------------- Methods Display Classes - Ending ----------------------------------- diff --git a/src/instrumentserver/gui/shortcuts.py b/src/instrumentserver/gui/shortcuts.py index bfe0231..27c639f 100644 --- a/src/instrumentserver/gui/shortcuts.py +++ b/src/instrumentserver/gui/shortcuts.py @@ -1,14 +1,10 @@ import logging -import os from collections import defaultdict -from typing import Callable, Optional +from typing import Callable, Optional, Union import yaml -from instrumentserver import QtCore, QtGui, QtWidgets, getInstrumentserverPath - -_ICON_DIR = getInstrumentserverPath("resource", "icons") - +from instrumentserver import QtCore, QtGui, QtWidgets logger = logging.getLogger(__name__) @@ -21,9 +17,10 @@ class KeyboardShortcutManager: The active mapping starts from defaults and can be customized by the user and persisted to a JSON file. - Qt does not poll for key presses — instead, register() and apply_to_action() - hand each mapping entry to Qt's event system (QShortcut / QAction.setShortcut), - which fires the associated callback when the key is pressed. + Qt does not poll for key presses — instead, register() hands each mapping entry + to Qt's event system via QShortcut, which fires the associated callback when the + key is pressed. register_tooltip() tracks widgets whose tooltips should display + the current key hint and be updated live when the user rebinds. """ REGISTRY: dict[str, tuple[str, str]] = { @@ -39,10 +36,11 @@ class KeyboardShortcutManager: "refresh_item": ("Ctrl+R", "Refresh the selected parameter"), "toggle_python": ("Ctrl+P", "Toggle Python eval for selected parameter"), "delete_item": ("Ctrl+Backspace", "Delete the selected parameter"), + "run_method": ("Ctrl+Return", "Runs the selected method"), "clear_add": ("Ctrl+Shift+N", "Clear regions of add parameter bar"), "add_item": ("Ctrl+N", "Jump cursor to the add parameter bar"), - "load_items": ("Ctrl+O", "Load parameters from JSON file"), - "save_items": ("Ctrl+S", "Save parameters to JSON file"), + "load_items": ("Ctrl+Shift+O", "Load parameters from JSON file"), + "save_items": ("Ctrl+Shift+S", "Save parameters to JSON file"), "fit_column": ("Ctrl+Shift+D", "Fits column width"), "sort_column": ("Ctrl+D", "Toggle sorting of selected column"), "edit_value": ("Right", "Jump cursor to value field for selected parameter"), @@ -50,8 +48,10 @@ class KeyboardShortcutManager: def __init__(self) -> None: self.mapping: dict[str, str] = {k: v[0] for k, v in self.REGISTRY.items()} - self._shortcut_map: dict[str, QtWidgets.QShortcut] = {} - self._action_map: dict[str, QtWidgets.QAction] = {} + self._shortcut_map: dict[str, list[QtWidgets.QShortcut]] = defaultdict(list) + self._tooltip_widgets: dict[ + str, list[tuple[Union[QtWidgets.QAction, QtWidgets.QWidget], str]] + ] = defaultdict(list) def load_from_dict(self, config: dict[str, str]) -> None: """Override the current mapping with entries read from serverConfig file.""" @@ -70,16 +70,30 @@ def save(self, path: str) -> None: with open(path, "w") as f: yaml.dump(data, f, indent=2) - def apply_to_action( - self, action_id: str, qaction: Optional[QtWidgets.QAction] + def register_tooltip( + self, + action_id: str, + widget: Optional[Union[QtWidgets.QAction, QtWidgets.QWidget]], ) -> None: - """Set the shortcut from the current mapping on an existing QAction and retain a reference for live rebinding.""" - if qaction is None: + """Append the current key hint to widget's tooltip and track it for live rebinding.""" + if widget is None: return - key = self.mapping.get(action_id) - if key: - qaction.setShortcut(QtGui.QKeySequence(key)) - self._action_map[action_id] = qaction + key = self.mapping.get(action_id, "") + if not key: + return + base_tip = widget.toolTip() + widget.setToolTip(f"{base_tip} [{key}]" if base_tip else f"[{key}]") + self._tooltip_widgets[action_id].append((widget, base_tip)) + widget.destroyed.connect( + lambda _, aid=action_id, ref=widget: self._remove_tooltip_widget(aid, ref) + ) + + def _remove_tooltip_widget( + self, action_id: str, widget: Union[QtWidgets.QAction, QtWidgets.QWidget] + ) -> None: + self._tooltip_widgets[action_id] = [ + (w, t) for w, t in self._tooltip_widgets[action_id] if w is not widget + ] def register( self, action_id: str, callback: Callable, widget: QtWidgets.QWidget @@ -94,16 +108,26 @@ def register( key = self.mapping.get(action_id) if key: sc = QtWidgets.QShortcut(QtGui.QKeySequence(key), widget) + sc.setContext(QtCore.Qt.ShortcutContext.WidgetWithChildrenShortcut) sc.activated.connect(callback) - self._shortcut_map[action_id] = sc + self._shortcut_map[action_id].append(sc) + sc.destroyed.connect( + lambda _, aid=action_id, ref=sc: ( + self._shortcut_map[aid].remove(ref) + if ref in self._shortcut_map[aid] + else None + ) + ) def rebind(self, action_id: str, new_key: str) -> None: """Update a shortcut immediately. Updates the mapping and the live Qt objects.""" self.mapping[action_id] = new_key - if action_id in self._shortcut_map: - self._shortcut_map[action_id].setKey(QtGui.QKeySequence(new_key)) - if action_id in self._action_map: - self._action_map[action_id].setShortcut(QtGui.QKeySequence(new_key)) + for sc in self._shortcut_map.get(action_id, []): + sc.setKey(QtGui.QKeySequence(new_key)) + for widget, base_tip in self._tooltip_widgets.get(action_id, []): + widget.setToolTip( + f"{base_tip} [{new_key}]" if base_tip else f"[{new_key}]" + ) logger.debug(f"Rebound '{action_id}' to '{new_key}'") @@ -113,10 +137,10 @@ class ShortcutEditorWidget(QtWidgets.QWidget): Intended to be embedded as a tab in the server window. Changes made in the table are applied live to the manager (and therefore all registered shortcuts) - when Save is clicked. Use 'Save to file' to persist across sessions. + when 'Save to File' is clicked. Also use 'Save to file' to persist across sessions. Each row has a small colored indicator dot in the rightmost column: - - white : saved and unique + - transparent : saved and unique - orange: unsaved change (widget value differs from manager.mapping) - red : duplicate key sequence shared with another action (takes priority) @@ -134,6 +158,7 @@ def __init__( ) -> None: super().__init__(parent) self.manager = manager + self._file_mapping: dict[str, str] = dict(manager.mapping) self._table = QtWidgets.QTableWidget(len(manager.REGISTRY), 4, self) self._table.setHorizontalHeaderLabels(["Action", "Description", "Shortcut", ""]) @@ -241,7 +266,7 @@ def _updateAllIndicators(self) -> None: self._applyIndicator( dot, "duplicate", f"Duplicate: also bound to {', '.join(others)}" ) - elif current != self.manager.mapping.get(action_id, ""): + elif current != self._file_mapping.get(action_id, ""): self._applyIndicator(dot, "unsaved", "Unsaved change") else: self._applyIndicator(dot, "ok", "") @@ -249,13 +274,14 @@ def _updateAllIndicators(self) -> None: @staticmethod def _applyIndicator(dot: QtWidgets.QLabel, state: str, tooltip: str) -> None: dot.setToolTip(tooltip) + if state == "ok": - icon_file = "alert-octagon.svg" + icon = QtGui.QIcon(":/icons/no-alert.svg") elif state == "unsaved": - icon_file = "alert-octagon-orange.svg" + icon = QtGui.QIcon(":/icons/orange-alert.svg") else: # duplicate - icon_file = "alert-octagon-red.svg" - pix = QtGui.QIcon(os.path.join(_ICON_DIR, icon_file)).pixmap(20, 20) + icon = QtGui.QIcon(":/icons/red-alert.svg") + pix = icon.pixmap(20, 20) dot.setPixmap(pix) @QtCore.Slot() @@ -279,7 +305,6 @@ def _save(self) -> None: widget = self._table.cellWidget(row, 2) if isinstance(widget, QtWidgets.QKeySequenceEdit): self.manager.rebind(action_id, widget.keySequence().toString()) - self._updateAllIndicators() @QtCore.Slot() def _saveToFile(self) -> None: @@ -287,6 +312,8 @@ def _saveToFile(self) -> None: if self.configPath: try: self.manager.save(self.configPath) + self._file_mapping = dict(self.manager.mapping) + self._updateAllIndicators() logger.info(f"Saved shortcuts to {self.configPath}") except Exception as e: logger.warning(f"Failed to save shortcuts to {self.configPath}: {e}") diff --git a/src/instrumentserver/resource.py b/src/instrumentserver/resource.py index d4fd9e9..061fd31 100644 --- a/src/instrumentserver/resource.py +++ b/src/instrumentserver/resource.py @@ -65,134 +65,175 @@ \x31\x3d\x22\x31\x32\x22\x20\x79\x31\x3d\x22\x31\x36\x22\x20\x78\ \x32\x3d\x22\x31\x32\x2e\x30\x31\x22\x20\x79\x32\x3d\x22\x31\x36\ \x22\x3e\x3c\x2f\x6c\x69\x6e\x65\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x07\xd9\ +\x00\x00\x07\xe1\ \x3c\ \x73\x76\x67\x20\x77\x69\x64\x74\x68\x3d\x22\x38\x30\x22\x20\x68\ \x65\x69\x67\x68\x74\x3d\x22\x38\x30\x22\x20\x78\x6d\x6c\x6e\x73\ \x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\ \x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x20\x66\x69\ -\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x3e\x0a\x20\x3c\x67\x3e\x0a\ -\x20\x20\x3c\x74\x69\x74\x6c\x65\x3e\x4c\x61\x79\x65\x72\x20\x31\ -\x3c\x2f\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\ -\x20\x64\x3d\x22\x6d\x33\x38\x2e\x31\x34\x34\x31\x2c\x31\x35\x2e\ -\x31\x36\x31\x36\x31\x63\x30\x2e\x36\x37\x31\x38\x2c\x2d\x31\x2e\ -\x36\x37\x32\x39\x20\x33\x2e\x30\x34\x2c\x2d\x31\x2e\x36\x37\x32\ -\x39\x20\x33\x2e\x37\x31\x31\x38\x2c\x30\x6c\x35\x2e\x39\x35\x32\ -\x32\x2c\x31\x34\x2e\x38\x32\x32\x32\x63\x30\x2e\x32\x38\x36\x31\ -\x2c\x30\x2e\x37\x31\x32\x34\x20\x30\x2e\x39\x35\x34\x37\x2c\x31\ -\x2e\x31\x39\x38\x31\x20\x31\x2e\x37\x32\x30\x37\x2c\x31\x2e\x32\ -\x35\x30\x31\x6c\x31\x35\x2e\x39\x33\x36\x2c\x31\x2e\x30\x38\x30\ -\x35\x63\x31\x2e\x37\x39\x38\x37\x2c\x30\x2e\x31\x32\x32\x20\x32\ -\x2e\x35\x33\x30\x35\x2c\x32\x2e\x33\x37\x34\x34\x20\x31\x2e\x31\ -\x34\x37\x2c\x33\x2e\x35\x33\x30\x32\x6c\x2d\x31\x32\x2e\x32\x35\ -\x37\x34\x2c\x31\x30\x2e\x32\x34\x31\x32\x63\x2d\x30\x2e\x35\x38\ -\x39\x31\x2c\x30\x2e\x34\x39\x32\x32\x20\x2d\x30\x2e\x38\x34\x34\ -\x35\x2c\x31\x2e\x32\x37\x38\x32\x20\x2d\x30\x2e\x36\x35\x37\x32\ -\x2c\x32\x2e\x30\x32\x32\x37\x6c\x33\x2e\x38\x39\x36\x39\x2c\x31\ -\x35\x2e\x34\x39\x63\x30\x2e\x34\x33\x39\x38\x2c\x31\x2e\x37\x34\ -\x38\x33\x20\x2d\x31\x2e\x34\x37\x36\x32\x2c\x33\x2e\x31\x34\x30\ -\x34\x20\x2d\x33\x2e\x30\x30\x33\x2c\x32\x2e\x31\x38\x31\x38\x6c\ -\x2d\x31\x33\x2e\x35\x32\x37\x37\x2c\x2d\x38\x2e\x34\x39\x32\x38\ -\x63\x2d\x30\x2e\x36\x35\x30\x32\x2c\x2d\x30\x2e\x34\x30\x38\x32\ -\x20\x2d\x31\x2e\x34\x37\x36\x36\x2c\x2d\x30\x2e\x34\x30\x38\x32\ -\x20\x2d\x32\x2e\x31\x32\x36\x38\x2c\x30\x6c\x2d\x31\x33\x2e\x35\ -\x32\x37\x37\x2c\x38\x2e\x34\x39\x32\x38\x63\x2d\x31\x2e\x35\x32\ -\x36\x38\x2c\x30\x2e\x39\x35\x38\x36\x20\x2d\x33\x2e\x34\x34\x32\ -\x38\x2c\x2d\x30\x2e\x34\x33\x33\x35\x20\x2d\x33\x2e\x30\x30\x33\ -\x2c\x2d\x32\x2e\x31\x38\x31\x38\x6c\x33\x2e\x38\x39\x36\x39\x2c\ -\x2d\x31\x35\x2e\x34\x39\x63\x30\x2e\x31\x38\x37\x33\x2c\x2d\x30\ -\x2e\x37\x34\x34\x35\x20\x2d\x30\x2e\x30\x36\x38\x31\x2c\x2d\x31\ -\x2e\x35\x33\x30\x35\x20\x2d\x30\x2e\x36\x35\x37\x32\x2c\x2d\x32\ -\x2e\x30\x32\x32\x37\x6c\x2d\x31\x32\x2e\x32\x35\x37\x34\x2c\x2d\ -\x31\x30\x2e\x32\x34\x31\x32\x63\x2d\x31\x2e\x33\x38\x33\x35\x2c\ -\x2d\x31\x2e\x31\x35\x35\x38\x20\x2d\x30\x2e\x36\x35\x31\x37\x2c\ -\x2d\x33\x2e\x34\x30\x38\x32\x20\x31\x2e\x31\x34\x37\x2c\x2d\x33\ -\x2e\x35\x33\x30\x32\x6c\x31\x35\x2e\x39\x33\x36\x2c\x2d\x31\x2e\ -\x30\x38\x30\x35\x63\x30\x2e\x37\x36\x36\x2c\x2d\x30\x2e\x30\x35\ -\x32\x20\x31\x2e\x34\x33\x34\x36\x2c\x2d\x30\x2e\x35\x33\x37\x37\ -\x20\x31\x2e\x37\x32\x30\x37\x2c\x2d\x31\x2e\x32\x35\x30\x31\x6c\ -\x35\x2e\x39\x35\x32\x32\x2c\x2d\x31\x34\x2e\x38\x32\x32\x32\x7a\ -\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\x46\x32\x39\x39\x34\x41\x22\ -\x20\x69\x64\x3d\x22\x73\x76\x67\x5f\x31\x22\x2f\x3e\x0a\x20\x20\ -\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x6d\x33\x39\x2e\x30\x35\x31\ -\x35\x2c\x32\x36\x2e\x33\x31\x30\x38\x63\x30\x2e\x33\x33\x35\x39\ -\x2c\x2d\x30\x2e\x38\x33\x36\x34\x20\x31\x2e\x35\x32\x30\x31\x2c\ -\x2d\x30\x2e\x38\x33\x36\x34\x20\x31\x2e\x38\x35\x36\x2c\x30\x6c\ -\x32\x2e\x39\x37\x36\x2c\x37\x2e\x34\x31\x31\x31\x63\x30\x2e\x31\ -\x34\x33\x31\x2c\x30\x2e\x33\x35\x36\x32\x20\x30\x2e\x34\x37\x37\ -\x34\x2c\x30\x2e\x35\x39\x39\x31\x20\x30\x2e\x38\x36\x30\x34\x2c\ -\x30\x2e\x36\x32\x35\x31\x6c\x37\x2e\x39\x36\x38\x2c\x30\x2e\x35\ -\x34\x30\x33\x63\x30\x2e\x38\x39\x39\x33\x2c\x30\x2e\x30\x36\x30\ -\x39\x20\x31\x2e\x32\x36\x35\x32\x2c\x31\x2e\x31\x38\x37\x31\x20\ -\x30\x2e\x35\x37\x33\x35\x2c\x31\x2e\x37\x36\x35\x31\x6c\x2d\x36\ -\x2e\x31\x32\x38\x37\x2c\x35\x2e\x31\x32\x30\x35\x63\x2d\x30\x2e\ -\x32\x39\x34\x36\x2c\x30\x2e\x32\x34\x36\x32\x20\x2d\x30\x2e\x34\ -\x32\x32\x33\x2c\x30\x2e\x36\x33\x39\x32\x20\x2d\x30\x2e\x33\x32\ -\x38\x36\x2c\x31\x2e\x30\x31\x31\x34\x6c\x31\x2e\x39\x34\x38\x34\ -\x2c\x37\x2e\x37\x34\x35\x63\x30\x2e\x32\x31\x39\x39\x2c\x30\x2e\ -\x38\x37\x34\x32\x20\x2d\x30\x2e\x37\x33\x38\x31\x2c\x31\x2e\x35\ -\x37\x30\x32\x20\x2d\x31\x2e\x35\x30\x31\x35\x2c\x31\x2e\x30\x39\ -\x30\x39\x6c\x2d\x36\x2e\x37\x36\x33\x38\x2c\x2d\x34\x2e\x32\x34\ -\x36\x34\x63\x2d\x30\x2e\x33\x32\x35\x31\x2c\x2d\x30\x2e\x32\x30\ -\x34\x31\x20\x2d\x30\x2e\x37\x33\x38\x33\x2c\x2d\x30\x2e\x32\x30\ -\x34\x31\x20\x2d\x31\x2e\x30\x36\x33\x34\x2c\x30\x6c\x2d\x36\x2e\ -\x37\x36\x33\x38\x2c\x34\x2e\x32\x34\x36\x34\x63\x2d\x30\x2e\x37\ -\x36\x33\x35\x2c\x30\x2e\x34\x37\x39\x33\x20\x2d\x31\x2e\x37\x32\ -\x31\x34\x2c\x2d\x30\x2e\x32\x31\x36\x37\x20\x2d\x31\x2e\x35\x30\ -\x31\x35\x2c\x2d\x31\x2e\x30\x39\x30\x39\x6c\x31\x2e\x39\x34\x38\ -\x34\x2c\x2d\x37\x2e\x37\x34\x35\x63\x30\x2e\x30\x39\x33\x36\x2c\ -\x2d\x30\x2e\x33\x37\x32\x32\x20\x2d\x30\x2e\x30\x33\x34\x31\x2c\ -\x2d\x30\x2e\x37\x36\x35\x32\x20\x2d\x30\x2e\x33\x32\x38\x36\x2c\ -\x2d\x31\x2e\x30\x31\x31\x34\x6c\x2d\x36\x2e\x31\x32\x38\x37\x2c\ -\x2d\x35\x2e\x31\x32\x30\x35\x63\x2d\x30\x2e\x36\x39\x31\x38\x2c\ -\x2d\x30\x2e\x35\x37\x38\x20\x2d\x30\x2e\x33\x32\x35\x38\x2c\x2d\ -\x31\x2e\x37\x30\x34\x32\x20\x30\x2e\x35\x37\x33\x35\x2c\x2d\x31\ -\x2e\x37\x36\x35\x31\x6c\x37\x2e\x39\x36\x38\x2c\x2d\x30\x2e\x35\ -\x34\x30\x33\x63\x30\x2e\x33\x38\x33\x2c\x2d\x30\x2e\x30\x32\x36\ -\x20\x30\x2e\x37\x31\x37\x33\x2c\x2d\x30\x2e\x32\x36\x38\x39\x20\ -\x30\x2e\x38\x36\x30\x33\x2c\x2d\x30\x2e\x36\x32\x35\x31\x6c\x32\ -\x2e\x39\x37\x36\x31\x2c\x2d\x37\x2e\x34\x31\x31\x31\x7a\x22\x20\ -\x66\x69\x6c\x6c\x3d\x22\x23\x46\x32\x43\x39\x34\x43\x22\x20\x69\ -\x64\x3d\x22\x73\x76\x67\x5f\x32\x22\x2f\x3e\x0a\x20\x20\x3c\x70\ -\x61\x74\x68\x20\x66\x69\x6c\x6c\x3d\x22\x23\x66\x66\x30\x30\x30\ -\x30\x22\x20\x64\x3d\x22\x6d\x35\x2c\x33\x39\x2e\x39\x39\x39\x39\ -\x34\x6c\x30\x2c\x30\x63\x30\x2c\x2d\x31\x38\x2e\x31\x36\x32\x36\ -\x38\x20\x31\x35\x2e\x36\x37\x30\x30\x35\x2c\x2d\x33\x32\x2e\x38\ -\x38\x36\x35\x34\x20\x33\x34\x2e\x39\x39\x39\x39\x37\x2c\x2d\x33\ -\x32\x2e\x38\x38\x36\x35\x34\x6c\x30\x2c\x30\x63\x39\x2e\x32\x38\ -\x32\x37\x2c\x30\x20\x31\x38\x2e\x31\x38\x35\x31\x37\x2c\x33\x2e\ -\x34\x36\x34\x38\x33\x20\x32\x34\x2e\x37\x34\x38\x37\x34\x2c\x39\ -\x2e\x36\x33\x32\x32\x38\x63\x36\x2e\x35\x36\x33\x38\x33\x2c\x36\ -\x2e\x31\x36\x37\x34\x35\x20\x31\x30\x2e\x32\x35\x31\x32\x39\x2c\ -\x31\x34\x2e\x35\x33\x32\x33\x32\x20\x31\x30\x2e\x32\x35\x31\x32\ -\x39\x2c\x32\x33\x2e\x32\x35\x34\x32\x36\x6c\x30\x2c\x30\x63\x30\ -\x2c\x31\x38\x2e\x31\x36\x32\x39\x20\x2d\x31\x35\x2e\x36\x36\x39\ -\x39\x34\x2c\x33\x32\x2e\x38\x38\x36\x36\x36\x20\x2d\x33\x35\x2e\ -\x30\x30\x30\x30\x33\x2c\x33\x32\x2e\x38\x38\x36\x36\x36\x6c\x30\ -\x2c\x30\x63\x2d\x31\x39\x2e\x33\x32\x39\x39\x32\x2c\x30\x20\x2d\ -\x33\x34\x2e\x39\x39\x39\x39\x37\x2c\x2d\x31\x34\x2e\x37\x32\x33\ -\x37\x36\x20\x2d\x33\x34\x2e\x39\x39\x39\x39\x37\x2c\x2d\x33\x32\ -\x2e\x38\x38\x36\x36\x36\x6c\x30\x2c\x30\x7a\x6d\x35\x36\x2e\x35\ -\x31\x37\x30\x33\x2c\x31\x34\x2e\x37\x31\x31\x38\x6c\x30\x2c\x30\ -\x63\x37\x2e\x37\x30\x35\x38\x34\x2c\x2d\x39\x2e\x39\x35\x30\x37\ -\x31\x20\x36\x2e\x35\x36\x30\x33\x39\x2c\x2d\x32\x33\x2e\x36\x39\ -\x30\x33\x36\x20\x2d\x32\x2e\x37\x30\x30\x35\x37\x2c\x2d\x33\x32\ -\x2e\x33\x39\x32\x63\x2d\x39\x2e\x32\x36\x30\x39\x36\x2c\x2d\x38\ -\x2e\x37\x30\x31\x37\x31\x20\x2d\x32\x33\x2e\x38\x38\x33\x35\x35\ -\x2c\x2d\x39\x2e\x37\x37\x38\x20\x2d\x33\x34\x2e\x34\x37\x33\x34\ -\x35\x2c\x2d\x32\x2e\x35\x33\x37\x33\x38\x6c\x33\x37\x2e\x31\x37\ -\x34\x30\x32\x2c\x33\x34\x2e\x39\x32\x39\x33\x38\x7a\x6d\x2d\x34\ -\x33\x2e\x30\x33\x33\x39\x33\x2c\x2d\x32\x39\x2e\x34\x32\x33\x33\ -\x63\x2d\x37\x2e\x37\x30\x35\x39\x2c\x39\x2e\x39\x35\x30\x36\x36\ -\x20\x2d\x36\x2e\x35\x36\x30\x34\x37\x2c\x32\x33\x2e\x36\x39\x30\ -\x33\x20\x32\x2e\x37\x30\x30\x34\x34\x2c\x33\x32\x2e\x33\x39\x31\ -\x38\x32\x63\x39\x2e\x32\x36\x30\x38\x38\x2c\x38\x2e\x37\x30\x31\ -\x37\x36\x20\x32\x33\x2e\x38\x38\x33\x34\x37\x2c\x39\x2e\x37\x37\ -\x38\x30\x35\x20\x33\x34\x2e\x34\x37\x33\x33\x37\x2c\x32\x2e\x35\ -\x33\x37\x35\x6c\x2d\x33\x37\x2e\x31\x37\x33\x38\x31\x2c\x2d\x33\ -\x34\x2e\x39\x32\x39\x33\x32\x6c\x30\x2c\x30\x7a\x22\x20\x69\x64\ -\x3d\x22\x73\x76\x67\x5f\x34\x22\x2f\x3e\x0a\x20\x3c\x2f\x67\x3e\ -\x0a\x0a\x3c\x2f\x73\x76\x67\x3e\ +\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x3e\x0d\x0a\x20\x3c\x67\x3e\ +\x0d\x0a\x20\x20\x3c\x74\x69\x74\x6c\x65\x3e\x4c\x61\x79\x65\x72\ +\x20\x31\x3c\x2f\x74\x69\x74\x6c\x65\x3e\x0d\x0a\x20\x20\x3c\x70\ +\x61\x74\x68\x20\x64\x3d\x22\x6d\x33\x38\x2e\x31\x34\x34\x31\x2c\ +\x31\x35\x2e\x31\x36\x31\x36\x31\x63\x30\x2e\x36\x37\x31\x38\x2c\ +\x2d\x31\x2e\x36\x37\x32\x39\x20\x33\x2e\x30\x34\x2c\x2d\x31\x2e\ +\x36\x37\x32\x39\x20\x33\x2e\x37\x31\x31\x38\x2c\x30\x6c\x35\x2e\ +\x39\x35\x32\x32\x2c\x31\x34\x2e\x38\x32\x32\x32\x63\x30\x2e\x32\ +\x38\x36\x31\x2c\x30\x2e\x37\x31\x32\x34\x20\x30\x2e\x39\x35\x34\ +\x37\x2c\x31\x2e\x31\x39\x38\x31\x20\x31\x2e\x37\x32\x30\x37\x2c\ +\x31\x2e\x32\x35\x30\x31\x6c\x31\x35\x2e\x39\x33\x36\x2c\x31\x2e\ +\x30\x38\x30\x35\x63\x31\x2e\x37\x39\x38\x37\x2c\x30\x2e\x31\x32\ +\x32\x20\x32\x2e\x35\x33\x30\x35\x2c\x32\x2e\x33\x37\x34\x34\x20\ +\x31\x2e\x31\x34\x37\x2c\x33\x2e\x35\x33\x30\x32\x6c\x2d\x31\x32\ +\x2e\x32\x35\x37\x34\x2c\x31\x30\x2e\x32\x34\x31\x32\x63\x2d\x30\ +\x2e\x35\x38\x39\x31\x2c\x30\x2e\x34\x39\x32\x32\x20\x2d\x30\x2e\ +\x38\x34\x34\x35\x2c\x31\x2e\x32\x37\x38\x32\x20\x2d\x30\x2e\x36\ +\x35\x37\x32\x2c\x32\x2e\x30\x32\x32\x37\x6c\x33\x2e\x38\x39\x36\ +\x39\x2c\x31\x35\x2e\x34\x39\x63\x30\x2e\x34\x33\x39\x38\x2c\x31\ +\x2e\x37\x34\x38\x33\x20\x2d\x31\x2e\x34\x37\x36\x32\x2c\x33\x2e\ +\x31\x34\x30\x34\x20\x2d\x33\x2e\x30\x30\x33\x2c\x32\x2e\x31\x38\ +\x31\x38\x6c\x2d\x31\x33\x2e\x35\x32\x37\x37\x2c\x2d\x38\x2e\x34\ +\x39\x32\x38\x63\x2d\x30\x2e\x36\x35\x30\x32\x2c\x2d\x30\x2e\x34\ +\x30\x38\x32\x20\x2d\x31\x2e\x34\x37\x36\x36\x2c\x2d\x30\x2e\x34\ +\x30\x38\x32\x20\x2d\x32\x2e\x31\x32\x36\x38\x2c\x30\x6c\x2d\x31\ +\x33\x2e\x35\x32\x37\x37\x2c\x38\x2e\x34\x39\x32\x38\x63\x2d\x31\ +\x2e\x35\x32\x36\x38\x2c\x30\x2e\x39\x35\x38\x36\x20\x2d\x33\x2e\ +\x34\x34\x32\x38\x2c\x2d\x30\x2e\x34\x33\x33\x35\x20\x2d\x33\x2e\ +\x30\x30\x33\x2c\x2d\x32\x2e\x31\x38\x31\x38\x6c\x33\x2e\x38\x39\ +\x36\x39\x2c\x2d\x31\x35\x2e\x34\x39\x63\x30\x2e\x31\x38\x37\x33\ +\x2c\x2d\x30\x2e\x37\x34\x34\x35\x20\x2d\x30\x2e\x30\x36\x38\x31\ +\x2c\x2d\x31\x2e\x35\x33\x30\x35\x20\x2d\x30\x2e\x36\x35\x37\x32\ +\x2c\x2d\x32\x2e\x30\x32\x32\x37\x6c\x2d\x31\x32\x2e\x32\x35\x37\ +\x34\x2c\x2d\x31\x30\x2e\x32\x34\x31\x32\x63\x2d\x31\x2e\x33\x38\ +\x33\x35\x2c\x2d\x31\x2e\x31\x35\x35\x38\x20\x2d\x30\x2e\x36\x35\ +\x31\x37\x2c\x2d\x33\x2e\x34\x30\x38\x32\x20\x31\x2e\x31\x34\x37\ +\x2c\x2d\x33\x2e\x35\x33\x30\x32\x6c\x31\x35\x2e\x39\x33\x36\x2c\ +\x2d\x31\x2e\x30\x38\x30\x35\x63\x30\x2e\x37\x36\x36\x2c\x2d\x30\ +\x2e\x30\x35\x32\x20\x31\x2e\x34\x33\x34\x36\x2c\x2d\x30\x2e\x35\ +\x33\x37\x37\x20\x31\x2e\x37\x32\x30\x37\x2c\x2d\x31\x2e\x32\x35\ +\x30\x31\x6c\x35\x2e\x39\x35\x32\x32\x2c\x2d\x31\x34\x2e\x38\x32\ +\x32\x32\x7a\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\x46\x32\x39\x39\ +\x34\x41\x22\x20\x69\x64\x3d\x22\x73\x76\x67\x5f\x31\x22\x2f\x3e\ +\x0d\x0a\x20\x20\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x6d\x33\x39\ +\x2e\x30\x35\x31\x35\x2c\x32\x36\x2e\x33\x31\x30\x38\x63\x30\x2e\ +\x33\x33\x35\x39\x2c\x2d\x30\x2e\x38\x33\x36\x34\x20\x31\x2e\x35\ +\x32\x30\x31\x2c\x2d\x30\x2e\x38\x33\x36\x34\x20\x31\x2e\x38\x35\ +\x36\x2c\x30\x6c\x32\x2e\x39\x37\x36\x2c\x37\x2e\x34\x31\x31\x31\ +\x63\x30\x2e\x31\x34\x33\x31\x2c\x30\x2e\x33\x35\x36\x32\x20\x30\ +\x2e\x34\x37\x37\x34\x2c\x30\x2e\x35\x39\x39\x31\x20\x30\x2e\x38\ +\x36\x30\x34\x2c\x30\x2e\x36\x32\x35\x31\x6c\x37\x2e\x39\x36\x38\ +\x2c\x30\x2e\x35\x34\x30\x33\x63\x30\x2e\x38\x39\x39\x33\x2c\x30\ +\x2e\x30\x36\x30\x39\x20\x31\x2e\x32\x36\x35\x32\x2c\x31\x2e\x31\ +\x38\x37\x31\x20\x30\x2e\x35\x37\x33\x35\x2c\x31\x2e\x37\x36\x35\ +\x31\x6c\x2d\x36\x2e\x31\x32\x38\x37\x2c\x35\x2e\x31\x32\x30\x35\ +\x63\x2d\x30\x2e\x32\x39\x34\x36\x2c\x30\x2e\x32\x34\x36\x32\x20\ +\x2d\x30\x2e\x34\x32\x32\x33\x2c\x30\x2e\x36\x33\x39\x32\x20\x2d\ +\x30\x2e\x33\x32\x38\x36\x2c\x31\x2e\x30\x31\x31\x34\x6c\x31\x2e\ +\x39\x34\x38\x34\x2c\x37\x2e\x37\x34\x35\x63\x30\x2e\x32\x31\x39\ +\x39\x2c\x30\x2e\x38\x37\x34\x32\x20\x2d\x30\x2e\x37\x33\x38\x31\ +\x2c\x31\x2e\x35\x37\x30\x32\x20\x2d\x31\x2e\x35\x30\x31\x35\x2c\ +\x31\x2e\x30\x39\x30\x39\x6c\x2d\x36\x2e\x37\x36\x33\x38\x2c\x2d\ +\x34\x2e\x32\x34\x36\x34\x63\x2d\x30\x2e\x33\x32\x35\x31\x2c\x2d\ +\x30\x2e\x32\x30\x34\x31\x20\x2d\x30\x2e\x37\x33\x38\x33\x2c\x2d\ +\x30\x2e\x32\x30\x34\x31\x20\x2d\x31\x2e\x30\x36\x33\x34\x2c\x30\ +\x6c\x2d\x36\x2e\x37\x36\x33\x38\x2c\x34\x2e\x32\x34\x36\x34\x63\ +\x2d\x30\x2e\x37\x36\x33\x35\x2c\x30\x2e\x34\x37\x39\x33\x20\x2d\ +\x31\x2e\x37\x32\x31\x34\x2c\x2d\x30\x2e\x32\x31\x36\x37\x20\x2d\ +\x31\x2e\x35\x30\x31\x35\x2c\x2d\x31\x2e\x30\x39\x30\x39\x6c\x31\ +\x2e\x39\x34\x38\x34\x2c\x2d\x37\x2e\x37\x34\x35\x63\x30\x2e\x30\ +\x39\x33\x36\x2c\x2d\x30\x2e\x33\x37\x32\x32\x20\x2d\x30\x2e\x30\ +\x33\x34\x31\x2c\x2d\x30\x2e\x37\x36\x35\x32\x20\x2d\x30\x2e\x33\ +\x32\x38\x36\x2c\x2d\x31\x2e\x30\x31\x31\x34\x6c\x2d\x36\x2e\x31\ +\x32\x38\x37\x2c\x2d\x35\x2e\x31\x32\x30\x35\x63\x2d\x30\x2e\x36\ +\x39\x31\x38\x2c\x2d\x30\x2e\x35\x37\x38\x20\x2d\x30\x2e\x33\x32\ +\x35\x38\x2c\x2d\x31\x2e\x37\x30\x34\x32\x20\x30\x2e\x35\x37\x33\ +\x35\x2c\x2d\x31\x2e\x37\x36\x35\x31\x6c\x37\x2e\x39\x36\x38\x2c\ +\x2d\x30\x2e\x35\x34\x30\x33\x63\x30\x2e\x33\x38\x33\x2c\x2d\x30\ +\x2e\x30\x32\x36\x20\x30\x2e\x37\x31\x37\x33\x2c\x2d\x30\x2e\x32\ +\x36\x38\x39\x20\x30\x2e\x38\x36\x30\x33\x2c\x2d\x30\x2e\x36\x32\ +\x35\x31\x6c\x32\x2e\x39\x37\x36\x31\x2c\x2d\x37\x2e\x34\x31\x31\ +\x31\x7a\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\x46\x32\x43\x39\x34\ +\x43\x22\x20\x69\x64\x3d\x22\x73\x76\x67\x5f\x32\x22\x2f\x3e\x0d\ +\x0a\x20\x20\x3c\x70\x61\x74\x68\x20\x66\x69\x6c\x6c\x3d\x22\x23\ +\x66\x66\x30\x30\x30\x30\x22\x20\x64\x3d\x22\x6d\x35\x2c\x33\x39\ +\x2e\x39\x39\x39\x39\x34\x6c\x30\x2c\x30\x63\x30\x2c\x2d\x31\x38\ +\x2e\x31\x36\x32\x36\x38\x20\x31\x35\x2e\x36\x37\x30\x30\x35\x2c\ +\x2d\x33\x32\x2e\x38\x38\x36\x35\x34\x20\x33\x34\x2e\x39\x39\x39\ +\x39\x37\x2c\x2d\x33\x32\x2e\x38\x38\x36\x35\x34\x6c\x30\x2c\x30\ +\x63\x39\x2e\x32\x38\x32\x37\x2c\x30\x20\x31\x38\x2e\x31\x38\x35\ +\x31\x37\x2c\x33\x2e\x34\x36\x34\x38\x33\x20\x32\x34\x2e\x37\x34\ +\x38\x37\x34\x2c\x39\x2e\x36\x33\x32\x32\x38\x63\x36\x2e\x35\x36\ +\x33\x38\x33\x2c\x36\x2e\x31\x36\x37\x34\x35\x20\x31\x30\x2e\x32\ +\x35\x31\x32\x39\x2c\x31\x34\x2e\x35\x33\x32\x33\x32\x20\x31\x30\ +\x2e\x32\x35\x31\x32\x39\x2c\x32\x33\x2e\x32\x35\x34\x32\x36\x6c\ +\x30\x2c\x30\x63\x30\x2c\x31\x38\x2e\x31\x36\x32\x39\x20\x2d\x31\ +\x35\x2e\x36\x36\x39\x39\x34\x2c\x33\x32\x2e\x38\x38\x36\x36\x36\ +\x20\x2d\x33\x35\x2e\x30\x30\x30\x30\x33\x2c\x33\x32\x2e\x38\x38\ +\x36\x36\x36\x6c\x30\x2c\x30\x63\x2d\x31\x39\x2e\x33\x32\x39\x39\ +\x32\x2c\x30\x20\x2d\x33\x34\x2e\x39\x39\x39\x39\x37\x2c\x2d\x31\ +\x34\x2e\x37\x32\x33\x37\x36\x20\x2d\x33\x34\x2e\x39\x39\x39\x39\ +\x37\x2c\x2d\x33\x32\x2e\x38\x38\x36\x36\x36\x6c\x30\x2c\x30\x7a\ +\x6d\x35\x36\x2e\x35\x31\x37\x30\x33\x2c\x31\x34\x2e\x37\x31\x31\ +\x38\x6c\x30\x2c\x30\x63\x37\x2e\x37\x30\x35\x38\x34\x2c\x2d\x39\ +\x2e\x39\x35\x30\x37\x31\x20\x36\x2e\x35\x36\x30\x33\x39\x2c\x2d\ +\x32\x33\x2e\x36\x39\x30\x33\x36\x20\x2d\x32\x2e\x37\x30\x30\x35\ +\x37\x2c\x2d\x33\x32\x2e\x33\x39\x32\x63\x2d\x39\x2e\x32\x36\x30\ +\x39\x36\x2c\x2d\x38\x2e\x37\x30\x31\x37\x31\x20\x2d\x32\x33\x2e\ +\x38\x38\x33\x35\x35\x2c\x2d\x39\x2e\x37\x37\x38\x20\x2d\x33\x34\ +\x2e\x34\x37\x33\x34\x35\x2c\x2d\x32\x2e\x35\x33\x37\x33\x38\x6c\ +\x33\x37\x2e\x31\x37\x34\x30\x32\x2c\x33\x34\x2e\x39\x32\x39\x33\ +\x38\x7a\x6d\x2d\x34\x33\x2e\x30\x33\x33\x39\x33\x2c\x2d\x32\x39\ +\x2e\x34\x32\x33\x33\x63\x2d\x37\x2e\x37\x30\x35\x39\x2c\x39\x2e\ +\x39\x35\x30\x36\x36\x20\x2d\x36\x2e\x35\x36\x30\x34\x37\x2c\x32\ +\x33\x2e\x36\x39\x30\x33\x20\x32\x2e\x37\x30\x30\x34\x34\x2c\x33\ +\x32\x2e\x33\x39\x31\x38\x32\x63\x39\x2e\x32\x36\x30\x38\x38\x2c\ +\x38\x2e\x37\x30\x31\x37\x36\x20\x32\x33\x2e\x38\x38\x33\x34\x37\ +\x2c\x39\x2e\x37\x37\x38\x30\x35\x20\x33\x34\x2e\x34\x37\x33\x33\ +\x37\x2c\x32\x2e\x35\x33\x37\x35\x6c\x2d\x33\x37\x2e\x31\x37\x33\ +\x38\x31\x2c\x2d\x33\x34\x2e\x39\x32\x39\x33\x32\x6c\x30\x2c\x30\ +\x7a\x22\x20\x69\x64\x3d\x22\x73\x76\x67\x5f\x34\x22\x2f\x3e\x0d\ +\x0a\x20\x3c\x2f\x67\x3e\x0d\x0a\x0d\x0a\x3c\x2f\x73\x76\x67\x3e\ +\ +\x00\x00\x02\x54\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x20\x73\x74\x61\x6e\x64\x61\x6c\x6f\x6e\x65\x3d\x22\ +\x6e\x6f\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x0a\x20\x20\x20\x78\x6d\ +\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\ +\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\ +\x0a\x20\x20\x20\x69\x64\x3d\x22\x73\x76\x67\x38\x22\x0a\x20\x20\ +\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x31\x22\x0a\x20\ +\x20\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\x65\x72\ +\x20\x66\x65\x61\x74\x68\x65\x72\x2d\x61\x6c\x65\x72\x74\x2d\x6f\ +\x63\x74\x61\x67\x6f\x6e\x22\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\ +\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\x6e\ +\x64\x22\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\ +\x65\x63\x61\x70\x3d\x22\x72\x6f\x75\x6e\x64\x22\x0a\x20\x20\x20\ +\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\x3d\x22\x32\x22\ +\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\ +\x65\x6e\x74\x43\x6f\x6c\x6f\x72\x22\x0a\x20\x20\x20\x66\x69\x6c\ +\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x0a\x20\x20\x20\x76\x69\x65\x77\ +\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\x22\x0a\ +\x20\x20\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x0a\x20\ +\x20\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\x22\x3e\x0a\x20\x20\ +\x3c\x70\x6f\x6c\x79\x67\x6f\x6e\x0a\x20\x20\x20\x20\x20\x73\x74\ +\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\x3a\x23\x65\x38\x37\x37\x32\ +\x32\x22\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x70\x6f\x6c\x79\ +\x67\x6f\x6e\x32\x22\x0a\x20\x20\x20\x20\x20\x70\x6f\x69\x6e\x74\ +\x73\x3d\x22\x37\x2e\x38\x36\x20\x32\x20\x31\x36\x2e\x31\x34\x20\ +\x32\x20\x32\x32\x20\x37\x2e\x38\x36\x20\x32\x32\x20\x31\x36\x2e\ +\x31\x34\x20\x31\x36\x2e\x31\x34\x20\x32\x32\x20\x37\x2e\x38\x36\ +\x20\x32\x32\x20\x32\x20\x31\x36\x2e\x31\x34\x20\x32\x20\x37\x2e\ +\x38\x36\x20\x37\x2e\x38\x36\x20\x32\x22\x20\x2f\x3e\x0a\x20\x20\ +\x3c\x6c\x69\x6e\x65\x20\x69\x64\x3d\x22\x6c\x69\x6e\x65\x34\x22\ +\x20\x79\x32\x3d\x22\x31\x32\x22\x20\x78\x32\x3d\x22\x31\x32\x22\ +\x20\x79\x31\x3d\x22\x38\x22\x20\x78\x31\x3d\x22\x31\x32\x22\x20\ +\x2f\x3e\x0a\x20\x20\x3c\x6c\x69\x6e\x65\x20\x69\x64\x3d\x22\x6c\ +\x69\x6e\x65\x36\x22\x20\x79\x32\x3d\x22\x31\x36\x22\x20\x78\x32\ +\x3d\x22\x31\x32\x2e\x30\x31\x22\x20\x79\x31\x3d\x22\x31\x36\x22\ +\x20\x78\x31\x3d\x22\x31\x32\x22\x20\x2f\x3e\x0a\x3c\x2f\x73\x76\ +\x67\x3e\x0a\ \x00\x00\x01\x6c\ \x3c\ \x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ @@ -240,127 +281,127 @@ \x3d\x22\x38\x20\x36\x20\x32\x20\x31\x32\x20\x38\x20\x31\x38\x22\ \x3e\x3c\x2f\x70\x6f\x6c\x79\x6c\x69\x6e\x65\x3e\x3c\x2f\x73\x76\ \x67\x3e\ -\x00\x00\x03\xa8\ +\x00\x00\x03\xae\ \x00\ -\x00\x0c\x99\x78\x9c\xdd\x56\x4b\x6f\xe3\x36\x10\xbe\xe7\x57\x08\ -\xca\xa5\x45\x23\x8a\xd4\xcb\xa2\x62\x79\x81\x36\x58\xb4\x87\x5e\ -\xba\xbb\xe8\x99\x21\x69\x5b\x1b\x89\x34\x28\x3a\xb6\xf7\xd7\xef\ -\x50\x0f\x5b\x76\x9c\xf4\x71\x28\xd0\x08\x36\xec\x79\x71\x66\xbe\ -\xf9\x38\xf6\xfc\xc3\xbe\xa9\xbd\x67\x69\xda\x4a\xab\xd2\x27\x08\ -\xfb\x9e\x54\x5c\x8b\x4a\xad\x4a\xff\xcb\xe7\x8f\x41\xee\x7b\xad\ -\x65\x4a\xb0\x5a\x2b\x59\xfa\x4a\xfb\x1f\x16\x37\xf3\xf6\x79\x75\ -\xe3\x79\x1e\x04\xab\xb6\x10\xbc\xf4\xd7\xd6\x6e\x8a\x30\xdc\x6c\ -\x4d\x8d\xb4\x59\x85\x82\x87\xb2\x96\x8d\x54\xb6\x0d\x09\x22\xa1\ -\x7f\x72\xe7\x27\x77\x6e\x24\xb3\xd5\xb3\xe4\xba\x69\xb4\x6a\xbb\ -\x48\xd5\xde\x4e\x9c\x8d\x58\x1e\xbd\x77\xbb\x1d\xda\xc5\x9d\x13\ -\xa1\x94\x86\x38\x0a\xa3\x28\x00\x8f\xa0\x3d\x28\xcb\xf6\xc1\x79\ -\x28\xd4\x78\x2d\x34\xc2\x18\x87\x60\x3b\x79\xfe\x3d\xaf\xa2\x05\ -\x54\x36\xf0\x3e\xba\x8f\x0a\xd4\xea\xad\xe1\x72\x09\x71\x12\x29\ -\x69\xc3\x87\xcf\x0f\x47\x63\x80\x91\xb0\x62\x72\x4c\xa5\x9e\x5a\ -\xce\x36\xf2\x2c\xeb\xa8\xec\x11\x60\x8d\x6c\x37\x8c\xcb\x36\x1c\ -\xf5\x5d\xfc\x28\x14\xd3\x79\x19\x4e\xbc\x1f\x30\xa5\x19\x16\xd9\ -\x12\xa7\x77\x5e\x84\x23\x1c\xe0\x24\xc0\xf4\xc7\x2e\x6a\x2c\xa4\ -\x10\x9a\xbb\x93\x4b\x5f\xee\x37\x30\x50\x34\x76\x57\x89\xd2\x87\ -\xef\x59\x27\x4c\x8e\x26\x9d\x82\xd7\xac\x05\x84\x96\x30\xa8\xb5\ -\x34\xde\xf0\x19\x00\x45\xfa\xa2\x5a\x6b\xf4\x93\x0c\xea\x4a\xc9\ -\xaf\xba\x82\x40\xa3\xb7\x4a\x5c\x9a\xa0\xec\x2b\x96\x5d\x25\xec\ -\xba\xf4\xa3\x89\xae\xf4\xf9\xd6\x18\xa0\xcd\x2f\xba\xd6\xa6\x33\ -\x2c\xab\xba\x76\xc4\x53\x7d\xc2\xe7\x4a\xee\x7e\xd6\xfb\xd2\xc7\ -\x1e\xf6\xa2\x04\x5e\x9d\x7a\x2d\xab\xd5\xda\xc2\x61\xbd\x38\x1e\ -\x9d\xf8\x0b\x10\xe7\x8d\xb4\x4c\x30\xcb\x9c\xa9\xef\x78\xd4\x90\ -\xa8\xf3\x00\x1f\x20\x52\xf1\xc7\xc3\xc7\x5e\x02\x99\xf3\xe2\x4f\ -\x6d\x9e\x06\x11\x1e\xe7\xc0\x1e\xf5\x16\xb2\xf8\x8b\xa3\x7a\x2e\ -\x78\x01\xa3\x6f\x98\x5d\x54\x0d\x5b\x49\xc7\x9a\x9f\x60\xd4\xf3\ -\xf0\x64\x38\x73\xb6\x87\x8d\x3c\x1d\xda\x1f\x6b\x64\xcf\xa1\xab\ -\x17\x49\xf0\xa6\x72\x41\xe1\x27\x0b\x50\xfc\xe6\x92\xf8\x5e\x78\ -\x71\x68\x65\x6b\xb9\xe8\x72\xf6\x5f\xc7\x2e\xc2\xa1\x8d\xa1\xc9\ -\x70\xd2\xe5\x3c\x1c\x41\xe8\x24\x21\x97\xed\x09\x1f\x27\x11\x3c\ -\xe4\x99\x1f\x49\xe4\x18\x24\xdc\x08\x06\xcf\x91\x92\xc3\xd4\x82\ -\x9a\x1d\xa4\x99\xf0\x69\xe2\xb2\xab\x94\xd0\xbb\xa0\x61\xfb\xaa\ -\xa9\xbe\x49\xc8\x81\x5f\x71\x39\x00\xfd\xf2\xf4\x15\x23\x4c\x9e\ -\xc4\xf9\xec\xd2\xca\x5d\x50\x84\x32\x1c\xc7\xf1\x8b\xd4\x7c\xdf\ -\x19\x93\x59\x7c\x25\xf2\x9b\xd6\x0d\x30\x85\xa2\x8c\xe6\xc9\x31\ -\x6d\xbb\xd6\xbb\x95\x71\x48\x2c\x59\xdd\x4a\xff\x84\xcc\x11\x82\ -\xfc\x95\x0a\x47\x2a\x12\x12\xbd\xe6\x32\xd0\x93\xd0\x59\x72\xe9\ -\xb1\x81\xf1\xb6\x6b\x06\x5e\xe3\xcd\xb8\x30\x6a\x58\x0d\xc0\x87\ -\x13\x7c\xab\x6d\x25\xa4\xd5\xb5\x34\x4c\x39\x0a\x91\xa3\x01\xea\ -\xbf\xa6\xd7\x8f\x5f\x25\xb7\xd7\x2c\x8f\xda\x08\x69\x8e\x19\xc8\ -\x99\x9a\xbb\x2b\x59\xfa\xb7\x59\xf7\x0c\x26\x57\xd1\x68\x58\x76\ -\xcf\xc8\x99\x0d\x6c\x8a\x01\x4b\x7b\xa8\x21\x8b\xbb\xc8\x85\xbb\ -\xc7\xf7\xfd\x5d\x2f\x6e\x71\xf7\xdc\x4f\xd7\x41\x11\xdd\x9f\xef\ -\x8d\xa2\x5b\x1b\xf7\x17\x7b\xa6\x80\x2b\x21\xcd\xa8\xed\x84\x1a\ -\x68\x65\x8b\x64\xd4\x09\x06\x28\x1a\xc3\x0e\xd3\x94\xc1\xd0\x5a\ -\x31\x76\x06\xf3\xfc\xdd\x4b\x50\x84\x73\x9a\xe5\xf4\x2e\x42\x69\ -\x4a\x71\x1a\x13\xef\x57\x2f\x22\x28\x4d\x28\x8d\xc8\x64\xf6\xae\ -\xa7\x3c\x7d\x49\x3e\xad\xa0\x56\xab\x61\x2f\x6e\xcd\x33\xb3\x5b\ -\x23\xdd\x78\xfe\xcf\x40\x10\x44\x31\xc5\x79\xf6\x26\x10\xf4\xfd\ -\x03\x41\x30\x8a\x28\xc5\xd9\xec\x2d\x20\x72\xf2\x5e\x81\x98\xa1\ -\x2c\x89\x93\x7c\x96\xdd\x65\x28\x89\x00\x87\x37\x61\x78\xb1\xb3\ -\xdf\x1f\x0c\x24\x41\x24\xa3\x78\x16\xbf\x09\xc4\xbf\xde\x10\x7f\ -\x15\x70\x79\x03\xd3\x53\x95\x8d\x07\x3f\x71\x18\xb8\x98\x02\x6b\ -\x73\x84\x63\xa8\x92\x7a\x6b\x8f\xa2\x24\x23\xb3\x6c\xfc\x2d\xf9\ -\x47\x50\xc3\x16\x48\xd3\x28\xff\x8f\x00\x77\x68\xcc\xdd\xff\xa7\ -\xc5\xcd\x77\xd2\xf3\xe7\xb2\ -\x00\x00\x03\x9b\ +\x00\x0c\xf0\x78\x9c\xe5\x56\x4d\x8f\xdb\x36\x10\xbd\x07\xc8\x7f\ +\x10\xb4\x97\x16\x5d\x51\xa4\xbe\x2c\x6a\x2d\x07\x68\x17\x41\x7b\ +\xe8\xa5\x4d\xd1\x33\x97\xa4\x6d\x65\x25\xd1\xa0\xe8\xaf\xfc\xfa\ +\x0e\x25\x53\xd6\xae\xbd\xdb\x16\x01\x02\x24\x11\x6c\xd8\x33\xf3\ +\x86\xc3\x79\x7c\x1c\x7b\xfe\xee\xd0\xd4\xde\x4e\xea\xae\x52\x6d\ +\xe9\x13\x84\x7d\x4f\xb6\x5c\x89\xaa\x5d\x95\xfe\x5f\x1f\xde\x07\ +\xb9\xef\x75\x86\xb5\x82\xd5\xaa\x95\xa5\xdf\x2a\xff\xdd\xe2\xed\ +\x9b\x79\xb7\x5b\xbd\x7d\xe3\x79\x1e\xa4\xb7\x5d\x21\x78\xe9\xaf\ +\x8d\xd9\x14\x61\xb8\xd9\xea\x1a\x29\xbd\x0a\x05\x0f\x65\x2d\x1b\ +\xd9\x9a\x2e\x24\x88\x84\xfe\x04\xcf\xcf\x78\xae\x25\x33\xd5\x4e\ +\x72\xd5\x34\xaa\xed\xfa\xd4\xb6\xbb\x99\xa2\xb5\x58\x8e\xf0\xfd\ +\x7e\x8f\xf6\x71\x8f\x22\x94\xd2\x10\x47\x61\x14\x05\x80\x08\xba\ +\x63\x6b\xd8\x21\x78\x96\x0b\xfb\xbc\x96\x1b\x61\x8c\x43\x88\x4d\ +\xa0\xff\x11\x56\x74\x40\xce\x06\xde\x23\xde\x39\x50\xa7\xb6\x9a\ +\xcb\x25\x24\x4a\xd4\x4a\x13\xde\x7f\xb8\x1f\x83\x01\x46\xc2\x88\ +\xe9\x3a\x55\xfb\xd8\x71\xb6\x91\x4f\xea\x3a\xe7\x40\x03\x6b\x64\ +\xb7\x61\x5c\x76\xa1\xf3\x0f\x0b\x38\xab\x98\x1e\x9c\xe6\xc4\xfb\ +\x01\x53\x9a\x61\x91\x2d\x71\x7a\xeb\x45\x38\xc2\x01\x4e\x02\x4c\ +\x7f\x1c\xd2\xdc\x5e\x0a\xa1\xb8\x5d\xbb\xf4\xe5\x61\x03\x47\x8b\ +\xc6\x0e\x2b\x51\xfa\x60\x64\x83\x35\x59\x9d\x0c\x1e\x5e\xb3\x0e\ +\x88\x5a\xc2\x91\xad\xa5\xf6\x4e\x9f\x01\xe8\xe5\xb4\xb3\xce\x68\ +\xf5\x28\x83\xba\x6a\xe5\x47\x55\x41\xaa\x56\xdb\x56\x5c\xc4\x60\ +\xf3\xd7\x42\xfb\x4a\x98\x75\xe9\x47\x53\x67\xe9\xf3\xad\xd6\xa0\ +\xa2\x5f\x54\xad\xf4\x10\x59\x56\x75\x6d\xa5\xd8\x9e\xaa\xee\x2a\ +\xb9\xff\x59\x1d\x4a\x1f\x7b\xd8\x8b\x12\x78\x0d\xfe\xb5\xac\x56\ +\x6b\x03\x0b\x9e\x6c\xb7\x7e\xe2\x2f\xac\x3d\x6f\xa4\x61\x82\x19\ +\xd6\x07\x87\xee\x9d\x8b\x44\x03\x06\x50\x20\xae\xe2\x8f\xfb\xf7\ +\x27\x13\x1c\x9c\x17\x7f\x2b\xfd\xe8\x6c\x78\x2c\x84\x3d\xa8\x2d\ +\xd4\xf2\x17\x67\xff\x5c\xf0\x02\xe4\xd0\x30\xb3\xa8\x1a\xb6\x92\ +\x56\x4a\x3f\xc1\xe9\xcf\xc3\x73\xe0\x29\xda\x1c\x37\x72\xb2\xee\ +\xb0\xb2\x96\x83\xb2\xae\xde\x31\xc1\x9b\xca\x66\x85\x7f\x1a\x60\ +\xe5\x37\x5b\xc6\xf7\xc2\xe7\xcb\x56\xa6\x96\x8b\xbe\xec\xf0\x75\ +\xec\x25\x3c\x35\xe3\x9a\x0d\xa7\xdd\xce\x43\x47\xc7\x60\x0a\xb9\ +\xec\x26\x5c\x59\x93\x60\x57\x6d\x3e\xca\xcb\x6a\x4b\xd8\x33\x71\ +\x58\x27\xd7\xd3\x49\x06\x35\x3b\x4a\x3d\x55\xda\x04\xb3\xaf\x5a\ +\xa1\xf6\x41\xc3\x0e\x55\x53\x7d\x92\x50\x06\xbf\x84\x39\x82\x32\ +\xf3\xf4\xa5\x28\xc8\x81\xc4\xf9\xec\x22\xcc\x6d\x5a\x84\x32\x1c\ +\xc7\xf1\x65\x79\x7e\xe8\xa3\xc9\x2c\xbe\x96\xfb\x49\xa9\x06\xf4\ +\x43\x51\x46\xf3\xe4\x5c\xba\x5b\xab\xfd\x4a\x5b\x4a\x96\xac\xee\ +\xa4\x3f\xe1\x68\xe4\x22\x7f\x69\x9f\x4e\xa4\x84\x44\x2f\x62\x4e\ +\xc2\x25\x74\x96\x5c\x40\x36\x70\xe2\xdd\x9a\x01\x6c\xbc\x39\xcf\ +\xa2\x0a\x66\x08\x68\x64\xc2\xe4\x6a\x5b\x09\x69\x54\x2d\x35\x6b\ +\xad\xae\xc8\x39\x02\x6d\x5c\x0d\xa8\x87\x8f\x92\x9b\xab\xa1\x07\ +\xa5\x85\xd4\x63\x15\xf2\xd4\xcf\xed\xbd\x2d\xfd\x9b\xac\x7f\x5c\ +\xcc\xee\xcb\x45\x96\xfd\x33\xea\x68\x03\x43\xc5\x11\x6b\x8e\x35\ +\x94\xb2\xf7\xbd\xb0\xd7\xfd\x6e\x98\x09\xc5\x0d\xee\x9f\xbb\xe9\ +\xdc\x28\xa2\xbb\xa7\x13\xa6\xe8\x07\xcc\xdd\xb3\x91\x54\xc0\x75\ +\x91\xda\x79\x7b\xa3\x06\xa5\x99\x22\x71\x3e\xc1\x80\x4e\xad\xd9\ +\x71\x5a\x32\x38\xb5\x57\x8c\xdd\xc1\xe1\xfe\xee\x25\x28\xc2\x39\ +\xcd\x72\x7a\x1b\xa1\x34\xa5\x38\x8d\x89\xf7\xab\x17\x11\x94\x26\ +\x94\x46\x64\xaa\x04\xdb\x56\x9e\x5e\xd1\xa3\x6a\x61\xbb\x46\xc1\ +\x18\xdd\xea\x1d\x33\x5b\x2d\xed\x49\x7d\xe5\x6c\x10\x44\x31\xc5\ +\x79\xf6\x3a\x1b\xf4\xfb\x60\x83\x60\x14\x51\x8a\xb3\xd9\xab\x6c\ +\xe4\xe4\x5b\x66\x63\x86\xb2\x24\x4e\xf2\x59\x76\x9b\xa1\x24\x02\ +\x32\x5e\xe7\xe2\x72\xac\x7f\x93\x5c\x90\x04\x91\x8c\xe2\x59\xfc\ +\x3a\x1b\x9f\x33\x35\xfe\x2d\xe5\xe2\x52\xa6\x93\xbd\x36\x1e\xfc\ +\x18\x62\x90\x66\x0a\x2a\xce\x11\x8e\x61\xaf\xd4\x5b\x7b\x14\x25\ +\x19\x99\x65\xe3\xef\xcd\xff\xa2\x1c\x66\x43\x9a\x46\xf9\x17\x22\ +\xbe\xa7\x64\x6e\xff\x7a\xc1\xe7\x3f\xa8\xb2\xec\x1d\ +\x00\x00\x03\x9d\ \x3c\ \x73\x76\x67\x20\x77\x69\x64\x74\x68\x3d\x22\x38\x30\x22\x20\x68\ \x65\x69\x67\x68\x74\x3d\x22\x38\x30\x22\x20\x76\x69\x65\x77\x42\ \x6f\x78\x3d\x22\x30\x20\x30\x20\x38\x30\x20\x38\x30\x22\x20\x66\ \x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x78\x6d\x6c\x6e\x73\ \x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\ -\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x3e\x0a\x20\ -\x20\x3c\x70\x61\x74\x68\x20\x66\x69\x6c\x6c\x2d\x72\x75\x6c\x65\ -\x3d\x22\x65\x76\x65\x6e\x6f\x64\x64\x22\x20\x63\x6c\x69\x70\x2d\ -\x72\x75\x6c\x65\x3d\x22\x65\x76\x65\x6e\x6f\x64\x64\x22\x20\x64\ -\x3d\x22\x4d\x33\x33\x2e\x39\x32\x35\x36\x20\x39\x2e\x38\x38\x36\ -\x34\x37\x43\x33\x33\x2e\x31\x36\x34\x38\x20\x39\x2e\x38\x38\x36\ -\x34\x37\x20\x33\x32\x2e\x34\x34\x35\x34\x20\x31\x30\x2e\x32\x33\ -\x32\x39\x20\x33\x31\x2e\x39\x37\x31\x31\x20\x31\x30\x2e\x38\x32\ -\x37\x37\x4c\x32\x36\x2e\x36\x35\x30\x31\x20\x31\x37\x2e\x35\x48\ -\x31\x35\x43\x31\x33\x2e\x36\x31\x39\x33\x20\x31\x37\x2e\x35\x20\ -\x31\x32\x2e\x35\x20\x31\x38\x2e\x36\x31\x39\x33\x20\x31\x32\x2e\ -\x35\x20\x32\x30\x43\x31\x32\x2e\x35\x20\x32\x31\x2e\x33\x38\x30\ -\x37\x20\x31\x33\x2e\x36\x31\x39\x33\x20\x32\x32\x2e\x35\x20\x31\ -\x35\x20\x32\x32\x2e\x35\x48\x31\x36\x2e\x35\x56\x36\x34\x43\x31\ -\x36\x2e\x35\x20\x36\x37\x2e\x35\x38\x39\x39\x20\x31\x39\x2e\x34\ -\x31\x30\x31\x20\x37\x30\x2e\x35\x20\x32\x33\x20\x37\x30\x2e\x35\ -\x48\x35\x37\x43\x36\x30\x2e\x35\x38\x39\x38\x20\x37\x30\x2e\x35\ -\x20\x36\x33\x2e\x35\x20\x36\x37\x2e\x35\x38\x39\x39\x20\x36\x33\ -\x2e\x35\x20\x36\x34\x56\x32\x32\x2e\x35\x48\x36\x35\x43\x36\x36\ -\x2e\x33\x38\x30\x37\x20\x32\x32\x2e\x35\x20\x36\x37\x2e\x35\x20\ -\x32\x31\x2e\x33\x38\x30\x37\x20\x36\x37\x2e\x35\x20\x32\x30\x43\ -\x36\x37\x2e\x35\x20\x31\x38\x2e\x36\x31\x39\x33\x20\x36\x36\x2e\ -\x33\x38\x30\x37\x20\x31\x37\x2e\x35\x20\x36\x35\x20\x31\x37\x2e\ -\x35\x48\x35\x33\x2e\x33\x34\x39\x39\x4c\x34\x38\x2e\x30\x32\x39\ -\x20\x31\x30\x2e\x38\x32\x37\x38\x43\x34\x37\x2e\x35\x35\x34\x36\ -\x20\x31\x30\x2e\x32\x33\x32\x39\x20\x34\x36\x2e\x38\x33\x35\x32\ -\x20\x39\x2e\x38\x38\x36\x34\x37\x20\x34\x36\x2e\x30\x37\x34\x34\ -\x20\x39\x2e\x38\x38\x36\x34\x37\x48\x33\x33\x2e\x39\x32\x35\x36\ -\x5a\x4d\x33\x33\x20\x32\x37\x2e\x35\x43\x33\x34\x2e\x33\x38\x30\ -\x37\x20\x32\x37\x2e\x35\x20\x33\x35\x2e\x35\x20\x32\x38\x2e\x36\ -\x31\x39\x33\x20\x33\x35\x2e\x35\x20\x33\x30\x56\x35\x38\x43\x33\ -\x35\x2e\x35\x20\x35\x39\x2e\x33\x38\x30\x37\x20\x33\x34\x2e\x33\ -\x38\x30\x37\x20\x36\x30\x2e\x35\x20\x33\x33\x20\x36\x30\x2e\x35\ -\x43\x33\x31\x2e\x36\x31\x39\x33\x20\x36\x30\x2e\x35\x20\x33\x30\ -\x2e\x35\x20\x35\x39\x2e\x33\x38\x30\x37\x20\x33\x30\x2e\x35\x20\ -\x35\x38\x56\x33\x30\x43\x33\x30\x2e\x35\x20\x32\x38\x2e\x36\x31\ -\x39\x33\x20\x33\x31\x2e\x36\x31\x39\x33\x20\x32\x37\x2e\x35\x20\ -\x33\x33\x20\x32\x37\x2e\x35\x5a\x4d\x34\x39\x2e\x35\x20\x33\x30\ -\x43\x34\x39\x2e\x35\x20\x32\x38\x2e\x36\x31\x39\x33\x20\x34\x38\ -\x2e\x33\x38\x30\x37\x20\x32\x37\x2e\x35\x20\x34\x37\x20\x32\x37\ -\x2e\x35\x43\x34\x35\x2e\x36\x31\x39\x33\x20\x32\x37\x2e\x35\x20\ -\x34\x34\x2e\x35\x20\x32\x38\x2e\x36\x31\x39\x33\x20\x34\x34\x2e\ -\x35\x20\x33\x30\x56\x35\x38\x43\x34\x34\x2e\x35\x20\x35\x39\x2e\ -\x33\x38\x30\x37\x20\x34\x35\x2e\x36\x31\x39\x33\x20\x36\x30\x2e\ -\x35\x20\x34\x37\x20\x36\x30\x2e\x35\x43\x34\x38\x2e\x33\x38\x30\ -\x37\x20\x36\x30\x2e\x35\x20\x34\x39\x2e\x35\x20\x35\x39\x2e\x33\ -\x38\x30\x37\x20\x34\x39\x2e\x35\x20\x35\x38\x56\x33\x30\x5a\x4d\ -\x34\x36\x2e\x39\x35\x33\x36\x20\x31\x37\x2e\x34\x39\x38\x36\x4c\ -\x34\x34\x2e\x38\x37\x30\x34\x20\x31\x34\x2e\x38\x38\x36\x35\x48\ -\x33\x35\x2e\x31\x32\x39\x36\x4c\x33\x33\x2e\x30\x34\x36\x34\x20\ -\x31\x37\x2e\x34\x39\x38\x36\x48\x34\x36\x2e\x39\x35\x33\x36\x5a\ -\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\x43\x32\x43\x43\x44\x45\x22\ -\x20\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ +\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x3e\x0d\x0a\ +\x20\x20\x3c\x70\x61\x74\x68\x20\x66\x69\x6c\x6c\x2d\x72\x75\x6c\ +\x65\x3d\x22\x65\x76\x65\x6e\x6f\x64\x64\x22\x20\x63\x6c\x69\x70\ +\x2d\x72\x75\x6c\x65\x3d\x22\x65\x76\x65\x6e\x6f\x64\x64\x22\x20\ +\x64\x3d\x22\x4d\x33\x33\x2e\x39\x32\x35\x36\x20\x39\x2e\x38\x38\ +\x36\x34\x37\x43\x33\x33\x2e\x31\x36\x34\x38\x20\x39\x2e\x38\x38\ +\x36\x34\x37\x20\x33\x32\x2e\x34\x34\x35\x34\x20\x31\x30\x2e\x32\ +\x33\x32\x39\x20\x33\x31\x2e\x39\x37\x31\x31\x20\x31\x30\x2e\x38\ +\x32\x37\x37\x4c\x32\x36\x2e\x36\x35\x30\x31\x20\x31\x37\x2e\x35\ +\x48\x31\x35\x43\x31\x33\x2e\x36\x31\x39\x33\x20\x31\x37\x2e\x35\ +\x20\x31\x32\x2e\x35\x20\x31\x38\x2e\x36\x31\x39\x33\x20\x31\x32\ +\x2e\x35\x20\x32\x30\x43\x31\x32\x2e\x35\x20\x32\x31\x2e\x33\x38\ +\x30\x37\x20\x31\x33\x2e\x36\x31\x39\x33\x20\x32\x32\x2e\x35\x20\ +\x31\x35\x20\x32\x32\x2e\x35\x48\x31\x36\x2e\x35\x56\x36\x34\x43\ +\x31\x36\x2e\x35\x20\x36\x37\x2e\x35\x38\x39\x39\x20\x31\x39\x2e\ +\x34\x31\x30\x31\x20\x37\x30\x2e\x35\x20\x32\x33\x20\x37\x30\x2e\ +\x35\x48\x35\x37\x43\x36\x30\x2e\x35\x38\x39\x38\x20\x37\x30\x2e\ +\x35\x20\x36\x33\x2e\x35\x20\x36\x37\x2e\x35\x38\x39\x39\x20\x36\ +\x33\x2e\x35\x20\x36\x34\x56\x32\x32\x2e\x35\x48\x36\x35\x43\x36\ +\x36\x2e\x33\x38\x30\x37\x20\x32\x32\x2e\x35\x20\x36\x37\x2e\x35\ +\x20\x32\x31\x2e\x33\x38\x30\x37\x20\x36\x37\x2e\x35\x20\x32\x30\ +\x43\x36\x37\x2e\x35\x20\x31\x38\x2e\x36\x31\x39\x33\x20\x36\x36\ +\x2e\x33\x38\x30\x37\x20\x31\x37\x2e\x35\x20\x36\x35\x20\x31\x37\ +\x2e\x35\x48\x35\x33\x2e\x33\x34\x39\x39\x4c\x34\x38\x2e\x30\x32\ +\x39\x20\x31\x30\x2e\x38\x32\x37\x38\x43\x34\x37\x2e\x35\x35\x34\ +\x36\x20\x31\x30\x2e\x32\x33\x32\x39\x20\x34\x36\x2e\x38\x33\x35\ +\x32\x20\x39\x2e\x38\x38\x36\x34\x37\x20\x34\x36\x2e\x30\x37\x34\ +\x34\x20\x39\x2e\x38\x38\x36\x34\x37\x48\x33\x33\x2e\x39\x32\x35\ +\x36\x5a\x4d\x33\x33\x20\x32\x37\x2e\x35\x43\x33\x34\x2e\x33\x38\ +\x30\x37\x20\x32\x37\x2e\x35\x20\x33\x35\x2e\x35\x20\x32\x38\x2e\ +\x36\x31\x39\x33\x20\x33\x35\x2e\x35\x20\x33\x30\x56\x35\x38\x43\ +\x33\x35\x2e\x35\x20\x35\x39\x2e\x33\x38\x30\x37\x20\x33\x34\x2e\ +\x33\x38\x30\x37\x20\x36\x30\x2e\x35\x20\x33\x33\x20\x36\x30\x2e\ +\x35\x43\x33\x31\x2e\x36\x31\x39\x33\x20\x36\x30\x2e\x35\x20\x33\ +\x30\x2e\x35\x20\x35\x39\x2e\x33\x38\x30\x37\x20\x33\x30\x2e\x35\ +\x20\x35\x38\x56\x33\x30\x43\x33\x30\x2e\x35\x20\x32\x38\x2e\x36\ +\x31\x39\x33\x20\x33\x31\x2e\x36\x31\x39\x33\x20\x32\x37\x2e\x35\ +\x20\x33\x33\x20\x32\x37\x2e\x35\x5a\x4d\x34\x39\x2e\x35\x20\x33\ +\x30\x43\x34\x39\x2e\x35\x20\x32\x38\x2e\x36\x31\x39\x33\x20\x34\ +\x38\x2e\x33\x38\x30\x37\x20\x32\x37\x2e\x35\x20\x34\x37\x20\x32\ +\x37\x2e\x35\x43\x34\x35\x2e\x36\x31\x39\x33\x20\x32\x37\x2e\x35\ +\x20\x34\x34\x2e\x35\x20\x32\x38\x2e\x36\x31\x39\x33\x20\x34\x34\ +\x2e\x35\x20\x33\x30\x56\x35\x38\x43\x34\x34\x2e\x35\x20\x35\x39\ +\x2e\x33\x38\x30\x37\x20\x34\x35\x2e\x36\x31\x39\x33\x20\x36\x30\ +\x2e\x35\x20\x34\x37\x20\x36\x30\x2e\x35\x43\x34\x38\x2e\x33\x38\ +\x30\x37\x20\x36\x30\x2e\x35\x20\x34\x39\x2e\x35\x20\x35\x39\x2e\ +\x33\x38\x30\x37\x20\x34\x39\x2e\x35\x20\x35\x38\x56\x33\x30\x5a\ +\x4d\x34\x36\x2e\x39\x35\x33\x36\x20\x31\x37\x2e\x34\x39\x38\x36\ +\x4c\x34\x34\x2e\x38\x37\x30\x34\x20\x31\x34\x2e\x38\x38\x36\x35\ +\x48\x33\x35\x2e\x31\x32\x39\x36\x4c\x33\x33\x2e\x30\x34\x36\x34\ +\x20\x31\x37\x2e\x34\x39\x38\x36\x48\x34\x36\x2e\x39\x35\x33\x36\ +\x5a\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\x43\x32\x43\x43\x44\x45\ +\x22\x20\x2f\x3e\x0d\x0a\x3c\x2f\x73\x76\x67\x3e\ \x00\x00\x01\x88\ \x3c\ \x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ @@ -413,101 +454,101 @@ \x6e\x65\x20\x78\x31\x3d\x22\x31\x35\x22\x20\x79\x31\x3d\x22\x31\ \x32\x22\x20\x78\x32\x3d\x22\x33\x22\x20\x79\x32\x3d\x22\x31\x32\ \x22\x3e\x3c\x2f\x6c\x69\x6e\x65\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x05\xba\ +\x00\x00\x05\xbd\ \x3c\ \x73\x76\x67\x20\x77\x69\x64\x74\x68\x3d\x22\x38\x30\x22\x20\x68\ \x65\x69\x67\x68\x74\x3d\x22\x38\x30\x22\x20\x76\x69\x65\x77\x42\ \x6f\x78\x3d\x22\x30\x20\x30\x20\x38\x30\x20\x38\x30\x22\x20\x66\ \x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x78\x6d\x6c\x6e\x73\ \x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\ -\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x3e\x0a\x20\ -\x20\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x33\x38\x2e\x31\x34\ -\x34\x31\x20\x31\x32\x2e\x36\x32\x31\x37\x43\x33\x38\x2e\x38\x31\ -\x35\x39\x20\x31\x30\x2e\x39\x34\x38\x38\x20\x34\x31\x2e\x31\x38\ -\x34\x31\x20\x31\x30\x2e\x39\x34\x38\x38\x20\x34\x31\x2e\x38\x35\ -\x35\x39\x20\x31\x32\x2e\x36\x32\x31\x37\x4c\x34\x37\x2e\x38\x30\ -\x38\x31\x20\x32\x37\x2e\x34\x34\x33\x39\x43\x34\x38\x2e\x30\x39\ -\x34\x32\x20\x32\x38\x2e\x31\x35\x36\x33\x20\x34\x38\x2e\x37\x36\ -\x32\x38\x20\x32\x38\x2e\x36\x34\x32\x20\x34\x39\x2e\x35\x32\x38\ -\x38\x20\x32\x38\x2e\x36\x39\x34\x4c\x36\x35\x2e\x34\x36\x34\x38\ -\x20\x32\x39\x2e\x37\x37\x34\x35\x43\x36\x37\x2e\x32\x36\x33\x35\ -\x20\x32\x39\x2e\x38\x39\x36\x35\x20\x36\x37\x2e\x39\x39\x35\x33\ -\x20\x33\x32\x2e\x31\x34\x38\x39\x20\x36\x36\x2e\x36\x31\x31\x38\ -\x20\x33\x33\x2e\x33\x30\x34\x37\x4c\x35\x34\x2e\x33\x35\x34\x34\ -\x20\x34\x33\x2e\x35\x34\x35\x39\x43\x35\x33\x2e\x37\x36\x35\x33\ -\x20\x34\x34\x2e\x30\x33\x38\x31\x20\x35\x33\x2e\x35\x30\x39\x39\ -\x20\x34\x34\x2e\x38\x32\x34\x31\x20\x35\x33\x2e\x36\x39\x37\x32\ -\x20\x34\x35\x2e\x35\x36\x38\x36\x4c\x35\x37\x2e\x35\x39\x34\x31\ -\x20\x36\x31\x2e\x30\x35\x38\x36\x43\x35\x38\x2e\x30\x33\x33\x39\ -\x20\x36\x32\x2e\x38\x30\x36\x39\x20\x35\x36\x2e\x31\x31\x37\x39\ -\x20\x36\x34\x2e\x31\x39\x39\x20\x35\x34\x2e\x35\x39\x31\x31\x20\ -\x36\x33\x2e\x32\x34\x30\x34\x4c\x34\x31\x2e\x30\x36\x33\x34\x20\ -\x35\x34\x2e\x37\x34\x37\x36\x43\x34\x30\x2e\x34\x31\x33\x32\x20\ -\x35\x34\x2e\x33\x33\x39\x34\x20\x33\x39\x2e\x35\x38\x36\x38\x20\ -\x35\x34\x2e\x33\x33\x39\x34\x20\x33\x38\x2e\x39\x33\x36\x36\x20\ -\x35\x34\x2e\x37\x34\x37\x36\x4c\x32\x35\x2e\x34\x30\x38\x39\x20\ -\x36\x33\x2e\x32\x34\x30\x34\x43\x32\x33\x2e\x38\x38\x32\x31\x20\ -\x36\x34\x2e\x31\x39\x39\x20\x32\x31\x2e\x39\x36\x36\x31\x20\x36\ -\x32\x2e\x38\x30\x36\x39\x20\x32\x32\x2e\x34\x30\x35\x39\x20\x36\ -\x31\x2e\x30\x35\x38\x36\x4c\x32\x36\x2e\x33\x30\x32\x38\x20\x34\ -\x35\x2e\x35\x36\x38\x36\x43\x32\x36\x2e\x34\x39\x30\x31\x20\x34\ -\x34\x2e\x38\x32\x34\x31\x20\x32\x36\x2e\x32\x33\x34\x37\x20\x34\ -\x34\x2e\x30\x33\x38\x31\x20\x32\x35\x2e\x36\x34\x35\x36\x20\x34\ -\x33\x2e\x35\x34\x35\x39\x4c\x31\x33\x2e\x33\x38\x38\x32\x20\x33\ -\x33\x2e\x33\x30\x34\x37\x43\x31\x32\x2e\x30\x30\x34\x37\x20\x33\ -\x32\x2e\x31\x34\x38\x39\x20\x31\x32\x2e\x37\x33\x36\x35\x20\x32\ -\x39\x2e\x38\x39\x36\x35\x20\x31\x34\x2e\x35\x33\x35\x32\x20\x32\ -\x39\x2e\x37\x37\x34\x35\x4c\x33\x30\x2e\x34\x37\x31\x32\x20\x32\ -\x38\x2e\x36\x39\x34\x43\x33\x31\x2e\x32\x33\x37\x32\x20\x32\x38\ -\x2e\x36\x34\x32\x20\x33\x31\x2e\x39\x30\x35\x38\x20\x32\x38\x2e\ -\x31\x35\x36\x33\x20\x33\x32\x2e\x31\x39\x31\x39\x20\x32\x37\x2e\ -\x34\x34\x33\x39\x4c\x33\x38\x2e\x31\x34\x34\x31\x20\x31\x32\x2e\ -\x36\x32\x31\x37\x5a\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\x46\x32\ -\x39\x39\x34\x41\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\ -\x20\x64\x3d\x22\x4d\x33\x39\x2e\x30\x35\x31\x35\x20\x32\x36\x2e\ -\x33\x31\x30\x38\x43\x33\x39\x2e\x33\x38\x37\x34\x20\x32\x35\x2e\ -\x34\x37\x34\x34\x20\x34\x30\x2e\x35\x37\x31\x36\x20\x32\x35\x2e\ -\x34\x37\x34\x34\x20\x34\x30\x2e\x39\x30\x37\x35\x20\x32\x36\x2e\ -\x33\x31\x30\x38\x4c\x34\x33\x2e\x38\x38\x33\x35\x20\x33\x33\x2e\ -\x37\x32\x31\x39\x43\x34\x34\x2e\x30\x32\x36\x36\x20\x33\x34\x2e\ -\x30\x37\x38\x31\x20\x34\x34\x2e\x33\x36\x30\x39\x20\x33\x34\x2e\ -\x33\x32\x31\x20\x34\x34\x2e\x37\x34\x33\x39\x20\x33\x34\x2e\x33\ -\x34\x37\x4c\x35\x32\x2e\x37\x31\x31\x39\x20\x33\x34\x2e\x38\x38\ -\x37\x33\x43\x35\x33\x2e\x36\x31\x31\x32\x20\x33\x34\x2e\x39\x34\ -\x38\x32\x20\x35\x33\x2e\x39\x37\x37\x31\x20\x33\x36\x2e\x30\x37\ -\x34\x34\x20\x35\x33\x2e\x32\x38\x35\x34\x20\x33\x36\x2e\x36\x35\ -\x32\x34\x4c\x34\x37\x2e\x31\x35\x36\x37\x20\x34\x31\x2e\x37\x37\ -\x32\x39\x43\x34\x36\x2e\x38\x36\x32\x31\x20\x34\x32\x2e\x30\x31\ -\x39\x31\x20\x34\x36\x2e\x37\x33\x34\x34\x20\x34\x32\x2e\x34\x31\ -\x32\x31\x20\x34\x36\x2e\x38\x32\x38\x31\x20\x34\x32\x2e\x37\x38\ -\x34\x33\x4c\x34\x38\x2e\x37\x37\x36\x35\x20\x35\x30\x2e\x35\x32\ -\x39\x33\x43\x34\x38\x2e\x39\x39\x36\x34\x20\x35\x31\x2e\x34\x30\ -\x33\x35\x20\x34\x38\x2e\x30\x33\x38\x34\x20\x35\x32\x2e\x30\x39\ -\x39\x35\x20\x34\x37\x2e\x32\x37\x35\x20\x35\x31\x2e\x36\x32\x30\ -\x32\x4c\x34\x30\x2e\x35\x31\x31\x32\x20\x34\x37\x2e\x33\x37\x33\ -\x38\x43\x34\x30\x2e\x31\x38\x36\x31\x20\x34\x37\x2e\x31\x36\x39\ -\x37\x20\x33\x39\x2e\x37\x37\x32\x39\x20\x34\x37\x2e\x31\x36\x39\ -\x37\x20\x33\x39\x2e\x34\x34\x37\x38\x20\x34\x37\x2e\x33\x37\x33\ -\x38\x4c\x33\x32\x2e\x36\x38\x34\x20\x35\x31\x2e\x36\x32\x30\x32\ -\x43\x33\x31\x2e\x39\x32\x30\x35\x20\x35\x32\x2e\x30\x39\x39\x35\ -\x20\x33\x30\x2e\x39\x36\x32\x36\x20\x35\x31\x2e\x34\x30\x33\x35\ -\x20\x33\x31\x2e\x31\x38\x32\x35\x20\x35\x30\x2e\x35\x32\x39\x33\ -\x4c\x33\x33\x2e\x31\x33\x30\x39\x20\x34\x32\x2e\x37\x38\x34\x33\ -\x43\x33\x33\x2e\x32\x32\x34\x35\x20\x34\x32\x2e\x34\x31\x32\x31\ -\x20\x33\x33\x2e\x30\x39\x36\x38\x20\x34\x32\x2e\x30\x31\x39\x31\ -\x20\x33\x32\x2e\x38\x30\x32\x33\x20\x34\x31\x2e\x37\x37\x32\x39\ -\x4c\x32\x36\x2e\x36\x37\x33\x36\x20\x33\x36\x2e\x36\x35\x32\x34\ -\x43\x32\x35\x2e\x39\x38\x31\x38\x20\x33\x36\x2e\x30\x37\x34\x34\ -\x20\x32\x36\x2e\x33\x34\x37\x38\x20\x33\x34\x2e\x39\x34\x38\x32\ -\x20\x32\x37\x2e\x32\x34\x37\x31\x20\x33\x34\x2e\x38\x38\x37\x33\ -\x4c\x33\x35\x2e\x32\x31\x35\x31\x20\x33\x34\x2e\x33\x34\x37\x43\ -\x33\x35\x2e\x35\x39\x38\x31\x20\x33\x34\x2e\x33\x32\x31\x20\x33\ -\x35\x2e\x39\x33\x32\x34\x20\x33\x34\x2e\x30\x37\x38\x31\x20\x33\ -\x36\x2e\x30\x37\x35\x34\x20\x33\x33\x2e\x37\x32\x31\x39\x4c\x33\ -\x39\x2e\x30\x35\x31\x35\x20\x32\x36\x2e\x33\x31\x30\x38\x5a\x22\ -\x20\x66\x69\x6c\x6c\x3d\x22\x23\x46\x32\x43\x39\x34\x43\x22\x20\ -\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x04\xc2\ +\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x3e\x0d\x0a\ +\x20\x20\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x33\x38\x2e\x31\ +\x34\x34\x31\x20\x31\x32\x2e\x36\x32\x31\x37\x43\x33\x38\x2e\x38\ +\x31\x35\x39\x20\x31\x30\x2e\x39\x34\x38\x38\x20\x34\x31\x2e\x31\ +\x38\x34\x31\x20\x31\x30\x2e\x39\x34\x38\x38\x20\x34\x31\x2e\x38\ +\x35\x35\x39\x20\x31\x32\x2e\x36\x32\x31\x37\x4c\x34\x37\x2e\x38\ +\x30\x38\x31\x20\x32\x37\x2e\x34\x34\x33\x39\x43\x34\x38\x2e\x30\ +\x39\x34\x32\x20\x32\x38\x2e\x31\x35\x36\x33\x20\x34\x38\x2e\x37\ +\x36\x32\x38\x20\x32\x38\x2e\x36\x34\x32\x20\x34\x39\x2e\x35\x32\ +\x38\x38\x20\x32\x38\x2e\x36\x39\x34\x4c\x36\x35\x2e\x34\x36\x34\ +\x38\x20\x32\x39\x2e\x37\x37\x34\x35\x43\x36\x37\x2e\x32\x36\x33\ +\x35\x20\x32\x39\x2e\x38\x39\x36\x35\x20\x36\x37\x2e\x39\x39\x35\ +\x33\x20\x33\x32\x2e\x31\x34\x38\x39\x20\x36\x36\x2e\x36\x31\x31\ +\x38\x20\x33\x33\x2e\x33\x30\x34\x37\x4c\x35\x34\x2e\x33\x35\x34\ +\x34\x20\x34\x33\x2e\x35\x34\x35\x39\x43\x35\x33\x2e\x37\x36\x35\ +\x33\x20\x34\x34\x2e\x30\x33\x38\x31\x20\x35\x33\x2e\x35\x30\x39\ +\x39\x20\x34\x34\x2e\x38\x32\x34\x31\x20\x35\x33\x2e\x36\x39\x37\ +\x32\x20\x34\x35\x2e\x35\x36\x38\x36\x4c\x35\x37\x2e\x35\x39\x34\ +\x31\x20\x36\x31\x2e\x30\x35\x38\x36\x43\x35\x38\x2e\x30\x33\x33\ +\x39\x20\x36\x32\x2e\x38\x30\x36\x39\x20\x35\x36\x2e\x31\x31\x37\ +\x39\x20\x36\x34\x2e\x31\x39\x39\x20\x35\x34\x2e\x35\x39\x31\x31\ +\x20\x36\x33\x2e\x32\x34\x30\x34\x4c\x34\x31\x2e\x30\x36\x33\x34\ +\x20\x35\x34\x2e\x37\x34\x37\x36\x43\x34\x30\x2e\x34\x31\x33\x32\ +\x20\x35\x34\x2e\x33\x33\x39\x34\x20\x33\x39\x2e\x35\x38\x36\x38\ +\x20\x35\x34\x2e\x33\x33\x39\x34\x20\x33\x38\x2e\x39\x33\x36\x36\ +\x20\x35\x34\x2e\x37\x34\x37\x36\x4c\x32\x35\x2e\x34\x30\x38\x39\ +\x20\x36\x33\x2e\x32\x34\x30\x34\x43\x32\x33\x2e\x38\x38\x32\x31\ +\x20\x36\x34\x2e\x31\x39\x39\x20\x32\x31\x2e\x39\x36\x36\x31\x20\ +\x36\x32\x2e\x38\x30\x36\x39\x20\x32\x32\x2e\x34\x30\x35\x39\x20\ +\x36\x31\x2e\x30\x35\x38\x36\x4c\x32\x36\x2e\x33\x30\x32\x38\x20\ +\x34\x35\x2e\x35\x36\x38\x36\x43\x32\x36\x2e\x34\x39\x30\x31\x20\ +\x34\x34\x2e\x38\x32\x34\x31\x20\x32\x36\x2e\x32\x33\x34\x37\x20\ +\x34\x34\x2e\x30\x33\x38\x31\x20\x32\x35\x2e\x36\x34\x35\x36\x20\ +\x34\x33\x2e\x35\x34\x35\x39\x4c\x31\x33\x2e\x33\x38\x38\x32\x20\ +\x33\x33\x2e\x33\x30\x34\x37\x43\x31\x32\x2e\x30\x30\x34\x37\x20\ +\x33\x32\x2e\x31\x34\x38\x39\x20\x31\x32\x2e\x37\x33\x36\x35\x20\ +\x32\x39\x2e\x38\x39\x36\x35\x20\x31\x34\x2e\x35\x33\x35\x32\x20\ +\x32\x39\x2e\x37\x37\x34\x35\x4c\x33\x30\x2e\x34\x37\x31\x32\x20\ +\x32\x38\x2e\x36\x39\x34\x43\x33\x31\x2e\x32\x33\x37\x32\x20\x32\ +\x38\x2e\x36\x34\x32\x20\x33\x31\x2e\x39\x30\x35\x38\x20\x32\x38\ +\x2e\x31\x35\x36\x33\x20\x33\x32\x2e\x31\x39\x31\x39\x20\x32\x37\ +\x2e\x34\x34\x33\x39\x4c\x33\x38\x2e\x31\x34\x34\x31\x20\x31\x32\ +\x2e\x36\x32\x31\x37\x5a\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\x46\ +\x32\x39\x39\x34\x41\x22\x20\x2f\x3e\x0d\x0a\x20\x20\x3c\x70\x61\ +\x74\x68\x20\x64\x3d\x22\x4d\x33\x39\x2e\x30\x35\x31\x35\x20\x32\ +\x36\x2e\x33\x31\x30\x38\x43\x33\x39\x2e\x33\x38\x37\x34\x20\x32\ +\x35\x2e\x34\x37\x34\x34\x20\x34\x30\x2e\x35\x37\x31\x36\x20\x32\ +\x35\x2e\x34\x37\x34\x34\x20\x34\x30\x2e\x39\x30\x37\x35\x20\x32\ +\x36\x2e\x33\x31\x30\x38\x4c\x34\x33\x2e\x38\x38\x33\x35\x20\x33\ +\x33\x2e\x37\x32\x31\x39\x43\x34\x34\x2e\x30\x32\x36\x36\x20\x33\ +\x34\x2e\x30\x37\x38\x31\x20\x34\x34\x2e\x33\x36\x30\x39\x20\x33\ +\x34\x2e\x33\x32\x31\x20\x34\x34\x2e\x37\x34\x33\x39\x20\x33\x34\ +\x2e\x33\x34\x37\x4c\x35\x32\x2e\x37\x31\x31\x39\x20\x33\x34\x2e\ +\x38\x38\x37\x33\x43\x35\x33\x2e\x36\x31\x31\x32\x20\x33\x34\x2e\ +\x39\x34\x38\x32\x20\x35\x33\x2e\x39\x37\x37\x31\x20\x33\x36\x2e\ +\x30\x37\x34\x34\x20\x35\x33\x2e\x32\x38\x35\x34\x20\x33\x36\x2e\ +\x36\x35\x32\x34\x4c\x34\x37\x2e\x31\x35\x36\x37\x20\x34\x31\x2e\ +\x37\x37\x32\x39\x43\x34\x36\x2e\x38\x36\x32\x31\x20\x34\x32\x2e\ +\x30\x31\x39\x31\x20\x34\x36\x2e\x37\x33\x34\x34\x20\x34\x32\x2e\ +\x34\x31\x32\x31\x20\x34\x36\x2e\x38\x32\x38\x31\x20\x34\x32\x2e\ +\x37\x38\x34\x33\x4c\x34\x38\x2e\x37\x37\x36\x35\x20\x35\x30\x2e\ +\x35\x32\x39\x33\x43\x34\x38\x2e\x39\x39\x36\x34\x20\x35\x31\x2e\ +\x34\x30\x33\x35\x20\x34\x38\x2e\x30\x33\x38\x34\x20\x35\x32\x2e\ +\x30\x39\x39\x35\x20\x34\x37\x2e\x32\x37\x35\x20\x35\x31\x2e\x36\ +\x32\x30\x32\x4c\x34\x30\x2e\x35\x31\x31\x32\x20\x34\x37\x2e\x33\ +\x37\x33\x38\x43\x34\x30\x2e\x31\x38\x36\x31\x20\x34\x37\x2e\x31\ +\x36\x39\x37\x20\x33\x39\x2e\x37\x37\x32\x39\x20\x34\x37\x2e\x31\ +\x36\x39\x37\x20\x33\x39\x2e\x34\x34\x37\x38\x20\x34\x37\x2e\x33\ +\x37\x33\x38\x4c\x33\x32\x2e\x36\x38\x34\x20\x35\x31\x2e\x36\x32\ +\x30\x32\x43\x33\x31\x2e\x39\x32\x30\x35\x20\x35\x32\x2e\x30\x39\ +\x39\x35\x20\x33\x30\x2e\x39\x36\x32\x36\x20\x35\x31\x2e\x34\x30\ +\x33\x35\x20\x33\x31\x2e\x31\x38\x32\x35\x20\x35\x30\x2e\x35\x32\ +\x39\x33\x4c\x33\x33\x2e\x31\x33\x30\x39\x20\x34\x32\x2e\x37\x38\ +\x34\x33\x43\x33\x33\x2e\x32\x32\x34\x35\x20\x34\x32\x2e\x34\x31\ +\x32\x31\x20\x33\x33\x2e\x30\x39\x36\x38\x20\x34\x32\x2e\x30\x31\ +\x39\x31\x20\x33\x32\x2e\x38\x30\x32\x33\x20\x34\x31\x2e\x37\x37\ +\x32\x39\x4c\x32\x36\x2e\x36\x37\x33\x36\x20\x33\x36\x2e\x36\x35\ +\x32\x34\x43\x32\x35\x2e\x39\x38\x31\x38\x20\x33\x36\x2e\x30\x37\ +\x34\x34\x20\x32\x36\x2e\x33\x34\x37\x38\x20\x33\x34\x2e\x39\x34\ +\x38\x32\x20\x32\x37\x2e\x32\x34\x37\x31\x20\x33\x34\x2e\x38\x38\ +\x37\x33\x4c\x33\x35\x2e\x32\x31\x35\x31\x20\x33\x34\x2e\x33\x34\ +\x37\x43\x33\x35\x2e\x35\x39\x38\x31\x20\x33\x34\x2e\x33\x32\x31\ +\x20\x33\x35\x2e\x39\x33\x32\x34\x20\x33\x34\x2e\x30\x37\x38\x31\ +\x20\x33\x36\x2e\x30\x37\x35\x34\x20\x33\x33\x2e\x37\x32\x31\x39\ +\x4c\x33\x39\x2e\x30\x35\x31\x35\x20\x32\x36\x2e\x33\x31\x30\x38\ +\x5a\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\x46\x32\x43\x39\x34\x43\ +\x22\x20\x2f\x3e\x0d\x0a\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x04\xc5\ \x3c\ \x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ \x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x75\x74\x66\ @@ -516,355 +557,365 @@ \x20\x77\x77\x77\x2e\x73\x76\x67\x72\x65\x70\x6f\x2e\x63\x6f\x6d\ \x2c\x20\x47\x65\x6e\x65\x72\x61\x74\x6f\x72\x3a\x20\x53\x56\x47\ \x20\x52\x65\x70\x6f\x20\x4d\x69\x78\x65\x72\x20\x54\x6f\x6f\x6c\ -\x73\x20\x2d\x2d\x3e\x0a\x3c\x73\x76\x67\x20\x77\x69\x64\x74\x68\ -\x3d\x22\x38\x30\x30\x70\x78\x22\x20\x68\x65\x69\x67\x68\x74\x3d\ -\x22\x38\x30\x30\x70\x78\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ -\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\x22\x20\x66\x69\x6c\x6c\ -\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\ -\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\ -\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x3e\x0a\x3c\x70\x61\x74\ -\x68\x20\x64\x3d\x22\x4d\x32\x30\x20\x39\x2e\x35\x30\x31\x39\x35\ -\x56\x38\x2e\x37\x34\x39\x38\x35\x43\x32\x30\x20\x37\x2e\x35\x30\ -\x37\x32\x31\x20\x31\x38\x2e\x39\x39\x32\x36\x20\x36\x2e\x34\x39\ -\x39\x38\x35\x20\x31\x37\x2e\x37\x35\x20\x36\x2e\x34\x39\x39\x38\ -\x35\x48\x31\x32\x2e\x30\x32\x34\x37\x4c\x39\x2e\x36\x34\x33\x36\ -\x38\x20\x34\x2e\x35\x31\x39\x39\x35\x43\x39\x2e\x32\x33\x39\x35\ -\x39\x20\x34\x2e\x31\x38\x33\x39\x33\x20\x38\x2e\x37\x33\x30\x36\ -\x33\x20\x33\x2e\x39\x39\x39\x39\x37\x20\x38\x2e\x32\x30\x35\x30\ -\x39\x20\x33\x2e\x39\x39\x39\x39\x37\x48\x34\x2e\x32\x34\x39\x35\ -\x37\x43\x33\x2e\x30\x30\x37\x32\x34\x20\x33\x2e\x39\x39\x39\x39\ -\x37\x20\x32\x20\x35\x2e\x30\x30\x36\x38\x36\x20\x31\x2e\x39\x39\ -\x39\x35\x37\x20\x36\x2e\x32\x34\x39\x31\x39\x4c\x31\x2e\x39\x39\ -\x35\x36\x31\x20\x31\x37\x2e\x37\x34\x39\x32\x43\x31\x2e\x39\x39\ -\x35\x31\x38\x20\x31\x38\x2e\x39\x39\x32\x31\x20\x33\x2e\x30\x30\ -\x32\x36\x36\x20\x32\x30\x20\x34\x2e\x32\x34\x35\x36\x31\x20\x32\ -\x30\x48\x34\x2e\x32\x37\x31\x39\x36\x43\x34\x2e\x32\x37\x36\x30\ -\x37\x20\x32\x30\x20\x34\x2e\x32\x38\x30\x31\x39\x20\x32\x30\x20\ -\x34\x2e\x32\x38\x34\x33\x31\x20\x32\x30\x48\x31\x38\x2e\x34\x36\ -\x39\x33\x43\x31\x39\x2e\x32\x37\x32\x33\x20\x32\x30\x20\x31\x39\ -\x2e\x39\x37\x32\x33\x20\x31\x39\x2e\x34\x35\x33\x35\x20\x32\x30\ -\x2e\x31\x36\x37\x20\x31\x38\x2e\x36\x37\x34\x35\x4c\x32\x31\x2e\ -\x39\x31\x36\x39\x20\x31\x31\x2e\x36\x37\x36\x35\x43\x32\x32\x2e\ -\x31\x39\x33\x31\x20\x31\x30\x2e\x35\x37\x31\x39\x20\x32\x31\x2e\ -\x33\x35\x37\x37\x20\x39\x2e\x35\x30\x31\x39\x35\x20\x32\x30\x2e\ -\x32\x31\x39\x32\x20\x39\x2e\x35\x30\x31\x39\x35\x48\x32\x30\x5a\ -\x4d\x34\x2e\x32\x34\x39\x35\x37\x20\x35\x2e\x34\x39\x39\x39\x37\ -\x48\x38\x2e\x32\x30\x35\x30\x39\x43\x38\x2e\x33\x38\x30\x32\x37\ -\x20\x35\x2e\x34\x39\x39\x39\x37\x20\x38\x2e\x35\x34\x39\x39\x33\ -\x20\x35\x2e\x35\x36\x31\x32\x39\x20\x38\x2e\x36\x38\x34\x36\x32\ -\x20\x35\x2e\x36\x37\x33\x33\x4c\x31\x31\x2e\x32\x37\x34\x31\x20\ -\x37\x2e\x38\x32\x36\x35\x32\x43\x31\x31\x2e\x34\x30\x38\x38\x20\ -\x37\x2e\x39\x33\x38\x35\x32\x20\x31\x31\x2e\x35\x37\x38\x34\x20\ -\x37\x2e\x39\x39\x39\x38\x35\x20\x31\x31\x2e\x37\x35\x33\x36\x20\ -\x37\x2e\x39\x39\x39\x38\x35\x48\x31\x37\x2e\x37\x35\x43\x31\x38\ -\x2e\x31\x36\x34\x32\x20\x37\x2e\x39\x39\x39\x38\x35\x20\x31\x38\ -\x2e\x35\x20\x38\x2e\x33\x33\x35\x36\x33\x20\x31\x38\x2e\x35\x20\ -\x38\x2e\x37\x34\x39\x38\x35\x56\x39\x2e\x35\x30\x31\x39\x35\x48\ -\x36\x2e\x34\x32\x33\x38\x35\x43\x35\x2e\x33\x39\x31\x33\x36\x20\ -\x39\x2e\x35\x30\x31\x39\x35\x20\x34\x2e\x34\x39\x31\x33\x37\x20\ -\x31\x30\x2e\x32\x30\x34\x37\x20\x34\x2e\x32\x34\x31\x20\x31\x31\ -\x2e\x32\x30\x36\x34\x4c\x33\x2e\x34\x39\x36\x38\x34\x20\x31\x34\ -\x2e\x31\x38\x33\x37\x4c\x33\x2e\x34\x39\x39\x35\x37\x20\x36\x2e\ -\x32\x34\x39\x37\x31\x43\x33\x2e\x34\x39\x39\x37\x31\x20\x35\x2e\ -\x38\x33\x35\x36\x20\x33\x2e\x38\x33\x35\x34\x36\x20\x35\x2e\x34\ -\x39\x39\x39\x37\x20\x34\x2e\x32\x34\x39\x35\x37\x20\x35\x2e\x34\ -\x39\x39\x39\x37\x5a\x4d\x35\x2e\x36\x39\x36\x32\x33\x20\x31\x31\ -\x2e\x35\x37\x30\x31\x43\x35\x2e\x37\x37\x39\x36\x39\x20\x31\x31\ -\x2e\x32\x33\x36\x32\x20\x36\x2e\x30\x37\x39\x36\x39\x20\x31\x31\ -\x2e\x30\x30\x32\x20\x36\x2e\x34\x32\x33\x38\x35\x20\x31\x31\x2e\ -\x30\x30\x32\x48\x32\x30\x2e\x32\x31\x39\x32\x43\x32\x30\x2e\x33\ -\x38\x31\x39\x20\x31\x31\x2e\x30\x30\x32\x20\x32\x30\x2e\x35\x30\ -\x31\x32\x20\x31\x31\x2e\x31\x35\x34\x38\x20\x32\x30\x2e\x34\x36\ -\x31\x37\x20\x31\x31\x2e\x33\x31\x32\x36\x4c\x31\x38\x2e\x37\x31\ -\x31\x39\x20\x31\x38\x2e\x33\x31\x30\x37\x43\x31\x38\x2e\x36\x38\ -\x34\x20\x31\x38\x2e\x34\x32\x31\x39\x20\x31\x38\x2e\x35\x38\x34\ -\x20\x31\x38\x2e\x35\x20\x31\x38\x2e\x34\x36\x39\x33\x20\x31\x38\ -\x2e\x35\x48\x34\x2e\x32\x38\x34\x33\x31\x43\x34\x2e\x31\x32\x31\ -\x36\x37\x20\x31\x38\x2e\x35\x20\x34\x2e\x30\x30\x32\x33\x33\x20\ -\x31\x38\x2e\x33\x34\x37\x32\x20\x34\x2e\x30\x34\x31\x37\x37\x20\ -\x31\x38\x2e\x31\x38\x39\x34\x4c\x35\x2e\x36\x39\x36\x32\x33\x20\ -\x31\x31\x2e\x35\x37\x30\x31\x5a\x22\x20\x66\x69\x6c\x6c\x3d\x22\ -\x23\x32\x31\x32\x31\x32\x31\x22\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\ -\x3e\ -\x00\x00\x09\x8f\ +\x73\x20\x2d\x2d\x3e\x0d\x0a\x3c\x73\x76\x67\x20\x77\x69\x64\x74\ +\x68\x3d\x22\x38\x30\x30\x70\x78\x22\x20\x68\x65\x69\x67\x68\x74\ +\x3d\x22\x38\x30\x30\x70\x78\x22\x20\x76\x69\x65\x77\x42\x6f\x78\ +\x3d\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\x22\x20\x66\x69\x6c\ +\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\ +\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\ +\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x3e\x0d\x0a\x3c\x70\ +\x61\x74\x68\x20\x64\x3d\x22\x4d\x32\x30\x20\x39\x2e\x35\x30\x31\ +\x39\x35\x56\x38\x2e\x37\x34\x39\x38\x35\x43\x32\x30\x20\x37\x2e\ +\x35\x30\x37\x32\x31\x20\x31\x38\x2e\x39\x39\x32\x36\x20\x36\x2e\ +\x34\x39\x39\x38\x35\x20\x31\x37\x2e\x37\x35\x20\x36\x2e\x34\x39\ +\x39\x38\x35\x48\x31\x32\x2e\x30\x32\x34\x37\x4c\x39\x2e\x36\x34\ +\x33\x36\x38\x20\x34\x2e\x35\x31\x39\x39\x35\x43\x39\x2e\x32\x33\ +\x39\x35\x39\x20\x34\x2e\x31\x38\x33\x39\x33\x20\x38\x2e\x37\x33\ +\x30\x36\x33\x20\x33\x2e\x39\x39\x39\x39\x37\x20\x38\x2e\x32\x30\ +\x35\x30\x39\x20\x33\x2e\x39\x39\x39\x39\x37\x48\x34\x2e\x32\x34\ +\x39\x35\x37\x43\x33\x2e\x30\x30\x37\x32\x34\x20\x33\x2e\x39\x39\ +\x39\x39\x37\x20\x32\x20\x35\x2e\x30\x30\x36\x38\x36\x20\x31\x2e\ +\x39\x39\x39\x35\x37\x20\x36\x2e\x32\x34\x39\x31\x39\x4c\x31\x2e\ +\x39\x39\x35\x36\x31\x20\x31\x37\x2e\x37\x34\x39\x32\x43\x31\x2e\ +\x39\x39\x35\x31\x38\x20\x31\x38\x2e\x39\x39\x32\x31\x20\x33\x2e\ +\x30\x30\x32\x36\x36\x20\x32\x30\x20\x34\x2e\x32\x34\x35\x36\x31\ +\x20\x32\x30\x48\x34\x2e\x32\x37\x31\x39\x36\x43\x34\x2e\x32\x37\ +\x36\x30\x37\x20\x32\x30\x20\x34\x2e\x32\x38\x30\x31\x39\x20\x32\ +\x30\x20\x34\x2e\x32\x38\x34\x33\x31\x20\x32\x30\x48\x31\x38\x2e\ +\x34\x36\x39\x33\x43\x31\x39\x2e\x32\x37\x32\x33\x20\x32\x30\x20\ +\x31\x39\x2e\x39\x37\x32\x33\x20\x31\x39\x2e\x34\x35\x33\x35\x20\ +\x32\x30\x2e\x31\x36\x37\x20\x31\x38\x2e\x36\x37\x34\x35\x4c\x32\ +\x31\x2e\x39\x31\x36\x39\x20\x31\x31\x2e\x36\x37\x36\x35\x43\x32\ +\x32\x2e\x31\x39\x33\x31\x20\x31\x30\x2e\x35\x37\x31\x39\x20\x32\ +\x31\x2e\x33\x35\x37\x37\x20\x39\x2e\x35\x30\x31\x39\x35\x20\x32\ +\x30\x2e\x32\x31\x39\x32\x20\x39\x2e\x35\x30\x31\x39\x35\x48\x32\ +\x30\x5a\x4d\x34\x2e\x32\x34\x39\x35\x37\x20\x35\x2e\x34\x39\x39\ +\x39\x37\x48\x38\x2e\x32\x30\x35\x30\x39\x43\x38\x2e\x33\x38\x30\ +\x32\x37\x20\x35\x2e\x34\x39\x39\x39\x37\x20\x38\x2e\x35\x34\x39\ +\x39\x33\x20\x35\x2e\x35\x36\x31\x32\x39\x20\x38\x2e\x36\x38\x34\ +\x36\x32\x20\x35\x2e\x36\x37\x33\x33\x4c\x31\x31\x2e\x32\x37\x34\ +\x31\x20\x37\x2e\x38\x32\x36\x35\x32\x43\x31\x31\x2e\x34\x30\x38\ +\x38\x20\x37\x2e\x39\x33\x38\x35\x32\x20\x31\x31\x2e\x35\x37\x38\ +\x34\x20\x37\x2e\x39\x39\x39\x38\x35\x20\x31\x31\x2e\x37\x35\x33\ +\x36\x20\x37\x2e\x39\x39\x39\x38\x35\x48\x31\x37\x2e\x37\x35\x43\ +\x31\x38\x2e\x31\x36\x34\x32\x20\x37\x2e\x39\x39\x39\x38\x35\x20\ +\x31\x38\x2e\x35\x20\x38\x2e\x33\x33\x35\x36\x33\x20\x31\x38\x2e\ +\x35\x20\x38\x2e\x37\x34\x39\x38\x35\x56\x39\x2e\x35\x30\x31\x39\ +\x35\x48\x36\x2e\x34\x32\x33\x38\x35\x43\x35\x2e\x33\x39\x31\x33\ +\x36\x20\x39\x2e\x35\x30\x31\x39\x35\x20\x34\x2e\x34\x39\x31\x33\ +\x37\x20\x31\x30\x2e\x32\x30\x34\x37\x20\x34\x2e\x32\x34\x31\x20\ +\x31\x31\x2e\x32\x30\x36\x34\x4c\x33\x2e\x34\x39\x36\x38\x34\x20\ +\x31\x34\x2e\x31\x38\x33\x37\x4c\x33\x2e\x34\x39\x39\x35\x37\x20\ +\x36\x2e\x32\x34\x39\x37\x31\x43\x33\x2e\x34\x39\x39\x37\x31\x20\ +\x35\x2e\x38\x33\x35\x36\x20\x33\x2e\x38\x33\x35\x34\x36\x20\x35\ +\x2e\x34\x39\x39\x39\x37\x20\x34\x2e\x32\x34\x39\x35\x37\x20\x35\ +\x2e\x34\x39\x39\x39\x37\x5a\x4d\x35\x2e\x36\x39\x36\x32\x33\x20\ +\x31\x31\x2e\x35\x37\x30\x31\x43\x35\x2e\x37\x37\x39\x36\x39\x20\ +\x31\x31\x2e\x32\x33\x36\x32\x20\x36\x2e\x30\x37\x39\x36\x39\x20\ +\x31\x31\x2e\x30\x30\x32\x20\x36\x2e\x34\x32\x33\x38\x35\x20\x31\ +\x31\x2e\x30\x30\x32\x48\x32\x30\x2e\x32\x31\x39\x32\x43\x32\x30\ +\x2e\x33\x38\x31\x39\x20\x31\x31\x2e\x30\x30\x32\x20\x32\x30\x2e\ +\x35\x30\x31\x32\x20\x31\x31\x2e\x31\x35\x34\x38\x20\x32\x30\x2e\ +\x34\x36\x31\x37\x20\x31\x31\x2e\x33\x31\x32\x36\x4c\x31\x38\x2e\ +\x37\x31\x31\x39\x20\x31\x38\x2e\x33\x31\x30\x37\x43\x31\x38\x2e\ +\x36\x38\x34\x20\x31\x38\x2e\x34\x32\x31\x39\x20\x31\x38\x2e\x35\ +\x38\x34\x20\x31\x38\x2e\x35\x20\x31\x38\x2e\x34\x36\x39\x33\x20\ +\x31\x38\x2e\x35\x48\x34\x2e\x32\x38\x34\x33\x31\x43\x34\x2e\x31\ +\x32\x31\x36\x37\x20\x31\x38\x2e\x35\x20\x34\x2e\x30\x30\x32\x33\ +\x33\x20\x31\x38\x2e\x33\x34\x37\x32\x20\x34\x2e\x30\x34\x31\x37\ +\x37\x20\x31\x38\x2e\x31\x38\x39\x34\x4c\x35\x2e\x36\x39\x36\x32\ +\x33\x20\x31\x31\x2e\x35\x37\x30\x31\x5a\x22\x20\x66\x69\x6c\x6c\ +\x3d\x22\x23\x32\x31\x32\x31\x32\x31\x22\x2f\x3e\x0d\x0a\x3c\x2f\ +\x73\x76\x67\x3e\ +\x00\x00\x09\xd7\ \x3c\ \x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ \x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ \x2d\x38\x22\x20\x73\x74\x61\x6e\x64\x61\x6c\x6f\x6e\x65\x3d\x22\ -\x6e\x6f\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x0a\x20\x20\x20\x78\x6d\ -\x6c\x6e\x73\x3a\x64\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\ -\x75\x72\x6c\x2e\x6f\x72\x67\x2f\x64\x63\x2f\x65\x6c\x65\x6d\x65\ -\x6e\x74\x73\x2f\x31\x2e\x31\x2f\x22\x0a\x20\x20\x20\x78\x6d\x6c\ -\x6e\x73\x3a\x63\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x63\x72\ -\x65\x61\x74\x69\x76\x65\x63\x6f\x6d\x6d\x6f\x6e\x73\x2e\x6f\x72\ -\x67\x2f\x6e\x73\x23\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\ -\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\ -\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\x2f\x32\ -\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\x73\x23\ -\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x76\x67\x3d\x22\ -\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\ -\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x0a\x20\x20\x20\x78\ -\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\ -\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\ -\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x6f\x64\x69\x70\ -\x6f\x64\x69\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x73\x6f\x64\x69\ -\x70\x6f\x64\x69\x2e\x73\x6f\x75\x72\x63\x65\x66\x6f\x72\x67\x65\ -\x2e\x6e\x65\x74\x2f\x44\x54\x44\x2f\x73\x6f\x64\x69\x70\x6f\x64\ -\x69\x2d\x30\x2e\x64\x74\x64\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\ -\x73\x3a\x69\x6e\x6b\x73\x63\x61\x70\x65\x3d\x22\x68\x74\x74\x70\ -\x3a\x2f\x2f\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\x2e\ -\x6f\x72\x67\x2f\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x73\x2f\x69\ -\x6e\x6b\x73\x63\x61\x70\x65\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\ -\x63\x61\x70\x65\x3a\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ -\x30\x72\x63\x31\x20\x28\x30\x39\x39\x36\x30\x64\x36\x66\x30\x35\ -\x2c\x20\x32\x30\x32\x30\x2d\x30\x34\x2d\x30\x39\x29\x22\x0a\x20\ -\x20\x20\x73\x6f\x64\x69\x70\x6f\x64\x69\x3a\x64\x6f\x63\x6e\x61\ -\x6d\x65\x3d\x22\x63\x6f\x6c\x6c\x61\x70\x73\x65\x2e\x73\x76\x67\ -\x22\x0a\x20\x20\x20\x69\x64\x3d\x22\x73\x76\x67\x36\x22\x0a\x20\ -\x20\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x31\x22\x0a\ -\x20\x20\x20\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\x65\ -\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\x63\x6f\x64\x65\x22\x0a\ -\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\ -\x69\x6e\x3d\x22\x72\x6f\x75\x6e\x64\x22\x0a\x20\x20\x20\x73\x74\ -\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3d\x22\x72\x6f\ -\x75\x6e\x64\x22\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\ -\x69\x64\x74\x68\x3d\x22\x32\x22\x0a\x20\x20\x20\x73\x74\x72\x6f\ -\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\x6f\x72\ -\x22\x0a\x20\x20\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\ -\x0a\x20\x20\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\ -\x20\x32\x34\x20\x32\x34\x22\x0a\x20\x20\x20\x68\x65\x69\x67\x68\ -\x74\x3d\x22\x32\x34\x22\x0a\x20\x20\x20\x77\x69\x64\x74\x68\x3d\ -\x22\x32\x34\x22\x3e\x0a\x20\x20\x3c\x6d\x65\x74\x61\x64\x61\x74\ -\x61\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x6d\x65\x74\x61\x64\ -\x61\x74\x61\x31\x32\x22\x3e\x0a\x20\x20\x20\x20\x3c\x72\x64\x66\ -\x3a\x52\x44\x46\x3e\x0a\x20\x20\x20\x20\x20\x20\x3c\x63\x63\x3a\ -\x57\x6f\x72\x6b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x72\x64\ -\x66\x3a\x61\x62\x6f\x75\x74\x3d\x22\x22\x3e\x0a\x20\x20\x20\x20\ -\x20\x20\x20\x20\x3c\x64\x63\x3a\x66\x6f\x72\x6d\x61\x74\x3e\x69\ -\x6d\x61\x67\x65\x2f\x73\x76\x67\x2b\x78\x6d\x6c\x3c\x2f\x64\x63\ -\x3a\x66\x6f\x72\x6d\x61\x74\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\ -\x20\x3c\x64\x63\x3a\x74\x79\x70\x65\x0a\x20\x20\x20\x20\x20\x20\ -\x20\x20\x20\x20\x20\x72\x64\x66\x3a\x72\x65\x73\x6f\x75\x72\x63\ -\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\x75\x72\x6c\x2e\x6f\ -\x72\x67\x2f\x64\x63\x2f\x64\x63\x6d\x69\x74\x79\x70\x65\x2f\x53\ -\x74\x69\x6c\x6c\x49\x6d\x61\x67\x65\x22\x20\x2f\x3e\x0a\x20\x20\ -\x20\x20\x20\x20\x20\x20\x3c\x64\x63\x3a\x74\x69\x74\x6c\x65\x3e\ -\x3c\x2f\x64\x63\x3a\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\x20\x20\ -\x20\x20\x3c\x2f\x63\x63\x3a\x57\x6f\x72\x6b\x3e\x0a\x20\x20\x20\ -\x20\x3c\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\x0a\x20\x20\x3c\x2f\ -\x6d\x65\x74\x61\x64\x61\x74\x61\x3e\x0a\x20\x20\x3c\x64\x65\x66\ -\x73\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x64\x65\x66\x73\x31\ -\x30\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x73\x6f\x64\x69\x70\x6f\x64\ -\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x0a\x20\x20\x20\x20\ -\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x75\x72\x72\x65\x6e\ -\x74\x2d\x6c\x61\x79\x65\x72\x3d\x22\x73\x76\x67\x36\x22\x0a\x20\ +\x6e\x6f\x22\x3f\x3e\x0d\x0a\x3c\x73\x76\x67\x0d\x0a\x20\x20\x20\ +\x78\x6d\x6c\x6e\x73\x3a\x64\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\ +\x2f\x70\x75\x72\x6c\x2e\x6f\x72\x67\x2f\x64\x63\x2f\x65\x6c\x65\ +\x6d\x65\x6e\x74\x73\x2f\x31\x2e\x31\x2f\x22\x0d\x0a\x20\x20\x20\ +\x78\x6d\x6c\x6e\x73\x3a\x63\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\ +\x2f\x63\x72\x65\x61\x74\x69\x76\x65\x63\x6f\x6d\x6d\x6f\x6e\x73\ +\x2e\x6f\x72\x67\x2f\x6e\x73\x23\x22\x0d\x0a\x20\x20\x20\x78\x6d\ +\x6c\x6e\x73\x3a\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\ +\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\ +\x30\x32\x2f\x32\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\ +\x2d\x6e\x73\x23\x22\x0d\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\ +\x73\x76\x67\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\ +\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\ +\x0d\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ +\x30\x30\x2f\x73\x76\x67\x22\x0d\x0a\x20\x20\x20\x78\x6d\x6c\x6e\ +\x73\x3a\x73\x6f\x64\x69\x70\x6f\x64\x69\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x73\x6f\x64\x69\x70\x6f\x64\x69\x2e\x73\x6f\x75\x72\ +\x63\x65\x66\x6f\x72\x67\x65\x2e\x6e\x65\x74\x2f\x44\x54\x44\x2f\ +\x73\x6f\x64\x69\x70\x6f\x64\x69\x2d\x30\x2e\x64\x74\x64\x22\x0d\ +\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x69\x6e\x6b\x73\x63\x61\ +\x70\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x69\ +\x6e\x6b\x73\x63\x61\x70\x65\x2e\x6f\x72\x67\x2f\x6e\x61\x6d\x65\ +\x73\x70\x61\x63\x65\x73\x2f\x69\x6e\x6b\x73\x63\x61\x70\x65\x22\ +\x0d\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x76\x65\ +\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x30\x72\x63\x31\x20\x28\x30\ +\x39\x39\x36\x30\x64\x36\x66\x30\x35\x2c\x20\x32\x30\x32\x30\x2d\ +\x30\x34\x2d\x30\x39\x29\x22\x0d\x0a\x20\x20\x20\x73\x6f\x64\x69\ +\x70\x6f\x64\x69\x3a\x64\x6f\x63\x6e\x61\x6d\x65\x3d\x22\x63\x6f\ +\x6c\x6c\x61\x70\x73\x65\x2e\x73\x76\x67\x22\x0d\x0a\x20\x20\x20\ +\x69\x64\x3d\x22\x73\x76\x67\x36\x22\x0d\x0a\x20\x20\x20\x76\x65\ +\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x31\x22\x0d\x0a\x20\x20\x20\ +\x63\x6c\x61\x73\x73\x3d\x22\x66\x65\x61\x74\x68\x65\x72\x20\x66\ +\x65\x61\x74\x68\x65\x72\x2d\x63\x6f\x64\x65\x22\x0d\x0a\x20\x20\ +\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\ +\x3d\x22\x72\x6f\x75\x6e\x64\x22\x0d\x0a\x20\x20\x20\x73\x74\x72\ +\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3d\x22\x72\x6f\x75\ +\x6e\x64\x22\x0d\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\ +\x69\x64\x74\x68\x3d\x22\x32\x22\x0d\x0a\x20\x20\x20\x73\x74\x72\ +\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\x6f\ +\x72\x22\x0d\x0a\x20\x20\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\ +\x65\x22\x0d\x0a\x20\x20\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ +\x30\x20\x30\x20\x32\x34\x20\x32\x34\x22\x0d\x0a\x20\x20\x20\x68\ +\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x0d\x0a\x20\x20\x20\x77\ +\x69\x64\x74\x68\x3d\x22\x32\x34\x22\x3e\x0d\x0a\x20\x20\x3c\x6d\ +\x65\x74\x61\x64\x61\x74\x61\x0d\x0a\x20\x20\x20\x20\x20\x69\x64\ +\x3d\x22\x6d\x65\x74\x61\x64\x61\x74\x61\x31\x32\x22\x3e\x0d\x0a\ +\x20\x20\x20\x20\x3c\x72\x64\x66\x3a\x52\x44\x46\x3e\x0d\x0a\x20\ +\x20\x20\x20\x20\x20\x3c\x63\x63\x3a\x57\x6f\x72\x6b\x0d\x0a\x20\ +\x20\x20\x20\x20\x20\x20\x20\x20\x72\x64\x66\x3a\x61\x62\x6f\x75\ +\x74\x3d\x22\x22\x3e\x0d\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\ +\x64\x63\x3a\x66\x6f\x72\x6d\x61\x74\x3e\x69\x6d\x61\x67\x65\x2f\ +\x73\x76\x67\x2b\x78\x6d\x6c\x3c\x2f\x64\x63\x3a\x66\x6f\x72\x6d\ +\x61\x74\x3e\x0d\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x63\ +\x3a\x74\x79\x70\x65\x0d\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\ +\x20\x20\x72\x64\x66\x3a\x72\x65\x73\x6f\x75\x72\x63\x65\x3d\x22\ +\x68\x74\x74\x70\x3a\x2f\x2f\x70\x75\x72\x6c\x2e\x6f\x72\x67\x2f\ +\x64\x63\x2f\x64\x63\x6d\x69\x74\x79\x70\x65\x2f\x53\x74\x69\x6c\ +\x6c\x49\x6d\x61\x67\x65\x22\x20\x2f\x3e\x0d\x0a\x20\x20\x20\x20\ +\x20\x20\x20\x20\x3c\x64\x63\x3a\x74\x69\x74\x6c\x65\x3e\x3c\x2f\ +\x64\x63\x3a\x74\x69\x74\x6c\x65\x3e\x0d\x0a\x20\x20\x20\x20\x20\ +\x20\x3c\x2f\x63\x63\x3a\x57\x6f\x72\x6b\x3e\x0d\x0a\x20\x20\x20\ +\x20\x3c\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\x0d\x0a\x20\x20\x3c\ +\x2f\x6d\x65\x74\x61\x64\x61\x74\x61\x3e\x0d\x0a\x20\x20\x3c\x64\ +\x65\x66\x73\x0d\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x64\x65\ +\x66\x73\x31\x30\x22\x20\x2f\x3e\x0d\x0a\x20\x20\x3c\x73\x6f\x64\ +\x69\x70\x6f\x64\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x0d\ +\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\ +\x75\x72\x72\x65\x6e\x74\x2d\x6c\x61\x79\x65\x72\x3d\x22\x73\x76\ +\x67\x36\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\ +\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x6d\x61\x78\x69\x6d\x69\ +\x7a\x65\x64\x3d\x22\x30\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\ +\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x79\x3d\ +\x22\x31\x38\x35\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\ +\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x78\x3d\x22\x31\ +\x33\x38\x37\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\ +\x61\x70\x65\x3a\x63\x79\x3d\x22\x31\x32\x2e\x31\x38\x37\x33\x38\ +\x32\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ +\x65\x3a\x63\x78\x3d\x22\x31\x32\x2e\x34\x37\x33\x33\x38\x37\x22\ +\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\ +\x7a\x6f\x6f\x6d\x3d\x22\x32\x39\x2e\x36\x39\x38\x34\x38\x35\x22\ +\x0d\x0a\x20\x20\x20\x20\x20\x73\x68\x6f\x77\x67\x72\x69\x64\x3d\ +\x22\x66\x61\x6c\x73\x65\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x64\ +\x3d\x22\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x38\x22\x0d\x0a\x20\ \x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\ -\x64\x6f\x77\x2d\x6d\x61\x78\x69\x6d\x69\x7a\x65\x64\x3d\x22\x30\ -\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\ -\x77\x69\x6e\x64\x6f\x77\x2d\x79\x3d\x22\x31\x38\x35\x22\x0a\x20\ -\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\ -\x64\x6f\x77\x2d\x78\x3d\x22\x31\x33\x38\x37\x22\x0a\x20\x20\x20\ -\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x79\x3d\x22\x31\ -\x32\x2e\x31\x38\x37\x33\x38\x32\x22\x0a\x20\x20\x20\x20\x20\x69\ -\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x78\x3d\x22\x31\x32\x2e\x34\ -\x37\x33\x33\x38\x37\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\ -\x63\x61\x70\x65\x3a\x7a\x6f\x6f\x6d\x3d\x22\x32\x39\x2e\x36\x39\ -\x38\x34\x38\x35\x22\x0a\x20\x20\x20\x20\x20\x73\x68\x6f\x77\x67\ -\x72\x69\x64\x3d\x22\x66\x61\x6c\x73\x65\x22\x0a\x20\x20\x20\x20\ -\x20\x69\x64\x3d\x22\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x38\x22\ -\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\ -\x69\x6e\x64\x6f\x77\x2d\x68\x65\x69\x67\x68\x74\x3d\x22\x31\x31\ -\x32\x38\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ -\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x77\x69\x64\x74\x68\x3d\x22\ -\x31\x39\x37\x34\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\ +\x64\x6f\x77\x2d\x68\x65\x69\x67\x68\x74\x3d\x22\x31\x31\x32\x38\ +\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\ +\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x77\x69\x64\x74\x68\x3d\x22\x31\ +\x39\x37\x34\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\ \x61\x70\x65\x3a\x70\x61\x67\x65\x73\x68\x61\x64\x6f\x77\x3d\x22\ -\x32\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\ -\x3a\x70\x61\x67\x65\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\x22\ -\x0a\x20\x20\x20\x20\x20\x67\x75\x69\x64\x65\x74\x6f\x6c\x65\x72\ -\x61\x6e\x63\x65\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x20\x20\x67\ -\x72\x69\x64\x74\x6f\x6c\x65\x72\x61\x6e\x63\x65\x3d\x22\x31\x30\ -\x22\x0a\x20\x20\x20\x20\x20\x6f\x62\x6a\x65\x63\x74\x74\x6f\x6c\ -\x65\x72\x61\x6e\x63\x65\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x20\ -\x20\x62\x6f\x72\x64\x65\x72\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\ -\x31\x22\x0a\x20\x20\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x63\x6f\ -\x6c\x6f\x72\x3d\x22\x23\x36\x36\x36\x36\x36\x36\x22\x0a\x20\x20\ -\x20\x20\x20\x70\x61\x67\x65\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x66\ -\x66\x66\x66\x66\x66\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\ -\x68\x0a\x20\x20\x20\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\ -\x6c\x6c\x3a\x6e\x6f\x6e\x65\x3b\x73\x74\x72\x6f\x6b\x65\x3a\x23\ -\x30\x30\x30\x30\x30\x30\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\ -\x64\x74\x68\x3a\x31\x2e\x38\x39\x34\x34\x33\x3b\x73\x74\x72\x6f\ -\x6b\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3a\x72\x6f\x75\x6e\x64\ -\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\ -\x3a\x6d\x69\x74\x65\x72\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6d\x69\ -\x74\x65\x72\x6c\x69\x6d\x69\x74\x3a\x34\x3b\x73\x74\x72\x6f\x6b\ -\x65\x2d\x64\x61\x73\x68\x61\x72\x72\x61\x79\x3a\x6e\x6f\x6e\x65\ -\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6f\x70\x61\x63\x69\x74\x79\x3a\ -\x31\x22\x0a\x20\x20\x20\x20\x20\x64\x3d\x22\x4d\x20\x32\x2e\x38\ -\x39\x35\x37\x37\x30\x36\x2c\x33\x2e\x37\x33\x37\x35\x36\x34\x34\ -\x20\x48\x20\x32\x30\x2e\x32\x33\x36\x37\x32\x33\x22\x0a\x20\x20\ -\x20\x20\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x38\x35\x37\x22\x0a\ -\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x6f\ -\x6e\x6e\x65\x63\x74\x6f\x72\x2d\x63\x75\x72\x76\x61\x74\x75\x72\ -\x65\x3d\x22\x30\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\ -\x0a\x20\x20\x20\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\ -\x6c\x3a\x6e\x6f\x6e\x65\x3b\x73\x74\x72\x6f\x6b\x65\x3a\x23\x30\ -\x30\x30\x30\x30\x30\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\ -\x74\x68\x3a\x31\x2e\x38\x39\x34\x34\x33\x3b\x73\x74\x72\x6f\x6b\ -\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3a\x72\x6f\x75\x6e\x64\x3b\ -\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3a\ -\x6d\x69\x74\x65\x72\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6d\x69\x74\ -\x65\x72\x6c\x69\x6d\x69\x74\x3a\x34\x3b\x73\x74\x72\x6f\x6b\x65\ -\x2d\x64\x61\x73\x68\x61\x72\x72\x61\x79\x3a\x6e\x6f\x6e\x65\x3b\ -\x73\x74\x72\x6f\x6b\x65\x2d\x6f\x70\x61\x63\x69\x74\x79\x3a\x31\ -\x22\x0a\x20\x20\x20\x20\x20\x64\x3d\x22\x4d\x20\x32\x2e\x38\x39\ -\x35\x37\x37\x30\x36\x2c\x31\x31\x2e\x34\x33\x37\x31\x37\x32\x20\ -\x48\x20\x32\x30\x2e\x32\x33\x36\x37\x32\x33\x22\x0a\x20\x20\x20\ -\x20\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x38\x35\x39\x22\x0a\x20\ -\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x6f\x6e\ -\x6e\x65\x63\x74\x6f\x72\x2d\x63\x75\x72\x76\x61\x74\x75\x72\x65\ -\x3d\x22\x30\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\x0a\ +\x32\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ +\x65\x3a\x70\x61\x67\x65\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\ +\x22\x0d\x0a\x20\x20\x20\x20\x20\x67\x75\x69\x64\x65\x74\x6f\x6c\ +\x65\x72\x61\x6e\x63\x65\x3d\x22\x31\x30\x22\x0d\x0a\x20\x20\x20\ +\x20\x20\x67\x72\x69\x64\x74\x6f\x6c\x65\x72\x61\x6e\x63\x65\x3d\ +\x22\x31\x30\x22\x0d\x0a\x20\x20\x20\x20\x20\x6f\x62\x6a\x65\x63\ +\x74\x74\x6f\x6c\x65\x72\x61\x6e\x63\x65\x3d\x22\x31\x30\x22\x0d\ +\x0a\x20\x20\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x6f\x70\x61\x63\ +\x69\x74\x79\x3d\x22\x31\x22\x0d\x0a\x20\x20\x20\x20\x20\x62\x6f\ +\x72\x64\x65\x72\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x36\x36\x36\x36\ +\x36\x36\x22\x0d\x0a\x20\x20\x20\x20\x20\x70\x61\x67\x65\x63\x6f\ +\x6c\x6f\x72\x3d\x22\x23\x66\x66\x66\x66\x66\x66\x22\x20\x2f\x3e\ +\x0d\x0a\x20\x20\x3c\x70\x61\x74\x68\x0d\x0a\x20\x20\x20\x20\x20\ +\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\x3a\x6e\x6f\x6e\x65\ +\x3b\x73\x74\x72\x6f\x6b\x65\x3a\x23\x30\x30\x30\x30\x30\x30\x3b\ +\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\x3a\x31\x2e\x38\ +\x39\x34\x34\x33\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ +\x63\x61\x70\x3a\x72\x6f\x75\x6e\x64\x3b\x73\x74\x72\x6f\x6b\x65\ +\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3a\x6d\x69\x74\x65\x72\x3b\ +\x73\x74\x72\x6f\x6b\x65\x2d\x6d\x69\x74\x65\x72\x6c\x69\x6d\x69\ +\x74\x3a\x34\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x64\x61\x73\x68\x61\ +\x72\x72\x61\x79\x3a\x6e\x6f\x6e\x65\x3b\x73\x74\x72\x6f\x6b\x65\ +\x2d\x6f\x70\x61\x63\x69\x74\x79\x3a\x31\x22\x0d\x0a\x20\x20\x20\ +\x20\x20\x64\x3d\x22\x4d\x20\x32\x2e\x38\x39\x35\x37\x37\x30\x36\ +\x2c\x33\x2e\x37\x33\x37\x35\x36\x34\x34\x20\x48\x20\x32\x30\x2e\ +\x32\x33\x36\x37\x32\x33\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x64\ +\x3d\x22\x70\x61\x74\x68\x38\x35\x37\x22\x0d\x0a\x20\x20\x20\x20\ +\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x6f\x6e\x6e\x65\x63\ +\x74\x6f\x72\x2d\x63\x75\x72\x76\x61\x74\x75\x72\x65\x3d\x22\x30\ +\x22\x20\x2f\x3e\x0d\x0a\x20\x20\x3c\x70\x61\x74\x68\x0d\x0a\x20\ +\x20\x20\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\x3a\ +\x6e\x6f\x6e\x65\x3b\x73\x74\x72\x6f\x6b\x65\x3a\x23\x30\x30\x30\ +\x30\x30\x30\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\ +\x3a\x31\x2e\x38\x39\x34\x34\x33\x3b\x73\x74\x72\x6f\x6b\x65\x2d\ +\x6c\x69\x6e\x65\x63\x61\x70\x3a\x72\x6f\x75\x6e\x64\x3b\x73\x74\ +\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\x6e\x3a\x6d\x69\ +\x74\x65\x72\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6d\x69\x74\x65\x72\ +\x6c\x69\x6d\x69\x74\x3a\x34\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x64\ +\x61\x73\x68\x61\x72\x72\x61\x79\x3a\x6e\x6f\x6e\x65\x3b\x73\x74\ +\x72\x6f\x6b\x65\x2d\x6f\x70\x61\x63\x69\x74\x79\x3a\x31\x22\x0d\ +\x0a\x20\x20\x20\x20\x20\x64\x3d\x22\x4d\x20\x32\x2e\x38\x39\x35\ +\x37\x37\x30\x36\x2c\x31\x31\x2e\x34\x33\x37\x31\x37\x32\x20\x48\ +\x20\x32\x30\x2e\x32\x33\x36\x37\x32\x33\x22\x0d\x0a\x20\x20\x20\ +\x20\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x38\x35\x39\x22\x0d\x0a\ \x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x6f\ \x6e\x6e\x65\x63\x74\x6f\x72\x2d\x63\x75\x72\x76\x61\x74\x75\x72\ -\x65\x3d\x22\x30\x22\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x70\ -\x61\x74\x68\x38\x35\x35\x22\x0a\x20\x20\x20\x20\x20\x64\x3d\x22\ -\x4d\x20\x32\x2e\x38\x39\x35\x37\x37\x30\x36\x2c\x37\x2e\x35\x38\ -\x37\x33\x36\x38\x31\x20\x48\x20\x32\x30\x2e\x32\x33\x36\x37\x32\ -\x33\x22\x0a\x20\x20\x20\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\ -\x69\x6c\x6c\x3a\x6e\x6f\x6e\x65\x3b\x73\x74\x72\x6f\x6b\x65\x3a\ -\x23\x30\x30\x30\x30\x30\x30\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x77\ -\x69\x64\x74\x68\x3a\x31\x2e\x38\x39\x34\x34\x33\x3b\x73\x74\x72\ -\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3a\x72\x6f\x75\x6e\ -\x64\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\x69\ -\x6e\x3a\x6d\x69\x74\x65\x72\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6d\ -\x69\x74\x65\x72\x6c\x69\x6d\x69\x74\x3a\x34\x3b\x73\x74\x72\x6f\ -\x6b\x65\x2d\x64\x61\x73\x68\x61\x72\x72\x61\x79\x3a\x6e\x6f\x6e\ -\x65\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6f\x70\x61\x63\x69\x74\x79\ -\x3a\x31\x22\x20\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\x0a\ -\x00\x00\x07\x9d\ +\x65\x3d\x22\x30\x22\x20\x2f\x3e\x0d\x0a\x20\x20\x3c\x70\x61\x74\ +\x68\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\ +\x3a\x63\x6f\x6e\x6e\x65\x63\x74\x6f\x72\x2d\x63\x75\x72\x76\x61\ +\x74\x75\x72\x65\x3d\x22\x30\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\ +\x64\x3d\x22\x70\x61\x74\x68\x38\x35\x35\x22\x0d\x0a\x20\x20\x20\ +\x20\x20\x64\x3d\x22\x4d\x20\x32\x2e\x38\x39\x35\x37\x37\x30\x36\ +\x2c\x37\x2e\x35\x38\x37\x33\x36\x38\x31\x20\x48\x20\x32\x30\x2e\ +\x32\x33\x36\x37\x32\x33\x22\x0d\x0a\x20\x20\x20\x20\x20\x73\x74\ +\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\x3a\x6e\x6f\x6e\x65\x3b\x73\ +\x74\x72\x6f\x6b\x65\x3a\x23\x30\x30\x30\x30\x30\x30\x3b\x73\x74\ +\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\x3a\x31\x2e\x38\x39\x34\ +\x34\x33\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x63\x61\ +\x70\x3a\x72\x6f\x75\x6e\x64\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6c\ +\x69\x6e\x65\x6a\x6f\x69\x6e\x3a\x6d\x69\x74\x65\x72\x3b\x73\x74\ +\x72\x6f\x6b\x65\x2d\x6d\x69\x74\x65\x72\x6c\x69\x6d\x69\x74\x3a\ +\x34\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x64\x61\x73\x68\x61\x72\x72\ +\x61\x79\x3a\x6e\x6f\x6e\x65\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x6f\ +\x70\x61\x63\x69\x74\x79\x3a\x31\x22\x20\x2f\x3e\x0d\x0a\x3c\x2f\ +\x73\x76\x67\x3e\x0d\x0a\ +\x00\x00\x07\xe6\ \x3c\ \x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ \x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ \x2d\x38\x22\x20\x73\x74\x61\x6e\x64\x61\x6c\x6f\x6e\x65\x3d\x22\ -\x6e\x6f\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x0a\x20\x20\x20\x78\x6d\ -\x6c\x6e\x73\x3a\x64\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\ -\x75\x72\x6c\x2e\x6f\x72\x67\x2f\x64\x63\x2f\x65\x6c\x65\x6d\x65\ -\x6e\x74\x73\x2f\x31\x2e\x31\x2f\x22\x0a\x20\x20\x20\x78\x6d\x6c\ -\x6e\x73\x3a\x63\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x63\x72\ -\x65\x61\x74\x69\x76\x65\x63\x6f\x6d\x6d\x6f\x6e\x73\x2e\x6f\x72\ -\x67\x2f\x6e\x73\x23\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\ -\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\ -\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\x2f\x32\ -\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\x73\x23\ -\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x76\x67\x3d\x22\ -\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\ -\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x0a\x20\x20\x20\x78\ -\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\ -\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\ -\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x6f\x64\x69\x70\ -\x6f\x64\x69\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x73\x6f\x64\x69\ -\x70\x6f\x64\x69\x2e\x73\x6f\x75\x72\x63\x65\x66\x6f\x72\x67\x65\ -\x2e\x6e\x65\x74\x2f\x44\x54\x44\x2f\x73\x6f\x64\x69\x70\x6f\x64\ -\x69\x2d\x30\x2e\x64\x74\x64\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\ -\x73\x3a\x69\x6e\x6b\x73\x63\x61\x70\x65\x3d\x22\x68\x74\x74\x70\ -\x3a\x2f\x2f\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\x2e\ -\x6f\x72\x67\x2f\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x73\x2f\x69\ -\x6e\x6b\x73\x63\x61\x70\x65\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\ -\x63\x61\x70\x65\x3a\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ -\x30\x72\x63\x31\x20\x28\x30\x39\x39\x36\x30\x64\x36\x66\x30\x35\ -\x2c\x20\x32\x30\x32\x30\x2d\x30\x34\x2d\x30\x39\x29\x22\x0a\x20\ -\x20\x20\x73\x6f\x64\x69\x70\x6f\x64\x69\x3a\x64\x6f\x63\x6e\x61\ -\x6d\x65\x3d\x22\x61\x6c\x65\x72\x74\x2d\x6f\x63\x74\x61\x67\x6f\ -\x6e\x2d\x72\x65\x64\x2e\x73\x76\x67\x22\x0a\x20\x20\x20\x69\x64\ -\x3d\x22\x73\x76\x67\x38\x22\x0a\x20\x20\x20\x76\x65\x72\x73\x69\ -\x6f\x6e\x3d\x22\x31\x2e\x31\x22\x0a\x20\x20\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x66\x65\x61\x74\x68\x65\x72\x20\x66\x65\x61\x74\x68\ -\x65\x72\x2d\x61\x6c\x65\x72\x74\x2d\x6f\x63\x74\x61\x67\x6f\x6e\ -\x22\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ -\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\x6e\x64\x22\x0a\x20\x20\x20\ -\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3d\x22\ -\x72\x6f\x75\x6e\x64\x22\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\ -\x2d\x77\x69\x64\x74\x68\x3d\x22\x32\x22\x0a\x20\x20\x20\x73\x74\ -\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ -\x6f\x72\x22\x0a\x20\x20\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\ -\x65\x22\x0a\x20\x20\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\ -\x20\x30\x20\x32\x34\x20\x32\x34\x22\x0a\x20\x20\x20\x68\x65\x69\ -\x67\x68\x74\x3d\x22\x32\x34\x22\x0a\x20\x20\x20\x77\x69\x64\x74\ -\x68\x3d\x22\x32\x34\x22\x3e\x0a\x20\x20\x3c\x6d\x65\x74\x61\x64\ -\x61\x74\x61\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x6d\x65\x74\ -\x61\x64\x61\x74\x61\x31\x34\x22\x3e\x0a\x20\x20\x20\x20\x3c\x72\ -\x64\x66\x3a\x52\x44\x46\x3e\x0a\x20\x20\x20\x20\x20\x20\x3c\x63\ -\x63\x3a\x57\x6f\x72\x6b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\ -\x72\x64\x66\x3a\x61\x62\x6f\x75\x74\x3d\x22\x22\x3e\x0a\x20\x20\ -\x20\x20\x20\x20\x20\x20\x3c\x64\x63\x3a\x66\x6f\x72\x6d\x61\x74\ -\x3e\x69\x6d\x61\x67\x65\x2f\x73\x76\x67\x2b\x78\x6d\x6c\x3c\x2f\ -\x64\x63\x3a\x66\x6f\x72\x6d\x61\x74\x3e\x0a\x20\x20\x20\x20\x20\ -\x20\x20\x20\x3c\x64\x63\x3a\x74\x79\x70\x65\x0a\x20\x20\x20\x20\ -\x20\x20\x20\x20\x20\x20\x20\x72\x64\x66\x3a\x72\x65\x73\x6f\x75\ -\x72\x63\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\x75\x72\x6c\ -\x2e\x6f\x72\x67\x2f\x64\x63\x2f\x64\x63\x6d\x69\x74\x79\x70\x65\ -\x2f\x53\x74\x69\x6c\x6c\x49\x6d\x61\x67\x65\x22\x20\x2f\x3e\x0a\ -\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x63\x3a\x74\x69\x74\x6c\ -\x65\x3e\x3c\x2f\x64\x63\x3a\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\ -\x20\x20\x20\x20\x3c\x2f\x63\x63\x3a\x57\x6f\x72\x6b\x3e\x0a\x20\ -\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\x0a\x20\x20\ -\x3c\x2f\x6d\x65\x74\x61\x64\x61\x74\x61\x3e\x0a\x20\x20\x3c\x64\ -\x65\x66\x73\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x64\x65\x66\ -\x73\x31\x32\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x73\x6f\x64\x69\x70\ -\x6f\x64\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x0a\x20\x20\ -\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x75\x72\x72\ -\x65\x6e\x74\x2d\x6c\x61\x79\x65\x72\x3d\x22\x73\x76\x67\x38\x22\ -\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\ -\x69\x6e\x64\x6f\x77\x2d\x6d\x61\x78\x69\x6d\x69\x7a\x65\x64\x3d\ -\x22\x30\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ -\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x79\x3d\x22\x32\x38\x31\x22\ -\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\ -\x69\x6e\x64\x6f\x77\x2d\x78\x3d\x22\x31\x34\x33\x33\x22\x0a\x20\ -\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x79\x3d\ -\x22\x31\x32\x2e\x34\x30\x39\x37\x39\x37\x22\x0a\x20\x20\x20\x20\ -\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x78\x3d\x22\x31\x32\ -\x2e\x38\x32\x38\x32\x31\x36\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\ -\x6b\x73\x63\x61\x70\x65\x3a\x7a\x6f\x6f\x6d\x3d\x22\x32\x31\x22\ -\x0a\x20\x20\x20\x20\x20\x73\x68\x6f\x77\x67\x72\x69\x64\x3d\x22\ -\x66\x61\x6c\x73\x65\x22\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\ -\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x31\x30\x22\x0a\x20\x20\x20\ -\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\ -\x77\x2d\x68\x65\x69\x67\x68\x74\x3d\x22\x39\x35\x39\x22\x0a\x20\ -\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\ -\x64\x6f\x77\x2d\x77\x69\x64\x74\x68\x3d\x22\x31\x35\x34\x32\x22\ +\x6e\x6f\x22\x3f\x3e\x0d\x0a\x3c\x73\x76\x67\x0d\x0a\x20\x20\x20\ +\x78\x6d\x6c\x6e\x73\x3a\x64\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\ +\x2f\x70\x75\x72\x6c\x2e\x6f\x72\x67\x2f\x64\x63\x2f\x65\x6c\x65\ +\x6d\x65\x6e\x74\x73\x2f\x31\x2e\x31\x2f\x22\x0d\x0a\x20\x20\x20\ +\x78\x6d\x6c\x6e\x73\x3a\x63\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\ +\x2f\x63\x72\x65\x61\x74\x69\x76\x65\x63\x6f\x6d\x6d\x6f\x6e\x73\ +\x2e\x6f\x72\x67\x2f\x6e\x73\x23\x22\x0d\x0a\x20\x20\x20\x78\x6d\ +\x6c\x6e\x73\x3a\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\ +\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\ +\x30\x32\x2f\x32\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\ +\x2d\x6e\x73\x23\x22\x0d\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\ +\x73\x76\x67\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\ +\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\ +\x0d\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ +\x30\x30\x2f\x73\x76\x67\x22\x0d\x0a\x20\x20\x20\x78\x6d\x6c\x6e\ +\x73\x3a\x73\x6f\x64\x69\x70\x6f\x64\x69\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x73\x6f\x64\x69\x70\x6f\x64\x69\x2e\x73\x6f\x75\x72\ +\x63\x65\x66\x6f\x72\x67\x65\x2e\x6e\x65\x74\x2f\x44\x54\x44\x2f\ +\x73\x6f\x64\x69\x70\x6f\x64\x69\x2d\x30\x2e\x64\x74\x64\x22\x0d\ +\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x69\x6e\x6b\x73\x63\x61\ +\x70\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x69\ +\x6e\x6b\x73\x63\x61\x70\x65\x2e\x6f\x72\x67\x2f\x6e\x61\x6d\x65\ +\x73\x70\x61\x63\x65\x73\x2f\x69\x6e\x6b\x73\x63\x61\x70\x65\x22\ +\x0d\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x76\x65\ +\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x30\x72\x63\x31\x20\x28\x30\ +\x39\x39\x36\x30\x64\x36\x66\x30\x35\x2c\x20\x32\x30\x32\x30\x2d\ +\x30\x34\x2d\x30\x39\x29\x22\x0d\x0a\x20\x20\x20\x73\x6f\x64\x69\ +\x70\x6f\x64\x69\x3a\x64\x6f\x63\x6e\x61\x6d\x65\x3d\x22\x61\x6c\ +\x65\x72\x74\x2d\x6f\x63\x74\x61\x67\x6f\x6e\x2d\x72\x65\x64\x2e\ +\x73\x76\x67\x22\x0d\x0a\x20\x20\x20\x69\x64\x3d\x22\x73\x76\x67\ +\x38\x22\x0d\x0a\x20\x20\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\ +\x31\x2e\x31\x22\x0d\x0a\x20\x20\x20\x63\x6c\x61\x73\x73\x3d\x22\ +\x66\x65\x61\x74\x68\x65\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\ +\x61\x6c\x65\x72\x74\x2d\x6f\x63\x74\x61\x67\x6f\x6e\x22\x0d\x0a\ +\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\ +\x69\x6e\x3d\x22\x72\x6f\x75\x6e\x64\x22\x0d\x0a\x20\x20\x20\x73\ +\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3d\x22\x72\ +\x6f\x75\x6e\x64\x22\x0d\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\ +\x2d\x77\x69\x64\x74\x68\x3d\x22\x32\x22\x0d\x0a\x20\x20\x20\x73\ +\x74\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\ +\x6c\x6f\x72\x22\x0d\x0a\x20\x20\x20\x66\x69\x6c\x6c\x3d\x22\x6e\ +\x6f\x6e\x65\x22\x0d\x0a\x20\x20\x20\x76\x69\x65\x77\x42\x6f\x78\ +\x3d\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\x22\x0d\x0a\x20\x20\ +\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x0d\x0a\x20\x20\ +\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\x22\x3e\x0d\x0a\x20\x20\ +\x3c\x6d\x65\x74\x61\x64\x61\x74\x61\x0d\x0a\x20\x20\x20\x20\x20\ +\x69\x64\x3d\x22\x6d\x65\x74\x61\x64\x61\x74\x61\x31\x34\x22\x3e\ +\x0d\x0a\x20\x20\x20\x20\x3c\x72\x64\x66\x3a\x52\x44\x46\x3e\x0d\ +\x0a\x20\x20\x20\x20\x20\x20\x3c\x63\x63\x3a\x57\x6f\x72\x6b\x0d\ +\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x72\x64\x66\x3a\x61\x62\ +\x6f\x75\x74\x3d\x22\x22\x3e\x0d\x0a\x20\x20\x20\x20\x20\x20\x20\ +\x20\x3c\x64\x63\x3a\x66\x6f\x72\x6d\x61\x74\x3e\x69\x6d\x61\x67\ +\x65\x2f\x73\x76\x67\x2b\x78\x6d\x6c\x3c\x2f\x64\x63\x3a\x66\x6f\ +\x72\x6d\x61\x74\x3e\x0d\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\ +\x64\x63\x3a\x74\x79\x70\x65\x0d\x0a\x20\x20\x20\x20\x20\x20\x20\ +\x20\x20\x20\x20\x72\x64\x66\x3a\x72\x65\x73\x6f\x75\x72\x63\x65\ +\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\x75\x72\x6c\x2e\x6f\x72\ +\x67\x2f\x64\x63\x2f\x64\x63\x6d\x69\x74\x79\x70\x65\x2f\x53\x74\ +\x69\x6c\x6c\x49\x6d\x61\x67\x65\x22\x20\x2f\x3e\x0d\x0a\x20\x20\ +\x20\x20\x20\x20\x20\x20\x3c\x64\x63\x3a\x74\x69\x74\x6c\x65\x3e\ +\x3c\x2f\x64\x63\x3a\x74\x69\x74\x6c\x65\x3e\x0d\x0a\x20\x20\x20\ +\x20\x20\x20\x3c\x2f\x63\x63\x3a\x57\x6f\x72\x6b\x3e\x0d\x0a\x20\ +\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\x0d\x0a\x20\ +\x20\x3c\x2f\x6d\x65\x74\x61\x64\x61\x74\x61\x3e\x0d\x0a\x20\x20\ +\x3c\x64\x65\x66\x73\x0d\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\ +\x64\x65\x66\x73\x31\x32\x22\x20\x2f\x3e\x0d\x0a\x20\x20\x3c\x73\ +\x6f\x64\x69\x70\x6f\x64\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\ +\x77\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\ +\x3a\x63\x75\x72\x72\x65\x6e\x74\x2d\x6c\x61\x79\x65\x72\x3d\x22\ +\x73\x76\x67\x38\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\ +\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x6d\x61\x78\x69\ +\x6d\x69\x7a\x65\x64\x3d\x22\x30\x22\x0d\x0a\x20\x20\x20\x20\x20\ +\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\ +\x79\x3d\x22\x32\x38\x31\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\ +\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x78\x3d\ +\x22\x31\x34\x33\x33\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\ +\x73\x63\x61\x70\x65\x3a\x63\x79\x3d\x22\x31\x32\x2e\x34\x30\x39\ +\x37\x39\x37\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\ +\x61\x70\x65\x3a\x63\x78\x3d\x22\x31\x32\x2e\x38\x32\x38\x32\x31\ +\x36\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ +\x65\x3a\x7a\x6f\x6f\x6d\x3d\x22\x32\x31\x22\x0d\x0a\x20\x20\x20\ +\x20\x20\x73\x68\x6f\x77\x67\x72\x69\x64\x3d\x22\x66\x61\x6c\x73\ +\x65\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x6e\x61\x6d\ +\x65\x64\x76\x69\x65\x77\x31\x30\x22\x0d\x0a\x20\x20\x20\x20\x20\ +\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\ +\x68\x65\x69\x67\x68\x74\x3d\x22\x39\x35\x39\x22\x0d\x0a\x20\x20\ +\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\ +\x6f\x77\x2d\x77\x69\x64\x74\x68\x3d\x22\x31\x35\x34\x32\x22\x0d\ \x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x70\ -\x61\x67\x65\x73\x68\x61\x64\x6f\x77\x3d\x22\x32\x22\x0a\x20\x20\ -\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x70\x61\x67\x65\ -\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\x22\x0a\x20\x20\x20\x20\ -\x20\x67\x75\x69\x64\x65\x74\x6f\x6c\x65\x72\x61\x6e\x63\x65\x3d\ -\x22\x31\x30\x22\x0a\x20\x20\x20\x20\x20\x67\x72\x69\x64\x74\x6f\ -\x6c\x65\x72\x61\x6e\x63\x65\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\ -\x20\x20\x6f\x62\x6a\x65\x63\x74\x74\x6f\x6c\x65\x72\x61\x6e\x63\ -\x65\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x20\x20\x62\x6f\x72\x64\ -\x65\x72\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x31\x22\x0a\x20\x20\ -\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x63\x6f\x6c\x6f\x72\x3d\x22\ -\x23\x36\x36\x36\x36\x36\x36\x22\x0a\x20\x20\x20\x20\x20\x70\x61\ -\x67\x65\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x66\x66\x66\x66\x66\x66\ -\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x70\x6f\x6c\x79\x67\x6f\x6e\x0a\ -\x20\x20\x20\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\ -\x3a\x23\x30\x30\x66\x66\x30\x30\x22\x0a\x20\x20\x20\x20\x20\x69\ -\x64\x3d\x22\x70\x6f\x6c\x79\x67\x6f\x6e\x32\x22\x0a\x20\x20\x20\ -\x20\x20\x70\x6f\x69\x6e\x74\x73\x3d\x22\x37\x2e\x38\x36\x20\x32\ -\x20\x31\x36\x2e\x31\x34\x20\x32\x20\x32\x32\x20\x37\x2e\x38\x36\ -\x20\x32\x32\x20\x31\x36\x2e\x31\x34\x20\x31\x36\x2e\x31\x34\x20\ -\x32\x32\x20\x37\x2e\x38\x36\x20\x32\x32\x20\x32\x20\x31\x36\x2e\ -\x31\x34\x20\x32\x20\x37\x2e\x38\x36\x20\x37\x2e\x38\x36\x20\x32\ -\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x6c\x69\x6e\x65\x0a\x20\x20\x20\ -\x20\x20\x69\x64\x3d\x22\x6c\x69\x6e\x65\x34\x22\x0a\x20\x20\x20\ -\x20\x20\x79\x32\x3d\x22\x31\x32\x22\x0a\x20\x20\x20\x20\x20\x78\ -\x32\x3d\x22\x31\x32\x22\x0a\x20\x20\x20\x20\x20\x79\x31\x3d\x22\ -\x38\x22\x0a\x20\x20\x20\x20\x20\x78\x31\x3d\x22\x31\x32\x22\x20\ -\x2f\x3e\x0a\x20\x20\x3c\x6c\x69\x6e\x65\x0a\x20\x20\x20\x20\x20\ -\x69\x64\x3d\x22\x6c\x69\x6e\x65\x36\x22\x0a\x20\x20\x20\x20\x20\ -\x79\x32\x3d\x22\x31\x36\x22\x0a\x20\x20\x20\x20\x20\x78\x32\x3d\ -\x22\x31\x32\x2e\x30\x31\x22\x0a\x20\x20\x20\x20\x20\x79\x31\x3d\ -\x22\x31\x36\x22\x0a\x20\x20\x20\x20\x20\x78\x31\x3d\x22\x31\x32\ -\x22\x20\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\x0a\ +\x61\x67\x65\x73\x68\x61\x64\x6f\x77\x3d\x22\x32\x22\x0d\x0a\x20\ +\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x70\x61\x67\ +\x65\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\x22\x0d\x0a\x20\x20\ +\x20\x20\x20\x67\x75\x69\x64\x65\x74\x6f\x6c\x65\x72\x61\x6e\x63\ +\x65\x3d\x22\x31\x30\x22\x0d\x0a\x20\x20\x20\x20\x20\x67\x72\x69\ +\x64\x74\x6f\x6c\x65\x72\x61\x6e\x63\x65\x3d\x22\x31\x30\x22\x0d\ +\x0a\x20\x20\x20\x20\x20\x6f\x62\x6a\x65\x63\x74\x74\x6f\x6c\x65\ +\x72\x61\x6e\x63\x65\x3d\x22\x31\x30\x22\x0d\x0a\x20\x20\x20\x20\ +\x20\x62\x6f\x72\x64\x65\x72\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\ +\x31\x22\x0d\x0a\x20\x20\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x63\ +\x6f\x6c\x6f\x72\x3d\x22\x23\x36\x36\x36\x36\x36\x36\x22\x0d\x0a\ +\x20\x20\x20\x20\x20\x70\x61\x67\x65\x63\x6f\x6c\x6f\x72\x3d\x22\ +\x23\x66\x66\x66\x66\x66\x66\x22\x20\x2f\x3e\x0d\x0a\x20\x20\x3c\ +\x70\x6f\x6c\x79\x67\x6f\x6e\x0d\x0a\x20\x20\x20\x20\x20\x73\x74\ +\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\x3a\x23\x30\x30\x66\x66\x30\ +\x30\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x70\x6f\x6c\ +\x79\x67\x6f\x6e\x32\x22\x0d\x0a\x20\x20\x20\x20\x20\x70\x6f\x69\ +\x6e\x74\x73\x3d\x22\x37\x2e\x38\x36\x20\x32\x20\x31\x36\x2e\x31\ +\x34\x20\x32\x20\x32\x32\x20\x37\x2e\x38\x36\x20\x32\x32\x20\x31\ +\x36\x2e\x31\x34\x20\x31\x36\x2e\x31\x34\x20\x32\x32\x20\x37\x2e\ +\x38\x36\x20\x32\x32\x20\x32\x20\x31\x36\x2e\x31\x34\x20\x32\x20\ +\x37\x2e\x38\x36\x20\x37\x2e\x38\x36\x20\x32\x22\x20\x2f\x3e\x0d\ +\x0a\x20\x20\x3c\x6c\x69\x6e\x65\x0d\x0a\x20\x20\x20\x20\x20\x69\ +\x64\x3d\x22\x6c\x69\x6e\x65\x34\x22\x0d\x0a\x20\x20\x20\x20\x20\ +\x79\x32\x3d\x22\x31\x32\x22\x0d\x0a\x20\x20\x20\x20\x20\x78\x32\ +\x3d\x22\x31\x32\x22\x0d\x0a\x20\x20\x20\x20\x20\x79\x31\x3d\x22\ +\x38\x22\x0d\x0a\x20\x20\x20\x20\x20\x78\x31\x3d\x22\x31\x32\x22\ +\x20\x2f\x3e\x0d\x0a\x20\x20\x3c\x6c\x69\x6e\x65\x0d\x0a\x20\x20\ +\x20\x20\x20\x69\x64\x3d\x22\x6c\x69\x6e\x65\x36\x22\x0d\x0a\x20\ +\x20\x20\x20\x20\x79\x32\x3d\x22\x31\x36\x22\x0d\x0a\x20\x20\x20\ +\x20\x20\x78\x32\x3d\x22\x31\x32\x2e\x30\x31\x22\x0d\x0a\x20\x20\ +\x20\x20\x20\x79\x31\x3d\x22\x31\x36\x22\x0d\x0a\x20\x20\x20\x20\ +\x20\x78\x31\x3d\x22\x31\x32\x22\x20\x2f\x3e\x0d\x0a\x3c\x2f\x73\ +\x76\x67\x3e\x0d\x0a\ \x00\x00\x01\x90\ \x3c\ \x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ @@ -918,233 +969,239 @@ \x20\x79\x31\x3d\x22\x39\x22\x20\x78\x32\x3d\x22\x31\x38\x22\x20\ \x79\x32\x3d\x22\x31\x35\x22\x3e\x3c\x2f\x6c\x69\x6e\x65\x3e\x3c\ \x2f\x73\x76\x67\x3e\ -\x00\x00\x07\x9d\ +\x00\x00\x07\xe6\ \x3c\ \x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ \x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ \x2d\x38\x22\x20\x73\x74\x61\x6e\x64\x61\x6c\x6f\x6e\x65\x3d\x22\ -\x6e\x6f\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x0a\x20\x20\x20\x78\x6d\ -\x6c\x6e\x73\x3a\x64\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\ -\x75\x72\x6c\x2e\x6f\x72\x67\x2f\x64\x63\x2f\x65\x6c\x65\x6d\x65\ -\x6e\x74\x73\x2f\x31\x2e\x31\x2f\x22\x0a\x20\x20\x20\x78\x6d\x6c\ -\x6e\x73\x3a\x63\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x63\x72\ -\x65\x61\x74\x69\x76\x65\x63\x6f\x6d\x6d\x6f\x6e\x73\x2e\x6f\x72\ -\x67\x2f\x6e\x73\x23\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\ -\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\ -\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\x2f\x32\ -\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\x73\x23\ -\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x76\x67\x3d\x22\ -\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\ -\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x0a\x20\x20\x20\x78\ -\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\ -\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\ -\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x6f\x64\x69\x70\ -\x6f\x64\x69\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x73\x6f\x64\x69\ -\x70\x6f\x64\x69\x2e\x73\x6f\x75\x72\x63\x65\x66\x6f\x72\x67\x65\ -\x2e\x6e\x65\x74\x2f\x44\x54\x44\x2f\x73\x6f\x64\x69\x70\x6f\x64\ -\x69\x2d\x30\x2e\x64\x74\x64\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\ -\x73\x3a\x69\x6e\x6b\x73\x63\x61\x70\x65\x3d\x22\x68\x74\x74\x70\ -\x3a\x2f\x2f\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\x2e\ -\x6f\x72\x67\x2f\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x73\x2f\x69\ -\x6e\x6b\x73\x63\x61\x70\x65\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\ -\x63\x61\x70\x65\x3a\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ -\x30\x72\x63\x31\x20\x28\x30\x39\x39\x36\x30\x64\x36\x66\x30\x35\ -\x2c\x20\x32\x30\x32\x30\x2d\x30\x34\x2d\x30\x39\x29\x22\x0a\x20\ -\x20\x20\x73\x6f\x64\x69\x70\x6f\x64\x69\x3a\x64\x6f\x63\x6e\x61\ -\x6d\x65\x3d\x22\x61\x6c\x65\x72\x74\x2d\x6f\x63\x74\x61\x67\x6f\ -\x6e\x2d\x72\x65\x64\x2e\x73\x76\x67\x22\x0a\x20\x20\x20\x69\x64\ -\x3d\x22\x73\x76\x67\x38\x22\x0a\x20\x20\x20\x76\x65\x72\x73\x69\ -\x6f\x6e\x3d\x22\x31\x2e\x31\x22\x0a\x20\x20\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x66\x65\x61\x74\x68\x65\x72\x20\x66\x65\x61\x74\x68\ -\x65\x72\x2d\x61\x6c\x65\x72\x74\x2d\x6f\x63\x74\x61\x67\x6f\x6e\ -\x22\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\ -\x6a\x6f\x69\x6e\x3d\x22\x72\x6f\x75\x6e\x64\x22\x0a\x20\x20\x20\ -\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3d\x22\ -\x72\x6f\x75\x6e\x64\x22\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\ -\x2d\x77\x69\x64\x74\x68\x3d\x22\x32\x22\x0a\x20\x20\x20\x73\x74\ -\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\x6c\ -\x6f\x72\x22\x0a\x20\x20\x20\x66\x69\x6c\x6c\x3d\x22\x6e\x6f\x6e\ -\x65\x22\x0a\x20\x20\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\ -\x20\x30\x20\x32\x34\x20\x32\x34\x22\x0a\x20\x20\x20\x68\x65\x69\ -\x67\x68\x74\x3d\x22\x32\x34\x22\x0a\x20\x20\x20\x77\x69\x64\x74\ -\x68\x3d\x22\x32\x34\x22\x3e\x0a\x20\x20\x3c\x6d\x65\x74\x61\x64\ -\x61\x74\x61\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x6d\x65\x74\ -\x61\x64\x61\x74\x61\x31\x34\x22\x3e\x0a\x20\x20\x20\x20\x3c\x72\ -\x64\x66\x3a\x52\x44\x46\x3e\x0a\x20\x20\x20\x20\x20\x20\x3c\x63\ -\x63\x3a\x57\x6f\x72\x6b\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\ -\x72\x64\x66\x3a\x61\x62\x6f\x75\x74\x3d\x22\x22\x3e\x0a\x20\x20\ -\x20\x20\x20\x20\x20\x20\x3c\x64\x63\x3a\x66\x6f\x72\x6d\x61\x74\ -\x3e\x69\x6d\x61\x67\x65\x2f\x73\x76\x67\x2b\x78\x6d\x6c\x3c\x2f\ -\x64\x63\x3a\x66\x6f\x72\x6d\x61\x74\x3e\x0a\x20\x20\x20\x20\x20\ -\x20\x20\x20\x3c\x64\x63\x3a\x74\x79\x70\x65\x0a\x20\x20\x20\x20\ -\x20\x20\x20\x20\x20\x20\x20\x72\x64\x66\x3a\x72\x65\x73\x6f\x75\ -\x72\x63\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\x75\x72\x6c\ -\x2e\x6f\x72\x67\x2f\x64\x63\x2f\x64\x63\x6d\x69\x74\x79\x70\x65\ -\x2f\x53\x74\x69\x6c\x6c\x49\x6d\x61\x67\x65\x22\x20\x2f\x3e\x0a\ -\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x64\x63\x3a\x74\x69\x74\x6c\ -\x65\x3e\x3c\x2f\x64\x63\x3a\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\ -\x20\x20\x20\x20\x3c\x2f\x63\x63\x3a\x57\x6f\x72\x6b\x3e\x0a\x20\ -\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\x0a\x20\x20\ -\x3c\x2f\x6d\x65\x74\x61\x64\x61\x74\x61\x3e\x0a\x20\x20\x3c\x64\ -\x65\x66\x73\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x64\x65\x66\ -\x73\x31\x32\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x73\x6f\x64\x69\x70\ -\x6f\x64\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x0a\x20\x20\ -\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x75\x72\x72\ -\x65\x6e\x74\x2d\x6c\x61\x79\x65\x72\x3d\x22\x73\x76\x67\x38\x22\ -\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\ -\x69\x6e\x64\x6f\x77\x2d\x6d\x61\x78\x69\x6d\x69\x7a\x65\x64\x3d\ -\x22\x30\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ -\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x79\x3d\x22\x32\x38\x31\x22\ -\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\ -\x69\x6e\x64\x6f\x77\x2d\x78\x3d\x22\x31\x34\x33\x33\x22\x0a\x20\ -\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x79\x3d\ -\x22\x31\x32\x2e\x34\x30\x39\x37\x39\x37\x22\x0a\x20\x20\x20\x20\ -\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x78\x3d\x22\x31\x32\ -\x2e\x38\x32\x38\x32\x31\x36\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\ -\x6b\x73\x63\x61\x70\x65\x3a\x7a\x6f\x6f\x6d\x3d\x22\x32\x31\x22\ -\x0a\x20\x20\x20\x20\x20\x73\x68\x6f\x77\x67\x72\x69\x64\x3d\x22\ -\x66\x61\x6c\x73\x65\x22\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\ -\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x31\x30\x22\x0a\x20\x20\x20\ -\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\ -\x77\x2d\x68\x65\x69\x67\x68\x74\x3d\x22\x39\x35\x39\x22\x0a\x20\ -\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\ -\x64\x6f\x77\x2d\x77\x69\x64\x74\x68\x3d\x22\x31\x35\x34\x32\x22\ +\x6e\x6f\x22\x3f\x3e\x0d\x0a\x3c\x73\x76\x67\x0d\x0a\x20\x20\x20\ +\x78\x6d\x6c\x6e\x73\x3a\x64\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\ +\x2f\x70\x75\x72\x6c\x2e\x6f\x72\x67\x2f\x64\x63\x2f\x65\x6c\x65\ +\x6d\x65\x6e\x74\x73\x2f\x31\x2e\x31\x2f\x22\x0d\x0a\x20\x20\x20\ +\x78\x6d\x6c\x6e\x73\x3a\x63\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\ +\x2f\x63\x72\x65\x61\x74\x69\x76\x65\x63\x6f\x6d\x6d\x6f\x6e\x73\ +\x2e\x6f\x72\x67\x2f\x6e\x73\x23\x22\x0d\x0a\x20\x20\x20\x78\x6d\ +\x6c\x6e\x73\x3a\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\ +\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\ +\x30\x32\x2f\x32\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\ +\x2d\x6e\x73\x23\x22\x0d\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\ +\x73\x76\x67\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\ +\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\ +\x0d\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ +\x30\x30\x2f\x73\x76\x67\x22\x0d\x0a\x20\x20\x20\x78\x6d\x6c\x6e\ +\x73\x3a\x73\x6f\x64\x69\x70\x6f\x64\x69\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x73\x6f\x64\x69\x70\x6f\x64\x69\x2e\x73\x6f\x75\x72\ +\x63\x65\x66\x6f\x72\x67\x65\x2e\x6e\x65\x74\x2f\x44\x54\x44\x2f\ +\x73\x6f\x64\x69\x70\x6f\x64\x69\x2d\x30\x2e\x64\x74\x64\x22\x0d\ +\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x69\x6e\x6b\x73\x63\x61\ +\x70\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x69\ +\x6e\x6b\x73\x63\x61\x70\x65\x2e\x6f\x72\x67\x2f\x6e\x61\x6d\x65\ +\x73\x70\x61\x63\x65\x73\x2f\x69\x6e\x6b\x73\x63\x61\x70\x65\x22\ +\x0d\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x76\x65\ +\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x30\x72\x63\x31\x20\x28\x30\ +\x39\x39\x36\x30\x64\x36\x66\x30\x35\x2c\x20\x32\x30\x32\x30\x2d\ +\x30\x34\x2d\x30\x39\x29\x22\x0d\x0a\x20\x20\x20\x73\x6f\x64\x69\ +\x70\x6f\x64\x69\x3a\x64\x6f\x63\x6e\x61\x6d\x65\x3d\x22\x61\x6c\ +\x65\x72\x74\x2d\x6f\x63\x74\x61\x67\x6f\x6e\x2d\x72\x65\x64\x2e\ +\x73\x76\x67\x22\x0d\x0a\x20\x20\x20\x69\x64\x3d\x22\x73\x76\x67\ +\x38\x22\x0d\x0a\x20\x20\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\ +\x31\x2e\x31\x22\x0d\x0a\x20\x20\x20\x63\x6c\x61\x73\x73\x3d\x22\ +\x66\x65\x61\x74\x68\x65\x72\x20\x66\x65\x61\x74\x68\x65\x72\x2d\ +\x61\x6c\x65\x72\x74\x2d\x6f\x63\x74\x61\x67\x6f\x6e\x22\x0d\x0a\ +\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x6a\x6f\ +\x69\x6e\x3d\x22\x72\x6f\x75\x6e\x64\x22\x0d\x0a\x20\x20\x20\x73\ +\x74\x72\x6f\x6b\x65\x2d\x6c\x69\x6e\x65\x63\x61\x70\x3d\x22\x72\ +\x6f\x75\x6e\x64\x22\x0d\x0a\x20\x20\x20\x73\x74\x72\x6f\x6b\x65\ +\x2d\x77\x69\x64\x74\x68\x3d\x22\x32\x22\x0d\x0a\x20\x20\x20\x73\ +\x74\x72\x6f\x6b\x65\x3d\x22\x63\x75\x72\x72\x65\x6e\x74\x43\x6f\ +\x6c\x6f\x72\x22\x0d\x0a\x20\x20\x20\x66\x69\x6c\x6c\x3d\x22\x6e\ +\x6f\x6e\x65\x22\x0d\x0a\x20\x20\x20\x76\x69\x65\x77\x42\x6f\x78\ +\x3d\x22\x30\x20\x30\x20\x32\x34\x20\x32\x34\x22\x0d\x0a\x20\x20\ +\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x32\x34\x22\x0d\x0a\x20\x20\ +\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x34\x22\x3e\x0d\x0a\x20\x20\ +\x3c\x6d\x65\x74\x61\x64\x61\x74\x61\x0d\x0a\x20\x20\x20\x20\x20\ +\x69\x64\x3d\x22\x6d\x65\x74\x61\x64\x61\x74\x61\x31\x34\x22\x3e\ +\x0d\x0a\x20\x20\x20\x20\x3c\x72\x64\x66\x3a\x52\x44\x46\x3e\x0d\ +\x0a\x20\x20\x20\x20\x20\x20\x3c\x63\x63\x3a\x57\x6f\x72\x6b\x0d\ +\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x72\x64\x66\x3a\x61\x62\ +\x6f\x75\x74\x3d\x22\x22\x3e\x0d\x0a\x20\x20\x20\x20\x20\x20\x20\ +\x20\x3c\x64\x63\x3a\x66\x6f\x72\x6d\x61\x74\x3e\x69\x6d\x61\x67\ +\x65\x2f\x73\x76\x67\x2b\x78\x6d\x6c\x3c\x2f\x64\x63\x3a\x66\x6f\ +\x72\x6d\x61\x74\x3e\x0d\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\ +\x64\x63\x3a\x74\x79\x70\x65\x0d\x0a\x20\x20\x20\x20\x20\x20\x20\ +\x20\x20\x20\x20\x72\x64\x66\x3a\x72\x65\x73\x6f\x75\x72\x63\x65\ +\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\x75\x72\x6c\x2e\x6f\x72\ +\x67\x2f\x64\x63\x2f\x64\x63\x6d\x69\x74\x79\x70\x65\x2f\x53\x74\ +\x69\x6c\x6c\x49\x6d\x61\x67\x65\x22\x20\x2f\x3e\x0d\x0a\x20\x20\ +\x20\x20\x20\x20\x20\x20\x3c\x64\x63\x3a\x74\x69\x74\x6c\x65\x3e\ +\x3c\x2f\x64\x63\x3a\x74\x69\x74\x6c\x65\x3e\x0d\x0a\x20\x20\x20\ +\x20\x20\x20\x3c\x2f\x63\x63\x3a\x57\x6f\x72\x6b\x3e\x0d\x0a\x20\ +\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\x0d\x0a\x20\ +\x20\x3c\x2f\x6d\x65\x74\x61\x64\x61\x74\x61\x3e\x0d\x0a\x20\x20\ +\x3c\x64\x65\x66\x73\x0d\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\ +\x64\x65\x66\x73\x31\x32\x22\x20\x2f\x3e\x0d\x0a\x20\x20\x3c\x73\ +\x6f\x64\x69\x70\x6f\x64\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\ +\x77\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\ +\x3a\x63\x75\x72\x72\x65\x6e\x74\x2d\x6c\x61\x79\x65\x72\x3d\x22\ +\x73\x76\x67\x38\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\ +\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x6d\x61\x78\x69\ +\x6d\x69\x7a\x65\x64\x3d\x22\x30\x22\x0d\x0a\x20\x20\x20\x20\x20\ +\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\ +\x79\x3d\x22\x32\x38\x31\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\ +\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x78\x3d\ +\x22\x31\x34\x33\x33\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\ +\x73\x63\x61\x70\x65\x3a\x63\x79\x3d\x22\x31\x32\x2e\x34\x30\x39\ +\x37\x39\x37\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\ +\x61\x70\x65\x3a\x63\x78\x3d\x22\x31\x32\x2e\x38\x32\x38\x32\x31\ +\x36\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ +\x65\x3a\x7a\x6f\x6f\x6d\x3d\x22\x32\x31\x22\x0d\x0a\x20\x20\x20\ +\x20\x20\x73\x68\x6f\x77\x67\x72\x69\x64\x3d\x22\x66\x61\x6c\x73\ +\x65\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x6e\x61\x6d\ +\x65\x64\x76\x69\x65\x77\x31\x30\x22\x0d\x0a\x20\x20\x20\x20\x20\ +\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\ +\x68\x65\x69\x67\x68\x74\x3d\x22\x39\x35\x39\x22\x0d\x0a\x20\x20\ +\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\ +\x6f\x77\x2d\x77\x69\x64\x74\x68\x3d\x22\x31\x35\x34\x32\x22\x0d\ \x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x70\ -\x61\x67\x65\x73\x68\x61\x64\x6f\x77\x3d\x22\x32\x22\x0a\x20\x20\ -\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x70\x61\x67\x65\ -\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\x22\x0a\x20\x20\x20\x20\ -\x20\x67\x75\x69\x64\x65\x74\x6f\x6c\x65\x72\x61\x6e\x63\x65\x3d\ -\x22\x31\x30\x22\x0a\x20\x20\x20\x20\x20\x67\x72\x69\x64\x74\x6f\ -\x6c\x65\x72\x61\x6e\x63\x65\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\ -\x20\x20\x6f\x62\x6a\x65\x63\x74\x74\x6f\x6c\x65\x72\x61\x6e\x63\ -\x65\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x20\x20\x62\x6f\x72\x64\ -\x65\x72\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x31\x22\x0a\x20\x20\ -\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x63\x6f\x6c\x6f\x72\x3d\x22\ -\x23\x36\x36\x36\x36\x36\x36\x22\x0a\x20\x20\x20\x20\x20\x70\x61\ -\x67\x65\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x66\x66\x66\x66\x66\x66\ -\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x70\x6f\x6c\x79\x67\x6f\x6e\x0a\ -\x20\x20\x20\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\ -\x3a\x23\x66\x66\x30\x30\x30\x30\x22\x0a\x20\x20\x20\x20\x20\x69\ -\x64\x3d\x22\x70\x6f\x6c\x79\x67\x6f\x6e\x32\x22\x0a\x20\x20\x20\ -\x20\x20\x70\x6f\x69\x6e\x74\x73\x3d\x22\x37\x2e\x38\x36\x20\x32\ -\x20\x31\x36\x2e\x31\x34\x20\x32\x20\x32\x32\x20\x37\x2e\x38\x36\ -\x20\x32\x32\x20\x31\x36\x2e\x31\x34\x20\x31\x36\x2e\x31\x34\x20\ -\x32\x32\x20\x37\x2e\x38\x36\x20\x32\x32\x20\x32\x20\x31\x36\x2e\ -\x31\x34\x20\x32\x20\x37\x2e\x38\x36\x20\x37\x2e\x38\x36\x20\x32\ -\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x6c\x69\x6e\x65\x0a\x20\x20\x20\ -\x20\x20\x69\x64\x3d\x22\x6c\x69\x6e\x65\x34\x22\x0a\x20\x20\x20\ -\x20\x20\x79\x32\x3d\x22\x31\x32\x22\x0a\x20\x20\x20\x20\x20\x78\ -\x32\x3d\x22\x31\x32\x22\x0a\x20\x20\x20\x20\x20\x79\x31\x3d\x22\ -\x38\x22\x0a\x20\x20\x20\x20\x20\x78\x31\x3d\x22\x31\x32\x22\x20\ -\x2f\x3e\x0a\x20\x20\x3c\x6c\x69\x6e\x65\x0a\x20\x20\x20\x20\x20\ -\x69\x64\x3d\x22\x6c\x69\x6e\x65\x36\x22\x0a\x20\x20\x20\x20\x20\ -\x79\x32\x3d\x22\x31\x36\x22\x0a\x20\x20\x20\x20\x20\x78\x32\x3d\ -\x22\x31\x32\x2e\x30\x31\x22\x0a\x20\x20\x20\x20\x20\x79\x31\x3d\ -\x22\x31\x36\x22\x0a\x20\x20\x20\x20\x20\x78\x31\x3d\x22\x31\x32\ -\x22\x20\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\x0a\ -\x00\x00\x06\x4c\ +\x61\x67\x65\x73\x68\x61\x64\x6f\x77\x3d\x22\x32\x22\x0d\x0a\x20\ +\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x70\x61\x67\ +\x65\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\x22\x0d\x0a\x20\x20\ +\x20\x20\x20\x67\x75\x69\x64\x65\x74\x6f\x6c\x65\x72\x61\x6e\x63\ +\x65\x3d\x22\x31\x30\x22\x0d\x0a\x20\x20\x20\x20\x20\x67\x72\x69\ +\x64\x74\x6f\x6c\x65\x72\x61\x6e\x63\x65\x3d\x22\x31\x30\x22\x0d\ +\x0a\x20\x20\x20\x20\x20\x6f\x62\x6a\x65\x63\x74\x74\x6f\x6c\x65\ +\x72\x61\x6e\x63\x65\x3d\x22\x31\x30\x22\x0d\x0a\x20\x20\x20\x20\ +\x20\x62\x6f\x72\x64\x65\x72\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\ +\x31\x22\x0d\x0a\x20\x20\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x63\ +\x6f\x6c\x6f\x72\x3d\x22\x23\x36\x36\x36\x36\x36\x36\x22\x0d\x0a\ +\x20\x20\x20\x20\x20\x70\x61\x67\x65\x63\x6f\x6c\x6f\x72\x3d\x22\ +\x23\x66\x66\x66\x66\x66\x66\x22\x20\x2f\x3e\x0d\x0a\x20\x20\x3c\ +\x70\x6f\x6c\x79\x67\x6f\x6e\x0d\x0a\x20\x20\x20\x20\x20\x73\x74\ +\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\x3a\x23\x66\x66\x30\x30\x30\ +\x30\x22\x0d\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x70\x6f\x6c\ +\x79\x67\x6f\x6e\x32\x22\x0d\x0a\x20\x20\x20\x20\x20\x70\x6f\x69\ +\x6e\x74\x73\x3d\x22\x37\x2e\x38\x36\x20\x32\x20\x31\x36\x2e\x31\ +\x34\x20\x32\x20\x32\x32\x20\x37\x2e\x38\x36\x20\x32\x32\x20\x31\ +\x36\x2e\x31\x34\x20\x31\x36\x2e\x31\x34\x20\x32\x32\x20\x37\x2e\ +\x38\x36\x20\x32\x32\x20\x32\x20\x31\x36\x2e\x31\x34\x20\x32\x20\ +\x37\x2e\x38\x36\x20\x37\x2e\x38\x36\x20\x32\x22\x20\x2f\x3e\x0d\ +\x0a\x20\x20\x3c\x6c\x69\x6e\x65\x0d\x0a\x20\x20\x20\x20\x20\x69\ +\x64\x3d\x22\x6c\x69\x6e\x65\x34\x22\x0d\x0a\x20\x20\x20\x20\x20\ +\x79\x32\x3d\x22\x31\x32\x22\x0d\x0a\x20\x20\x20\x20\x20\x78\x32\ +\x3d\x22\x31\x32\x22\x0d\x0a\x20\x20\x20\x20\x20\x79\x31\x3d\x22\ +\x38\x22\x0d\x0a\x20\x20\x20\x20\x20\x78\x31\x3d\x22\x31\x32\x22\ +\x20\x2f\x3e\x0d\x0a\x20\x20\x3c\x6c\x69\x6e\x65\x0d\x0a\x20\x20\ +\x20\x20\x20\x69\x64\x3d\x22\x6c\x69\x6e\x65\x36\x22\x0d\x0a\x20\ +\x20\x20\x20\x20\x79\x32\x3d\x22\x31\x36\x22\x0d\x0a\x20\x20\x20\ +\x20\x20\x78\x32\x3d\x22\x31\x32\x2e\x30\x31\x22\x0d\x0a\x20\x20\ +\x20\x20\x20\x79\x31\x3d\x22\x31\x36\x22\x0d\x0a\x20\x20\x20\x20\ +\x20\x78\x31\x3d\x22\x31\x32\x22\x20\x2f\x3e\x0d\x0a\x3c\x2f\x73\ +\x76\x67\x3e\x0d\x0a\ +\x00\x00\x06\x53\ \x3c\ \x73\x76\x67\x20\x77\x69\x64\x74\x68\x3d\x22\x38\x30\x22\x20\x68\ \x65\x69\x67\x68\x74\x3d\x22\x38\x30\x22\x20\x78\x6d\x6c\x6e\x73\ \x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\ \x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x20\x66\x69\ -\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x3e\x0a\x20\x3c\x67\x3e\x0a\ -\x20\x20\x3c\x74\x69\x74\x6c\x65\x3e\x4c\x61\x79\x65\x72\x20\x31\ -\x3c\x2f\x74\x69\x74\x6c\x65\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\ -\x20\x66\x69\x6c\x6c\x2d\x72\x75\x6c\x65\x3d\x22\x65\x76\x65\x6e\ -\x6f\x64\x64\x22\x20\x63\x6c\x69\x70\x2d\x72\x75\x6c\x65\x3d\x22\ -\x65\x76\x65\x6e\x6f\x64\x64\x22\x20\x64\x3d\x22\x6d\x33\x33\x2e\ -\x39\x32\x35\x36\x2c\x39\x2e\x36\x39\x33\x32\x34\x63\x2d\x30\x2e\ -\x37\x36\x30\x38\x2c\x30\x20\x2d\x31\x2e\x34\x38\x30\x32\x2c\x30\ -\x2e\x33\x34\x36\x34\x33\x20\x2d\x31\x2e\x39\x35\x34\x35\x2c\x30\ -\x2e\x39\x34\x31\x32\x33\x6c\x2d\x35\x2e\x33\x32\x31\x2c\x36\x2e\ -\x36\x37\x32\x33\x6c\x2d\x31\x31\x2e\x36\x35\x30\x31\x2c\x30\x63\ -\x2d\x31\x2e\x33\x38\x30\x37\x2c\x30\x20\x2d\x32\x2e\x35\x2c\x31\ -\x2e\x31\x31\x39\x33\x20\x2d\x32\x2e\x35\x2c\x32\x2e\x35\x63\x30\ -\x2c\x31\x2e\x33\x38\x30\x37\x20\x31\x2e\x31\x31\x39\x33\x2c\x32\ -\x2e\x35\x20\x32\x2e\x35\x2c\x32\x2e\x35\x6c\x31\x2e\x35\x2c\x30\ -\x6c\x30\x2c\x34\x31\x2e\x35\x63\x30\x2c\x33\x2e\x35\x38\x39\x39\ -\x20\x32\x2e\x39\x31\x30\x31\x2c\x36\x2e\x35\x20\x36\x2e\x35\x2c\ -\x36\x2e\x35\x6c\x33\x34\x2c\x30\x63\x33\x2e\x35\x38\x39\x38\x2c\ -\x30\x20\x36\x2e\x35\x2c\x2d\x32\x2e\x39\x31\x30\x31\x20\x36\x2e\ -\x35\x2c\x2d\x36\x2e\x35\x6c\x30\x2c\x2d\x34\x31\x2e\x35\x6c\x31\ -\x2e\x35\x2c\x30\x63\x31\x2e\x33\x38\x30\x37\x2c\x30\x20\x32\x2e\ -\x35\x2c\x2d\x31\x2e\x31\x31\x39\x33\x20\x32\x2e\x35\x2c\x2d\x32\ -\x2e\x35\x63\x30\x2c\x2d\x31\x2e\x33\x38\x30\x37\x20\x2d\x31\x2e\ -\x31\x31\x39\x33\x2c\x2d\x32\x2e\x35\x20\x2d\x32\x2e\x35\x2c\x2d\ -\x32\x2e\x35\x6c\x2d\x31\x31\x2e\x36\x35\x30\x31\x2c\x30\x6c\x2d\ -\x35\x2e\x33\x32\x30\x39\x2c\x2d\x36\x2e\x36\x37\x32\x32\x63\x2d\ -\x30\x2e\x34\x37\x34\x34\x2c\x2d\x30\x2e\x35\x39\x34\x39\x20\x2d\ -\x31\x2e\x31\x39\x33\x38\x2c\x2d\x30\x2e\x39\x34\x31\x33\x33\x20\ -\x2d\x31\x2e\x39\x35\x34\x36\x2c\x2d\x30\x2e\x39\x34\x31\x33\x33\ -\x6c\x2d\x31\x32\x2e\x31\x34\x38\x38\x2c\x30\x7a\x6d\x2d\x30\x2e\ -\x39\x32\x35\x36\x2c\x31\x37\x2e\x36\x31\x33\x35\x33\x63\x31\x2e\ -\x33\x38\x30\x37\x2c\x30\x20\x32\x2e\x35\x2c\x31\x2e\x31\x31\x39\ -\x33\x20\x32\x2e\x35\x2c\x32\x2e\x35\x6c\x30\x2c\x32\x38\x63\x30\ -\x2c\x31\x2e\x33\x38\x30\x37\x20\x2d\x31\x2e\x31\x31\x39\x33\x2c\ -\x32\x2e\x35\x20\x2d\x32\x2e\x35\x2c\x32\x2e\x35\x63\x2d\x31\x2e\ -\x33\x38\x30\x37\x2c\x30\x20\x2d\x32\x2e\x35\x2c\x2d\x31\x2e\x31\ -\x31\x39\x33\x20\x2d\x32\x2e\x35\x2c\x2d\x32\x2e\x35\x6c\x30\x2c\ -\x2d\x32\x38\x63\x30\x2c\x2d\x31\x2e\x33\x38\x30\x37\x20\x31\x2e\ -\x31\x31\x39\x33\x2c\x2d\x32\x2e\x35\x20\x32\x2e\x35\x2c\x2d\x32\ -\x2e\x35\x7a\x6d\x31\x36\x2e\x35\x2c\x32\x2e\x35\x63\x30\x2c\x2d\ -\x31\x2e\x33\x38\x30\x37\x20\x2d\x31\x2e\x31\x31\x39\x33\x2c\x2d\ -\x32\x2e\x35\x20\x2d\x32\x2e\x35\x2c\x2d\x32\x2e\x35\x63\x2d\x31\ -\x2e\x33\x38\x30\x37\x2c\x30\x20\x2d\x32\x2e\x35\x2c\x31\x2e\x31\ -\x31\x39\x33\x20\x2d\x32\x2e\x35\x2c\x32\x2e\x35\x6c\x30\x2c\x32\ -\x38\x63\x30\x2c\x31\x2e\x33\x38\x30\x37\x20\x31\x2e\x31\x31\x39\ -\x33\x2c\x32\x2e\x35\x20\x32\x2e\x35\x2c\x32\x2e\x35\x63\x31\x2e\ -\x33\x38\x30\x37\x2c\x30\x20\x32\x2e\x35\x2c\x2d\x31\x2e\x31\x31\ -\x39\x33\x20\x32\x2e\x35\x2c\x2d\x32\x2e\x35\x6c\x30\x2c\x2d\x32\ -\x38\x7a\x6d\x2d\x32\x2e\x35\x34\x36\x34\x2c\x2d\x31\x32\x2e\x35\ -\x30\x31\x34\x6c\x2d\x32\x2e\x30\x38\x33\x32\x2c\x2d\x32\x2e\x36\ -\x31\x32\x31\x6c\x2d\x39\x2e\x37\x34\x30\x38\x2c\x30\x6c\x2d\x32\ -\x2e\x30\x38\x33\x32\x2c\x32\x2e\x36\x31\x32\x31\x6c\x31\x33\x2e\ -\x39\x30\x37\x32\x2c\x30\x7a\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\ -\x43\x32\x43\x43\x44\x45\x22\x20\x69\x64\x3d\x22\x73\x76\x67\x5f\ -\x31\x22\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\x20\x66\x69\x6c\ -\x6c\x3d\x22\x23\x66\x66\x30\x30\x30\x30\x22\x20\x64\x3d\x22\x6d\ -\x37\x2e\x33\x33\x38\x33\x2c\x33\x39\x2e\x39\x39\x39\x39\x34\x6c\ -\x30\x2c\x30\x63\x30\x2c\x2d\x31\x38\x2e\x30\x38\x35\x33\x35\x20\ -\x31\x34\x2e\x36\x32\x33\x31\x36\x2c\x2d\x33\x32\x2e\x37\x34\x36\ -\x35\x31\x20\x33\x32\x2e\x36\x36\x31\x36\x37\x2c\x2d\x33\x32\x2e\ -\x37\x34\x36\x35\x31\x6c\x30\x2c\x30\x63\x38\x2e\x36\x36\x32\x35\ -\x33\x2c\x30\x20\x31\x36\x2e\x39\x37\x30\x32\x35\x2c\x33\x2e\x34\ -\x35\x30\x30\x38\x20\x32\x33\x2e\x30\x39\x35\x33\x32\x2c\x39\x2e\ -\x35\x39\x31\x32\x36\x63\x36\x2e\x31\x32\x35\x33\x31\x2c\x36\x2e\ -\x31\x34\x31\x31\x39\x20\x39\x2e\x35\x36\x36\x34\x32\x2c\x31\x34\ -\x2e\x34\x37\x30\x34\x34\x20\x39\x2e\x35\x36\x36\x34\x32\x2c\x32\ -\x33\x2e\x31\x35\x35\x32\x34\x6c\x30\x2c\x30\x63\x30\x2c\x31\x38\ -\x2e\x30\x38\x35\x35\x36\x20\x2d\x31\x34\x2e\x36\x32\x33\x30\x35\ -\x2c\x33\x32\x2e\x37\x34\x36\x36\x33\x20\x2d\x33\x32\x2e\x36\x36\ -\x31\x37\x33\x2c\x33\x32\x2e\x37\x34\x36\x36\x33\x6c\x30\x2c\x30\ -\x63\x2d\x31\x38\x2e\x30\x33\x38\x35\x31\x2c\x30\x20\x2d\x33\x32\ -\x2e\x36\x36\x31\x36\x37\x2c\x2d\x31\x34\x2e\x36\x36\x31\x30\x36\ -\x20\x2d\x33\x32\x2e\x36\x36\x31\x36\x37\x2c\x2d\x33\x32\x2e\x37\ -\x34\x36\x36\x33\x63\x30\x2c\x30\x20\x30\x2c\x30\x20\x30\x2c\x30\ -\x6c\x2d\x30\x2e\x30\x30\x30\x30\x31\x2c\x30\x7a\x6d\x35\x32\x2e\ -\x37\x34\x31\x32\x31\x2c\x31\x34\x2e\x36\x34\x39\x31\x36\x6c\x30\ -\x2c\x30\x63\x37\x2e\x31\x39\x31\x30\x32\x2c\x2d\x39\x2e\x39\x30\ -\x38\x33\x34\x20\x36\x2e\x31\x32\x32\x30\x39\x2c\x2d\x32\x33\x2e\ -\x35\x38\x39\x34\x38\x20\x2d\x32\x2e\x35\x32\x30\x31\x35\x2c\x2d\ -\x33\x32\x2e\x32\x35\x34\x30\x37\x63\x2d\x38\x2e\x36\x34\x32\x32\ -\x35\x2c\x2d\x38\x2e\x36\x36\x34\x36\x36\x20\x2d\x32\x32\x2e\x32\ -\x38\x37\x39\x33\x2c\x2d\x39\x2e\x37\x33\x36\x33\x37\x20\x2d\x33\ -\x32\x2e\x31\x37\x30\x33\x33\x2c\x2d\x32\x2e\x35\x32\x36\x35\x38\ -\x6c\x33\x34\x2e\x36\x39\x30\x34\x37\x2c\x33\x34\x2e\x37\x38\x30\ -\x36\x35\x6c\x30\x2e\x30\x30\x30\x30\x31\x2c\x30\x7a\x6d\x2d\x34\ -\x30\x2e\x31\x35\x38\x38\x39\x2c\x2d\x32\x39\x2e\x32\x39\x38\x30\ -\x31\x63\x2d\x37\x2e\x31\x39\x31\x30\x37\x2c\x39\x2e\x39\x30\x38\ -\x32\x38\x20\x2d\x36\x2e\x31\x32\x32\x31\x37\x2c\x32\x33\x2e\x35\ -\x38\x39\x34\x33\x20\x32\x2e\x35\x32\x30\x30\x32\x2c\x33\x32\x2e\ -\x32\x35\x33\x39\x63\x38\x2e\x36\x34\x32\x31\x37\x2c\x38\x2e\x36\ -\x36\x34\x37\x31\x20\x32\x32\x2e\x32\x38\x37\x38\x35\x2c\x39\x2e\ -\x37\x33\x36\x34\x31\x20\x33\x32\x2e\x31\x37\x30\x32\x35\x2c\x32\ -\x2e\x35\x32\x36\x37\x6c\x2d\x33\x34\x2e\x36\x39\x30\x32\x38\x2c\ -\x2d\x33\x34\x2e\x37\x38\x30\x35\x39\x6c\x30\x2c\x30\x6c\x30\x2e\ -\x30\x30\x30\x30\x31\x2c\x2d\x30\x2e\x30\x30\x30\x30\x31\x7a\x22\ -\x20\x69\x64\x3d\x22\x73\x76\x67\x5f\x33\x22\x2f\x3e\x0a\x20\x3c\ -\x2f\x67\x3e\x0a\x0a\x3c\x2f\x73\x76\x67\x3e\ +\x6c\x6c\x3d\x22\x6e\x6f\x6e\x65\x22\x3e\x0d\x0a\x20\x3c\x67\x3e\ +\x0d\x0a\x20\x20\x3c\x74\x69\x74\x6c\x65\x3e\x4c\x61\x79\x65\x72\ +\x20\x31\x3c\x2f\x74\x69\x74\x6c\x65\x3e\x0d\x0a\x20\x20\x3c\x70\ +\x61\x74\x68\x20\x66\x69\x6c\x6c\x2d\x72\x75\x6c\x65\x3d\x22\x65\ +\x76\x65\x6e\x6f\x64\x64\x22\x20\x63\x6c\x69\x70\x2d\x72\x75\x6c\ +\x65\x3d\x22\x65\x76\x65\x6e\x6f\x64\x64\x22\x20\x64\x3d\x22\x6d\ +\x33\x33\x2e\x39\x32\x35\x36\x2c\x39\x2e\x36\x39\x33\x32\x34\x63\ +\x2d\x30\x2e\x37\x36\x30\x38\x2c\x30\x20\x2d\x31\x2e\x34\x38\x30\ +\x32\x2c\x30\x2e\x33\x34\x36\x34\x33\x20\x2d\x31\x2e\x39\x35\x34\ +\x35\x2c\x30\x2e\x39\x34\x31\x32\x33\x6c\x2d\x35\x2e\x33\x32\x31\ +\x2c\x36\x2e\x36\x37\x32\x33\x6c\x2d\x31\x31\x2e\x36\x35\x30\x31\ +\x2c\x30\x63\x2d\x31\x2e\x33\x38\x30\x37\x2c\x30\x20\x2d\x32\x2e\ +\x35\x2c\x31\x2e\x31\x31\x39\x33\x20\x2d\x32\x2e\x35\x2c\x32\x2e\ +\x35\x63\x30\x2c\x31\x2e\x33\x38\x30\x37\x20\x31\x2e\x31\x31\x39\ +\x33\x2c\x32\x2e\x35\x20\x32\x2e\x35\x2c\x32\x2e\x35\x6c\x31\x2e\ +\x35\x2c\x30\x6c\x30\x2c\x34\x31\x2e\x35\x63\x30\x2c\x33\x2e\x35\ +\x38\x39\x39\x20\x32\x2e\x39\x31\x30\x31\x2c\x36\x2e\x35\x20\x36\ +\x2e\x35\x2c\x36\x2e\x35\x6c\x33\x34\x2c\x30\x63\x33\x2e\x35\x38\ +\x39\x38\x2c\x30\x20\x36\x2e\x35\x2c\x2d\x32\x2e\x39\x31\x30\x31\ +\x20\x36\x2e\x35\x2c\x2d\x36\x2e\x35\x6c\x30\x2c\x2d\x34\x31\x2e\ +\x35\x6c\x31\x2e\x35\x2c\x30\x63\x31\x2e\x33\x38\x30\x37\x2c\x30\ +\x20\x32\x2e\x35\x2c\x2d\x31\x2e\x31\x31\x39\x33\x20\x32\x2e\x35\ +\x2c\x2d\x32\x2e\x35\x63\x30\x2c\x2d\x31\x2e\x33\x38\x30\x37\x20\ +\x2d\x31\x2e\x31\x31\x39\x33\x2c\x2d\x32\x2e\x35\x20\x2d\x32\x2e\ +\x35\x2c\x2d\x32\x2e\x35\x6c\x2d\x31\x31\x2e\x36\x35\x30\x31\x2c\ +\x30\x6c\x2d\x35\x2e\x33\x32\x30\x39\x2c\x2d\x36\x2e\x36\x37\x32\ +\x32\x63\x2d\x30\x2e\x34\x37\x34\x34\x2c\x2d\x30\x2e\x35\x39\x34\ +\x39\x20\x2d\x31\x2e\x31\x39\x33\x38\x2c\x2d\x30\x2e\x39\x34\x31\ +\x33\x33\x20\x2d\x31\x2e\x39\x35\x34\x36\x2c\x2d\x30\x2e\x39\x34\ +\x31\x33\x33\x6c\x2d\x31\x32\x2e\x31\x34\x38\x38\x2c\x30\x7a\x6d\ +\x2d\x30\x2e\x39\x32\x35\x36\x2c\x31\x37\x2e\x36\x31\x33\x35\x33\ +\x63\x31\x2e\x33\x38\x30\x37\x2c\x30\x20\x32\x2e\x35\x2c\x31\x2e\ +\x31\x31\x39\x33\x20\x32\x2e\x35\x2c\x32\x2e\x35\x6c\x30\x2c\x32\ +\x38\x63\x30\x2c\x31\x2e\x33\x38\x30\x37\x20\x2d\x31\x2e\x31\x31\ +\x39\x33\x2c\x32\x2e\x35\x20\x2d\x32\x2e\x35\x2c\x32\x2e\x35\x63\ +\x2d\x31\x2e\x33\x38\x30\x37\x2c\x30\x20\x2d\x32\x2e\x35\x2c\x2d\ +\x31\x2e\x31\x31\x39\x33\x20\x2d\x32\x2e\x35\x2c\x2d\x32\x2e\x35\ +\x6c\x30\x2c\x2d\x32\x38\x63\x30\x2c\x2d\x31\x2e\x33\x38\x30\x37\ +\x20\x31\x2e\x31\x31\x39\x33\x2c\x2d\x32\x2e\x35\x20\x32\x2e\x35\ +\x2c\x2d\x32\x2e\x35\x7a\x6d\x31\x36\x2e\x35\x2c\x32\x2e\x35\x63\ +\x30\x2c\x2d\x31\x2e\x33\x38\x30\x37\x20\x2d\x31\x2e\x31\x31\x39\ +\x33\x2c\x2d\x32\x2e\x35\x20\x2d\x32\x2e\x35\x2c\x2d\x32\x2e\x35\ +\x63\x2d\x31\x2e\x33\x38\x30\x37\x2c\x30\x20\x2d\x32\x2e\x35\x2c\ +\x31\x2e\x31\x31\x39\x33\x20\x2d\x32\x2e\x35\x2c\x32\x2e\x35\x6c\ +\x30\x2c\x32\x38\x63\x30\x2c\x31\x2e\x33\x38\x30\x37\x20\x31\x2e\ +\x31\x31\x39\x33\x2c\x32\x2e\x35\x20\x32\x2e\x35\x2c\x32\x2e\x35\ +\x63\x31\x2e\x33\x38\x30\x37\x2c\x30\x20\x32\x2e\x35\x2c\x2d\x31\ +\x2e\x31\x31\x39\x33\x20\x32\x2e\x35\x2c\x2d\x32\x2e\x35\x6c\x30\ +\x2c\x2d\x32\x38\x7a\x6d\x2d\x32\x2e\x35\x34\x36\x34\x2c\x2d\x31\ +\x32\x2e\x35\x30\x31\x34\x6c\x2d\x32\x2e\x30\x38\x33\x32\x2c\x2d\ +\x32\x2e\x36\x31\x32\x31\x6c\x2d\x39\x2e\x37\x34\x30\x38\x2c\x30\ +\x6c\x2d\x32\x2e\x30\x38\x33\x32\x2c\x32\x2e\x36\x31\x32\x31\x6c\ +\x31\x33\x2e\x39\x30\x37\x32\x2c\x30\x7a\x22\x20\x66\x69\x6c\x6c\ +\x3d\x22\x23\x43\x32\x43\x43\x44\x45\x22\x20\x69\x64\x3d\x22\x73\ +\x76\x67\x5f\x31\x22\x2f\x3e\x0d\x0a\x20\x20\x3c\x70\x61\x74\x68\ +\x20\x66\x69\x6c\x6c\x3d\x22\x23\x66\x66\x30\x30\x30\x30\x22\x20\ +\x64\x3d\x22\x6d\x37\x2e\x33\x33\x38\x33\x2c\x33\x39\x2e\x39\x39\ +\x39\x39\x34\x6c\x30\x2c\x30\x63\x30\x2c\x2d\x31\x38\x2e\x30\x38\ +\x35\x33\x35\x20\x31\x34\x2e\x36\x32\x33\x31\x36\x2c\x2d\x33\x32\ +\x2e\x37\x34\x36\x35\x31\x20\x33\x32\x2e\x36\x36\x31\x36\x37\x2c\ +\x2d\x33\x32\x2e\x37\x34\x36\x35\x31\x6c\x30\x2c\x30\x63\x38\x2e\ +\x36\x36\x32\x35\x33\x2c\x30\x20\x31\x36\x2e\x39\x37\x30\x32\x35\ +\x2c\x33\x2e\x34\x35\x30\x30\x38\x20\x32\x33\x2e\x30\x39\x35\x33\ +\x32\x2c\x39\x2e\x35\x39\x31\x32\x36\x63\x36\x2e\x31\x32\x35\x33\ +\x31\x2c\x36\x2e\x31\x34\x31\x31\x39\x20\x39\x2e\x35\x36\x36\x34\ +\x32\x2c\x31\x34\x2e\x34\x37\x30\x34\x34\x20\x39\x2e\x35\x36\x36\ +\x34\x32\x2c\x32\x33\x2e\x31\x35\x35\x32\x34\x6c\x30\x2c\x30\x63\ +\x30\x2c\x31\x38\x2e\x30\x38\x35\x35\x36\x20\x2d\x31\x34\x2e\x36\ +\x32\x33\x30\x35\x2c\x33\x32\x2e\x37\x34\x36\x36\x33\x20\x2d\x33\ +\x32\x2e\x36\x36\x31\x37\x33\x2c\x33\x32\x2e\x37\x34\x36\x36\x33\ +\x6c\x30\x2c\x30\x63\x2d\x31\x38\x2e\x30\x33\x38\x35\x31\x2c\x30\ +\x20\x2d\x33\x32\x2e\x36\x36\x31\x36\x37\x2c\x2d\x31\x34\x2e\x36\ +\x36\x31\x30\x36\x20\x2d\x33\x32\x2e\x36\x36\x31\x36\x37\x2c\x2d\ +\x33\x32\x2e\x37\x34\x36\x36\x33\x63\x30\x2c\x30\x20\x30\x2c\x30\ +\x20\x30\x2c\x30\x6c\x2d\x30\x2e\x30\x30\x30\x30\x31\x2c\x30\x7a\ +\x6d\x35\x32\x2e\x37\x34\x31\x32\x31\x2c\x31\x34\x2e\x36\x34\x39\ +\x31\x36\x6c\x30\x2c\x30\x63\x37\x2e\x31\x39\x31\x30\x32\x2c\x2d\ +\x39\x2e\x39\x30\x38\x33\x34\x20\x36\x2e\x31\x32\x32\x30\x39\x2c\ +\x2d\x32\x33\x2e\x35\x38\x39\x34\x38\x20\x2d\x32\x2e\x35\x32\x30\ +\x31\x35\x2c\x2d\x33\x32\x2e\x32\x35\x34\x30\x37\x63\x2d\x38\x2e\ +\x36\x34\x32\x32\x35\x2c\x2d\x38\x2e\x36\x36\x34\x36\x36\x20\x2d\ +\x32\x32\x2e\x32\x38\x37\x39\x33\x2c\x2d\x39\x2e\x37\x33\x36\x33\ +\x37\x20\x2d\x33\x32\x2e\x31\x37\x30\x33\x33\x2c\x2d\x32\x2e\x35\ +\x32\x36\x35\x38\x6c\x33\x34\x2e\x36\x39\x30\x34\x37\x2c\x33\x34\ +\x2e\x37\x38\x30\x36\x35\x6c\x30\x2e\x30\x30\x30\x30\x31\x2c\x30\ +\x7a\x6d\x2d\x34\x30\x2e\x31\x35\x38\x38\x39\x2c\x2d\x32\x39\x2e\ +\x32\x39\x38\x30\x31\x63\x2d\x37\x2e\x31\x39\x31\x30\x37\x2c\x39\ +\x2e\x39\x30\x38\x32\x38\x20\x2d\x36\x2e\x31\x32\x32\x31\x37\x2c\ +\x32\x33\x2e\x35\x38\x39\x34\x33\x20\x32\x2e\x35\x32\x30\x30\x32\ +\x2c\x33\x32\x2e\x32\x35\x33\x39\x63\x38\x2e\x36\x34\x32\x31\x37\ +\x2c\x38\x2e\x36\x36\x34\x37\x31\x20\x32\x32\x2e\x32\x38\x37\x38\ +\x35\x2c\x39\x2e\x37\x33\x36\x34\x31\x20\x33\x32\x2e\x31\x37\x30\ +\x32\x35\x2c\x32\x2e\x35\x32\x36\x37\x6c\x2d\x33\x34\x2e\x36\x39\ +\x30\x32\x38\x2c\x2d\x33\x34\x2e\x37\x38\x30\x35\x39\x6c\x30\x2c\ +\x30\x6c\x30\x2e\x30\x30\x30\x30\x31\x2c\x2d\x30\x2e\x30\x30\x30\ +\x30\x31\x7a\x22\x20\x69\x64\x3d\x22\x73\x76\x67\x5f\x33\x22\x2f\ +\x3e\x0d\x0a\x20\x3c\x2f\x67\x3e\x0d\x0a\x0d\x0a\x3c\x2f\x73\x76\ +\x67\x3e\ \x00\x00\x09\x97\ \x3c\ \x73\x76\x67\x20\x66\x69\x6c\x6c\x3d\x22\x23\x30\x30\x30\x30\x30\ @@ -1324,6 +1381,10 @@ \x03\xdc\xdd\x87\ \x00\x73\ \x00\x74\x00\x61\x00\x72\x00\x2d\x00\x63\x00\x72\x00\x6f\x00\x73\x00\x73\x00\x65\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x10\ +\x05\x22\x88\x87\ +\x00\x6f\ +\x00\x72\x00\x61\x00\x6e\x00\x67\x00\x65\x00\x2d\x00\x61\x00\x6c\x00\x65\x00\x72\x00\x74\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x08\ \x05\x77\x54\xa7\ \x00\x6c\ @@ -1390,70 +1451,73 @@ qt_resource_struct_v1 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x00\x18\x00\x02\x00\x00\x00\x12\x00\x00\x00\x03\ +\x00\x00\x00\x18\x00\x02\x00\x00\x00\x13\x00\x00\x00\x03\ \x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x00\x04\ \x00\x00\x00\x4c\x00\x00\x00\x00\x00\x01\x00\x00\x01\x7d\ \x00\x00\x00\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x03\x21\ -\x00\x00\x00\x90\x00\x00\x00\x00\x00\x01\x00\x00\x0a\xfe\ -\x00\x00\x00\xa6\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x6e\ -\x00\x00\x00\xbc\x00\x01\x00\x00\x00\x01\x00\x00\x0d\xa5\ -\x00\x00\x00\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x11\x51\ -\x00\x00\x00\xee\x00\x00\x00\x00\x00\x01\x00\x00\x14\xf0\ -\x00\x00\x01\x04\x00\x00\x00\x00\x00\x01\x00\x00\x16\x7c\ -\x00\x00\x01\x18\x00\x00\x00\x00\x00\x01\x00\x00\x17\xf0\ -\x00\x00\x01\x2e\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xae\ -\x00\x00\x01\x48\x00\x00\x00\x00\x00\x01\x00\x00\x22\x74\ -\x00\x00\x01\x66\x00\x00\x00\x00\x00\x01\x00\x00\x2c\x07\ -\x00\x00\x01\x8a\x00\x00\x00\x00\x00\x01\x00\x00\x33\xa8\ -\x00\x00\x01\xa6\x00\x00\x00\x00\x00\x01\x00\x00\x35\x3c\ -\x00\x00\x01\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x36\xb6\ -\x00\x00\x01\xe0\x00\x00\x00\x00\x00\x01\x00\x00\x3e\x57\ -\x00\x00\x02\x08\x00\x00\x00\x00\x00\x01\x00\x00\x44\xa7\ +\x00\x00\x00\x90\x00\x00\x00\x00\x00\x01\x00\x00\x0b\x06\ +\x00\x00\x00\xb6\x00\x00\x00\x00\x00\x01\x00\x00\x0d\x5e\ +\x00\x00\x00\xcc\x00\x00\x00\x00\x00\x01\x00\x00\x0e\xce\ +\x00\x00\x00\xe2\x00\x01\x00\x00\x00\x01\x00\x00\x10\x05\ +\x00\x00\x00\xfc\x00\x00\x00\x00\x00\x01\x00\x00\x13\xb7\ +\x00\x00\x01\x14\x00\x00\x00\x00\x00\x01\x00\x00\x17\x58\ +\x00\x00\x01\x2a\x00\x00\x00\x00\x00\x01\x00\x00\x18\xe4\ +\x00\x00\x01\x3e\x00\x00\x00\x00\x00\x01\x00\x00\x1a\x58\ +\x00\x00\x01\x54\x00\x00\x00\x00\x00\x01\x00\x00\x20\x19\ +\x00\x00\x01\x6e\x00\x00\x00\x00\x00\x01\x00\x00\x24\xe2\ +\x00\x00\x01\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x2e\xbd\ +\x00\x00\x01\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x36\xa7\ +\x00\x00\x01\xcc\x00\x00\x00\x00\x00\x01\x00\x00\x38\x3b\ +\x00\x00\x01\xe6\x00\x00\x00\x00\x00\x01\x00\x00\x39\xb5\ +\x00\x00\x02\x06\x00\x00\x00\x00\x00\x01\x00\x00\x41\x9f\ +\x00\x00\x02\x2e\x00\x00\x00\x00\x00\x01\x00\x00\x47\xf6\ " qt_resource_struct_v2 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd5\ -\x00\x00\x00\x18\x00\x02\x00\x00\x00\x12\x00\x00\x00\x03\ +\x00\x00\x01\x9d\xe1\x83\x22\xfc\ +\x00\x00\x00\x18\x00\x02\x00\x00\x00\x13\x00\x00\x00\x03\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x00\x04\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ +\x00\x00\x01\x9d\xe1\x83\x22\xf9\ \x00\x00\x00\x4c\x00\x00\x00\x00\x00\x01\x00\x00\x01\x7d\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd3\ +\x00\x00\x01\x9d\xe1\x83\x22\xf5\ \x00\x00\x00\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x03\x21\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ -\x00\x00\x00\x90\x00\x00\x00\x00\x00\x01\x00\x00\x0a\xfe\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ -\x00\x00\x00\xa6\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x6e\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd3\ -\x00\x00\x00\xbc\x00\x01\x00\x00\x00\x01\x00\x00\x0d\xa5\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd3\ -\x00\x00\x00\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x11\x51\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd5\ -\x00\x00\x00\xee\x00\x00\x00\x00\x00\x01\x00\x00\x14\xf0\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ -\x00\x00\x01\x04\x00\x00\x00\x00\x00\x01\x00\x00\x16\x7c\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ -\x00\x00\x01\x18\x00\x00\x00\x00\x00\x01\x00\x00\x17\xf0\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd5\ -\x00\x00\x01\x2e\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xae\ -\x00\x00\x01\x9a\x4b\xc3\x1d\x94\ -\x00\x00\x01\x48\x00\x00\x00\x00\x00\x01\x00\x00\x22\x74\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd3\ -\x00\x00\x01\x66\x00\x00\x00\x00\x00\x01\x00\x00\x2c\x07\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd2\ -\x00\x00\x01\x8a\x00\x00\x00\x00\x00\x01\x00\x00\x33\xa8\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ -\x00\x00\x01\xa6\x00\x00\x00\x00\x00\x01\x00\x00\x35\x3c\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd3\ -\x00\x00\x01\xc0\x00\x00\x00\x00\x00\x01\x00\x00\x36\xb6\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd3\ -\x00\x00\x01\xe0\x00\x00\x00\x00\x00\x01\x00\x00\x3e\x57\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd5\ -\x00\x00\x02\x08\x00\x00\x00\x00\x00\x01\x00\x00\x44\xa7\ -\x00\x00\x01\x95\xe8\xaa\xa9\xd4\ +\x00\x00\x01\x9d\xe1\x83\x22\xfb\ +\x00\x00\x00\x90\x00\x00\x00\x00\x00\x01\x00\x00\x0b\x06\ +\x00\x00\x01\x9d\xfb\x58\x6d\xfe\ +\x00\x00\x00\xb6\x00\x00\x00\x00\x00\x01\x00\x00\x0d\x5e\ +\x00\x00\x01\x9d\xe1\x83\x22\xf8\ +\x00\x00\x00\xcc\x00\x00\x00\x00\x00\x01\x00\x00\x0e\xce\ +\x00\x00\x01\x9d\xe1\x83\x22\xf6\ +\x00\x00\x00\xe2\x00\x01\x00\x00\x00\x01\x00\x00\x10\x05\ +\x00\x00\x01\x9d\xe1\x83\x22\xf7\ +\x00\x00\x00\xfc\x00\x00\x00\x00\x00\x01\x00\x00\x13\xb7\ +\x00\x00\x01\x9d\xe1\x83\x22\xfc\ +\x00\x00\x01\x14\x00\x00\x00\x00\x00\x01\x00\x00\x17\x58\ +\x00\x00\x01\x9d\xe1\x83\x22\xfa\ +\x00\x00\x01\x2a\x00\x00\x00\x00\x00\x01\x00\x00\x18\xe4\ +\x00\x00\x01\x9d\xe1\x83\x22\xfb\ +\x00\x00\x01\x3e\x00\x00\x00\x00\x00\x01\x00\x00\x1a\x58\ +\x00\x00\x01\x9d\xe1\x83\x22\xfb\ +\x00\x00\x01\x54\x00\x00\x00\x00\x00\x01\x00\x00\x20\x19\ +\x00\x00\x01\x9d\xe1\x83\x22\xf8\ +\x00\x00\x01\x6e\x00\x00\x00\x00\x00\x01\x00\x00\x24\xe2\ +\x00\x00\x01\x9d\xe1\x83\x22\xf7\ +\x00\x00\x01\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x2e\xbd\ +\x00\x00\x01\x9d\xe1\x83\x22\xf4\ +\x00\x00\x01\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x36\xa7\ +\x00\x00\x01\x9d\xe1\x83\x22\xf9\ +\x00\x00\x01\xcc\x00\x00\x00\x00\x00\x01\x00\x00\x38\x3b\ +\x00\x00\x01\x9d\xe1\x83\x22\xf7\ +\x00\x00\x01\xe6\x00\x00\x00\x00\x00\x01\x00\x00\x39\xb5\ +\x00\x00\x01\x9d\xe1\x83\x22\xf5\ +\x00\x00\x02\x06\x00\x00\x00\x00\x00\x01\x00\x00\x41\x9f\ +\x00\x00\x01\x9d\xe1\x83\x22\xfc\ +\x00\x00\x02\x2e\x00\x00\x00\x00\x00\x01\x00\x00\x47\xf6\ +\x00\x00\x01\x9d\xe1\x83\x22\xf9\ " qt_version = [int(v) for v in QtCore.qVersion().split(".")] diff --git a/src/instrumentserver/resource.qrc b/src/instrumentserver/resource.qrc index 995f218..18a29c6 100644 --- a/src/instrumentserver/resource.qrc +++ b/src/instrumentserver/resource.qrc @@ -7,6 +7,7 @@ resource/icons/set.svg resource/icons/alert-octagon.svg resource/icons/alert-octagon-red.svg + resource/icons/alert-octagon-orange.svg resource/icons/python.svg resource/icons/code.svg resource/icons/delete.svg