diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0a19790
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,174 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# UV
+# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+#uv.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+# Ruff stuff:
+.ruff_cache/
+
+# PyPI configuration file
+.pypirc
diff --git a/README.md b/README.md
index 152e0a4..6e2551a 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,11 @@
# pyqtprotractor
-Multiplatform† protractor overlay tool for measuring angles on screen.
+Multiplatform† protractor overlay tool for measuring angles on screen.
-Just install the required PyQt 5 (for example `pip install PyQt5`, maybe with `--user`) and then run `./protractor.py`.
+Probably easiest to run with `uv`.
+
+```sh
+$ uv run pyqtprotractor
+```
† *Tested only on Linux so far, but since it is written in Python and Qt if should be able run on Windows and Mac as well.*
@@ -11,7 +15,16 @@ Run `./protractor.py`. An overlay should appear in the top left corner of your s
* You can move the individual points around by dragging them with the mouse.
* You can move the whole protractor around without changing the angle by dragging the lines or the angle text box.
-* You can work "around" the protractor, for example click between the arms to draw something in the window below.
+* You can work "around" the protractor, for example click between the arms to draw something in the window below (unless you're using Gnome Wayland on Linux).
+
+### Keyboard shortcuts
+
+Probably broken on non-Linux platforms, but worth a try.
+
+* `X` = toggle inverting angle around 180 degrees
+* `N` = toggle allow negative values
+* `Ctrl+C` = copy value
+* `Ctrl+Shift+C` = copy value, rounded to nearest integer
## Example
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..272339f
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,170 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1763622513,
+ "narHash": "sha256-1jQnuyu82FpiSxowrF/iFK6Toh9BYprfDqfs4BB+19M=",
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "rev": "c58bc7f5459328e4afac201c5c4feb7c818d604b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nixos",
+ "ref": "nixos-25.05",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "nixpkgs_2": {
+ "locked": {
+ "lastModified": 1725103162,
+ "narHash": "sha256-Ym04C5+qovuQDYL/rKWSR+WESseQBbNAe5DsXNx5trY=",
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "rev": "12228ff1752d7b7624a54e9c1af4b222b3c1073b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nixos",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "pyproject-build-systems": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ],
+ "pyproject-nix": [
+ "pyproject-nix"
+ ],
+ "uv2nix": [
+ "uv2nix"
+ ]
+ },
+ "locked": {
+ "lastModified": 1763662255,
+ "narHash": "sha256-4bocaOyLa3AfiS8KrWjZQYu+IAta05u3gYZzZ6zXbT0=",
+ "owner": "pyproject-nix",
+ "repo": "build-system-pkgs",
+ "rev": "042904167604c681a090c07eb6967b4dd4dae88c",
+ "type": "github"
+ },
+ "original": {
+ "owner": "pyproject-nix",
+ "repo": "build-system-pkgs",
+ "type": "github"
+ }
+ },
+ "pyproject-nix": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1763716960,
+ "narHash": "sha256-PUlomle4klGbnZr0wOn8z61Mbt7tXh6Yp3hZ9/CQkq0=",
+ "owner": "pyproject-nix",
+ "repo": "pyproject.nix",
+ "rev": "d6c61dbe0be75e2f4cf0efcdc62428175be4cfb5",
+ "type": "github"
+ },
+ "original": {
+ "owner": "pyproject-nix",
+ "repo": "pyproject.nix",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs",
+ "pyproject-build-systems": "pyproject-build-systems",
+ "pyproject-nix": "pyproject-nix",
+ "uv2nix": "uv2nix",
+ "uv2nix_hammer_overrides": "uv2nix_hammer_overrides"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "id": "systems",
+ "type": "indirect"
+ }
+ },
+ "treefmt-nix": {
+ "inputs": {
+ "nixpkgs": "nixpkgs_2"
+ },
+ "locked": {
+ "lastModified": 1725271838,
+ "narHash": "sha256-VcqxWT0O/gMaeWTTjf1r4MOyG49NaNxW4GHTO3xuThE=",
+ "owner": "numtide",
+ "repo": "treefmt-nix",
+ "rev": "9fb342d14b69aefdf46187f6bb80a4a0d97007cd",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "treefmt-nix",
+ "type": "github"
+ }
+ },
+ "uv2nix": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ],
+ "pyproject-nix": [
+ "pyproject-nix"
+ ]
+ },
+ "locked": {
+ "lastModified": 1763962610,
+ "narHash": "sha256-UqEV7q+Op+jBysp5RLLApVkJB63V9quNx6mO6++XnqE=",
+ "owner": "pyproject-nix",
+ "repo": "uv2nix",
+ "rev": "fa9fba1c36ef1acbd9291c7df281a6d71cfc47c0",
+ "type": "github"
+ },
+ "original": {
+ "owner": "pyproject-nix",
+ "repo": "uv2nix",
+ "type": "github"
+ }
+ },
+ "uv2nix_hammer_overrides": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ],
+ "systems": "systems",
+ "treefmt-nix": "treefmt-nix"
+ },
+ "locked": {
+ "lastModified": 1752080735,
+ "narHash": "sha256-jrxqssrqRK6I6V6wBt7rp4B/jIXIgQNsXXeV+yguuJs=",
+ "owner": "TyberiusPrime",
+ "repo": "uv2nix_hammer_overrides",
+ "rev": "68d84fd911afa2383a1c1cb28712ca1d090a8c32",
+ "type": "github"
+ },
+ "original": {
+ "owner": "TyberiusPrime",
+ "repo": "uv2nix_hammer_overrides",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..afa0f5e
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,104 @@
+{
+ description = "pyqtprotractor";
+
+ inputs = {
+ nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
+
+ pyproject-nix = {
+ url = "github:pyproject-nix/pyproject.nix";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+
+ uv2nix = {
+ url = "github:pyproject-nix/uv2nix";
+ inputs.pyproject-nix.follows = "pyproject-nix";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+
+ pyproject-build-systems = {
+ url = "github:pyproject-nix/build-system-pkgs";
+ inputs.pyproject-nix.follows = "pyproject-nix";
+ inputs.uv2nix.follows = "uv2nix";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+
+ # Override packages to make them build properly
+ uv2nix_hammer_overrides.url = "github:TyberiusPrime/uv2nix_hammer_overrides";
+ uv2nix_hammer_overrides.inputs.nixpkgs.follows = "nixpkgs";
+ };
+
+ outputs =
+ {
+ nixpkgs,
+ pyproject-nix,
+ uv2nix,
+ pyproject-build-systems,
+ uv2nix_hammer_overrides,
+ ...
+ }:
+ let
+ inherit (nixpkgs) lib;
+ forAllSystems = lib.genAttrs lib.systems.flakeExposed;
+
+ workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
+
+ overlay = workspace.mkPyprojectOverlay {
+ sourcePreference = "wheel";
+ };
+
+ editableOverlay = workspace.mkEditablePyprojectOverlay {
+ root = "$REPO_ROOT";
+ };
+
+ pythonSets = forAllSystems (
+ system:
+ let
+ pkgs = nixpkgs.legacyPackages.${system};
+ python = pkgs.python3;
+ in
+ (pkgs.callPackage pyproject-nix.build.packages {
+ inherit python;
+ }).overrideScope
+ (
+ lib.composeManyExtensions [
+ (uv2nix_hammer_overrides.overrides pkgs)
+ pyproject-build-systems.overlays.wheel
+ overlay
+ ]
+ )
+ );
+
+ in
+ {
+ devShells = forAllSystems (
+ system:
+ let
+ pkgs = nixpkgs.legacyPackages.${system};
+ pythonSet = pythonSets.${system}.overrideScope editableOverlay;
+ virtualenv = pythonSet.mkVirtualEnv "pyqtprotractor-dev-env" workspace.deps.all;
+ in
+ {
+ default = pkgs.mkShell {
+ packages = [
+ virtualenv
+ pkgs.uv
+ pkgs.qt5.qtbase
+ ];
+ env = {
+ UV_NO_SYNC = "1";
+ UV_PYTHON = pythonSet.python.interpreter;
+ UV_PYTHON_DOWNLOADS = "never";
+ };
+ shellHook = ''
+ unset PYTHONPATH
+ export REPO_ROOT=$(git rev-parse --show-toplevel)
+ '';
+ };
+ }
+ );
+
+ packages = forAllSystems (system: {
+ default = pythonSets.${system}.mkVirtualEnv "pyqtprotractor" workspace.deps.default;
+ });
+ };
+}
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..33fd722
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,19 @@
+[project]
+name = "pyqtprotractor"
+version = "0.1.0"
+description = "Multiplatform protractor overlay tool for measuring angles on screen"
+readme = "README.md"
+authors = [
+ { name = "Fritz Webering", email = "fritz@webering.eu" }
+]
+requires-python = ">=3.9"
+dependencies = [
+ "pyqt5>=5.15.11",
+]
+
+[project.scripts]
+pyqtprotractor = "pyqtprotractor:main"
+
+[build-system]
+requires = ["uv_build>=0.7.20,<0.10.0"]
+build-backend = "uv_build"
diff --git a/protractor.py b/src/pyqtprotractor/__init__.py
old mode 100755
new mode 100644
similarity index 71%
rename from protractor.py
rename to src/pyqtprotractor/__init__.py
index 319051c..2678886
--- a/protractor.py
+++ b/src/pyqtprotractor/__init__.py
@@ -22,10 +22,9 @@
import sys
import math
-from PyQt5.Qt import Qt, QApplication, QGuiApplication, QObject, QWidget, QRegion
-from PyQt5.QtCore import QPoint, QPointF, QRect, QRectF, QSize, pyqtSignal
+from PyQt5.Qt import Qt, QApplication, QWidget
+from PyQt5.QtCore import QPoint, QPointF, QRectF, pyqtSignal
from PyQt5.QtGui import QPainter, QPen
-from PyQt5.QtGui import QImage, QPixmap, QColor, QBitmap
from PyQt5.QtWidgets import QLabel
@@ -75,7 +74,7 @@ def __init__(self, parent):
def r(self): return self.width() / 4
- def paintEvent(self, event):
+ def paintEvent(self, a0):
dark = QPen(Qt.black, 1, Qt.SolidLine)
light = QPen(Qt.white, 2, Qt.SolidLine)
# light = QPen(QColor(200, 220, 255), 2, Qt.SolidLine)
@@ -100,15 +99,18 @@ def paintEvent(self, event):
qp.drawPoint(self.width()//2, self.height()//2)
qp.end()
- def mousePressEvent(self, event):
- self.offset = event.pos()
-
- def mouseMoveEvent(self, event):
- self.move(self.mapToParent(event.pos()) - self.offset)
+ def mousePressEvent(self, ev):
+ assert ev is not None
+ self.offset = ev.pos()
+
+ def mouseMoveEvent(self, ev):
+ assert ev is not None
+ self.move(self.mapToParent(ev.pos()) - self.offset)
self.moved.emit()
class Protractor(QLabel):
angleInvert = False
+ allowNegative = True
def __init__(self, parent):
super().__init__(parent, Qt.WindowType.FramelessWindowHint)
@@ -131,35 +133,48 @@ def __init__(self, parent):
self.label.setAutoFillBackground(True)
self.updateDisplay()
- def mousePressEvent(self, event):
+ def mousePressEvent(self, ev):
+ assert ev is not None
self.setCursor(Qt.ClosedHandCursor)
- self.offset = event.pos()
- event.accept()
+ self.offset = ev.pos()
+ ev.accept()
- def mouseMoveEvent(self, event):
- diff = event.pos() - self.offset
+ def mouseMoveEvent(self, ev):
+ assert ev is not None
+ diff = ev.pos() - self.offset
for widget in self.children():
widget.move(widget.pos() + diff)
- self.offset = event.pos()
+ self.offset = ev.pos()
self.updateDisplay()
- event.accept()
+ ev.accept()
- def mouseReleaseEvent(self, event):
+ def mouseReleaseEvent(self, ev):
self.setCursor(Qt.OpenHandCursor)
- def mouseDoubleClickEvent(self, event):
+ def mouseDoubleClickEvent(self, a0):
+ # For some reason, I couldn't make this function get called, at least
+ # on my system (Gnome 47 on Fedora Linux)
self.angleInvert = not self.angleInvert
self.updateDisplay()
- def updateDisplay(self):
- self.placeLabel()
- self.adjustSize()
+ def getAngle(self) -> float:
+ """
+ Calculates and returns angle in degrees
+ """
angle = threePointAngle(self.handle1.pos(), self.handleC.pos(), self.handle2.pos())
angleDeg = 180 * angle / math.pi
if self.angleInvert:
angleDeg = 180 - angleDeg
if angleDeg > 180:
- angleDeg = angleDeg - 360
+ angleDeg -= 360
+ if not self.allowNegative and angleDeg < 0:
+ angleDeg += 360
+ return angleDeg
+
+ def updateDisplay(self):
+ self.placeLabel()
+ self.adjustSize()
+ angleDeg = self.getAngle()
self.label.setText("%0.2f °" % angleDeg)
self.label.adjustSize()
self.updateMask()
@@ -186,7 +201,7 @@ def updateMask(self):
mask = mask.united(r.toRect())
self.setMask(mask)
- def paintEvent(self, event):
+ def paintEvent(self, a0):
dark = QPen(Qt.black, 1, Qt.SolidLine)
light = QPen(Qt.white, 2, Qt.SolidLine)
# light = QPen(QColor(200, 220, 255), 2, Qt.SolidLine)
@@ -195,28 +210,61 @@ def paintEvent(self, event):
qp.begin(self)
qp.setRenderHint(QPainter.Antialiasing)
qp.setRenderHint(QPainter.HighQualityAntialiasing)
- for pen in light, dark:
+ for pen in light, dark:
qp.setPen(pen)
drawShortenedLine(qp, centerPoint(self.handleC), centerPoint(self.handle1), self.handleC.r() + 1, self.handle1.r() + 1)
drawShortenedLine(qp, centerPoint(self.handleC), centerPoint(self.handle2), self.handleC.r() + 1, self.handle2.r() + 1)
qp.end()
- def keyPressEvent(self, event):
- if event.key() == Qt.Key_Escape:
+ def keyPressEvent(self, ev):
+ assert ev is not None
+ # This probably isn't the correct way to handle modifier keys, but I
+ # couldn't figure out how to get the modifier() method working
+ modifiers = (ev.nativeModifiers() - 16)
+
+ # Esc = Quit
+ if ev.key() == Qt.Key_Escape:
QApplication.quit()
+ # X = Toggle invert angle around 180
+ elif ev.key() == Qt.Key_X:
+ self.angleInvert = not self.angleInvert
+ self.updateDisplay()
+ # N = Toggle negative angles
+ elif ev.key() == Qt.Key_N:
+ self.allowNegative = not self.allowNegative
+ self.updateDisplay()
+ # Ctrl+C
+ elif ev.key() == Qt.Key_C and modifiers & 4:
+ # https://stackoverflow.com/a/23119741/6335363
+ cb = QApplication.clipboard()
+ angleDeg = self.getAngle()
+ if modifiers & 1:
+ # Shift = round to 0 decimal places
+ angleDeg = round(angleDeg)
+ else:
+ # Otherwise, 2 decimal places
+ angleDeg = round(angleDeg, 2)
+ cb.clear(mode=cb.Clipboard)
+ toCopy = str(angleDeg)
+ cb.setText(toCopy, mode=cb.Clipboard)
else:
- QWidget.keyPressEvent(self, event)
+ QWidget.keyPressEvent(self, ev)
+
+def main():
+ a = QApplication(sys.argv)
+
+ widget = Protractor(None)
+ widget.show()
-a = QApplication(sys.argv)
+ # Negative coordinates are necessary because with (0,0) xfwm will place
+ # the window on the current desktop and below the task bar.
+ taskBarSize = 32
+ allScreens = QApplication.desktop().geometry()
+ widgetSize = allScreens.adjusted(-10, -taskBarSize, 10, taskBarSize)
+ widget.setGeometry(widgetSize)
-widget = Protractor(None)
-widget.show()
+ a.exec()
-# Negative coordinates are necessary because with (0,0) xfwm will place
-# the window on the current desktop and below the task bar.
-taskBarSize = 32
-allScreens = QApplication.desktop().geometry()
-widgetSize = allScreens.adjusted(-10, -taskBarSize, 10, taskBarSize)
-widget.setGeometry(widgetSize)
-a.exec()
+if __name__ == '__main__':
+ main()
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..a36c87c
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,70 @@
+version = 1
+revision = 1
+requires-python = ">=3.9"
+
+[[package]]
+name = "pyqt5"
+version = "5.15.11"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyqt5-qt5" },
+ { name = "pyqt5-sip" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0e/07/c9ed0bd428df6f87183fca565a79fee19fa7c88c7f00a7f011ab4379e77a/PyQt5-5.15.11.tar.gz", hash = "sha256:fda45743ebb4a27b4b1a51c6d8ef455c4c1b5d610c90d2934c7802b5c1557c52", size = 3216775 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/11/64/42ec1b0bd72d87f87bde6ceb6869f444d91a2d601f2e67cd05febc0346a1/PyQt5-5.15.11-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8b03dd9380bb13c804f0bdb0f4956067f281785b5e12303d529f0462f9afdc2", size = 6579776 },
+ { url = "https://files.pythonhosted.org/packages/49/f5/3fb696f4683ea45d68b7e77302eff173493ac81e43d63adb60fa760b9f91/PyQt5-5.15.11-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:6cd75628f6e732b1ffcfe709ab833a0716c0445d7aec8046a48d5843352becb6", size = 7016415 },
+ { url = "https://files.pythonhosted.org/packages/b4/8c/4065950f9d013c4b2e588fe33cf04e564c2322842d84dbcbce5ba1dc28b0/PyQt5-5.15.11-cp38-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:cd672a6738d1ae33ef7d9efa8e6cb0a1525ecf53ec86da80a9e1b6ec38c8d0f1", size = 8188103 },
+ { url = "https://files.pythonhosted.org/packages/f3/f0/ae5a5b4f9b826b29ea4be841b2f2d951bcf5ae1d802f3732b145b57c5355/PyQt5-5.15.11-cp38-abi3-win32.whl", hash = "sha256:76be0322ceda5deecd1708a8d628e698089a1cea80d1a49d242a6d579a40babd", size = 5433308 },
+ { url = "https://files.pythonhosted.org/packages/56/d5/68eb9f3d19ce65df01b6c7b7a577ad3bbc9ab3a5dd3491a4756e71838ec9/PyQt5-5.15.11-cp38-abi3-win_amd64.whl", hash = "sha256:bdde598a3bb95022131a5c9ea62e0a96bd6fb28932cc1619fd7ba211531b7517", size = 6865864 },
+]
+
+[[package]]
+name = "pyqt5-qt5"
+version = "5.15.16"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2f/97/ef77663fb5d61b65f2e93edc135cc0b86724c1fc610c10a6867ae0c0baff/PyQt5_Qt5-5.15.16-1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2cfa8f50dd29618ef98f29355f83d8a5f3e41003be22128e9b5d94d214b6b468", size = 61080044 },
+ { url = "https://files.pythonhosted.org/packages/ff/89/65a4b9c6bc89012182ff46bf8e503a8aa5e5570df7cb628211c93012beca/PyQt5_Qt5-5.15.16-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:18b6fec012de60921fcb131cf2a21368171dc29050d43e4b81a64be407a36105", size = 39957063 },
+ { url = "https://files.pythonhosted.org/packages/0a/60/b6db285c87666b02aa11d0946d7bb381d8bdcc815cc5aa61fa91272b321b/PyQt5_Qt5-5.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e1a0e7ae35a7615c74a293705204579650930486a89af23082462f429dae504a", size = 37122434 },
+ { url = "https://files.pythonhosted.org/packages/dc/1a/c4f861c4d7a9844a207b8bc3aa9dd84c51f823784d405469cde83d736cf1/PyQt5_Qt5-5.15.16-py3-none-manylinux2014_x86_64.whl", hash = "sha256:5ee1754a6460849cba76c0f0c490c0ccc3b514abc780b141cf772db22b76b54b", size = 59854452 },
+]
+
+[[package]]
+name = "pyqt5-sip"
+version = "12.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/79/086b50414bafa71df494398ad277d72e58229a3d1c1b1c766d12b14c2e6d/pyqt5_sip-12.17.0.tar.gz", hash = "sha256:682dadcdbd2239af9fdc0c0628e2776b820e128bec88b49b8d692fe682f90b4f", size = 104042 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6b/23/1da570b7e143b6d216728c919cae2976f7dbff65db94e3d9f5b62df37ba5/PyQt5_sip-12.17.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec47914cc751608e587c1c2fdabeaf4af7fdc28b9f62796c583bea01c1a1aa3e", size = 122696 },
+ { url = "https://files.pythonhosted.org/packages/61/d5/506b1c3ad06268c601276572f1cde1c0dffd074b44e023f4d80f5ea49265/PyQt5_sip-12.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2f2a8dcc7626fe0da73a0918e05ce2460c7a14ddc946049310e6e35052105434", size = 270932 },
+ { url = "https://files.pythonhosted.org/packages/0b/9b/46159d8038374b076244a1930ead460e723453ec73f9b0330390ddfdd0ea/PyQt5_sip-12.17.0-cp310-cp310-win32.whl", hash = "sha256:0c75d28b8282be3c1d7dbc76950d6e6eba1e334783224e9b9835ce1a9c64f482", size = 49085 },
+ { url = "https://files.pythonhosted.org/packages/fe/66/b3eb937a620ce2a5db5c377beeca870d60fafd87aecc1bcca6921bbcf553/PyQt5_sip-12.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:8c4bc535bae0dfa764e8534e893619fe843ce5a2e25f901c439bcb960114f686", size = 59040 },
+ { url = "https://files.pythonhosted.org/packages/52/fd/7d6e3deca5ce37413956faf4e933ce6beb87ac0cc7b26d934b5ed998f88a/PyQt5_sip-12.17.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c912807dd638644168ea8c7a447bfd9d85a19471b98c2c588c4d2e911c09b0a", size = 122748 },
+ { url = "https://files.pythonhosted.org/packages/29/4d/e5981cde03b091fd83a1ef4ef6a4ca99ce6921d61b80c0222fc8eafdc99a/PyQt5_sip-12.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:71514a7d43b44faa1d65a74ad2c5da92c03a251bdc749f009c313f06cceacc9a", size = 276401 },
+ { url = "https://files.pythonhosted.org/packages/5f/30/4c282896b1e8841639cf2aca59acf57d8b261ed834ae976c959f25fa4a35/PyQt5_sip-12.17.0-cp311-cp311-win32.whl", hash = "sha256:023466ae96f72fbb8419b44c3f97475de6642fa5632520d0f50fc1a52a3e8200", size = 49091 },
+ { url = "https://files.pythonhosted.org/packages/24/c1/50fc7301aa39a50f451fc1b6b219e778c540a823fe9533a57b4793c859fd/PyQt5_sip-12.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb565469d08dcb0a427def0c45e722323beb62db79454260482b6948bfd52d47", size = 59036 },
+ { url = "https://files.pythonhosted.org/packages/a3/e6/e51367c28d69b5a462f38987f6024e766fd8205f121fe2f4d8ba2a6886b9/PyQt5_sip-12.17.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ea08341c8a5da00c81df0d689ecd4ee47a95e1ecad9e362581c92513f2068005", size = 124650 },
+ { url = "https://files.pythonhosted.org/packages/64/3b/e6d1f772b41d8445d6faf86cc9da65910484ebd9f7df83abc5d4955437d0/PyQt5_sip-12.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4a92478d6808040fbe614bb61500fbb3f19f72714b99369ec28d26a7e3494115", size = 281893 },
+ { url = "https://files.pythonhosted.org/packages/ed/c5/d17fc2ddb9156a593710c88afd98abcf4055a2224b772f8bec2c6eea879c/PyQt5_sip-12.17.0-cp312-cp312-win32.whl", hash = "sha256:b0ff280b28813e9bfd3a4de99490739fc29b776dc48f1c849caca7239a10fc8b", size = 49438 },
+ { url = "https://files.pythonhosted.org/packages/fe/c5/1174988d52c732d07033cf9a5067142b01d76be7731c6394a64d5c3ef65c/PyQt5_sip-12.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:54c31de7706d8a9a8c0fc3ea2c70468aba54b027d4974803f8eace9c22aad41c", size = 58017 },
+ { url = "https://files.pythonhosted.org/packages/fd/5d/f234e505af1a85189310521447ebc6052ebb697efded850d0f2b2555f7aa/PyQt5_sip-12.17.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c7a7ff355e369616b6bcb41d45b742327c104b2bf1674ec79b8d67f8f2fa9543", size = 124580 },
+ { url = "https://files.pythonhosted.org/packages/cd/cb/3b2050e9644d0021bdf25ddf7e4c3526e1edd0198879e76ba308e5d44faf/PyQt5_sip-12.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:419b9027e92b0b707632c370cfc6dc1f3b43c6313242fc4db57a537029bd179c", size = 281563 },
+ { url = "https://files.pythonhosted.org/packages/51/61/b8ebde7e0b32d0de44c521a0ace31439885b0423d7d45d010a2f7d92808c/PyQt5_sip-12.17.0-cp313-cp313-win32.whl", hash = "sha256:351beab964a19f5671b2a3e816ecf4d3543a99a7e0650f88a947fea251a7589f", size = 49383 },
+ { url = "https://files.pythonhosted.org/packages/15/ed/ff94d6b2910e7627380cb1fc9a518ff966e6d78285c8e54c9422b68305db/PyQt5_sip-12.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:672c209d05661fab8e17607c193bf43991d268a1eefbc2c4551fbf30fd8bb2ca", size = 58022 },
+ { url = "https://files.pythonhosted.org/packages/21/f7/3bed2754743ba52b8264c20a1c52df6ff9c5f6465c11ae108be3b841471a/PyQt5_sip-12.17.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d65a9c1b4cbbd8e856254609f56e897d2cb5c903f77b75fb720cb3a32c76b92b", size = 122688 },
+ { url = "https://files.pythonhosted.org/packages/23/63/8a934ea1f759eee60b4e0143e5b109d16560bf67593bfed76cca55799a1a/PyQt5_sip-12.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:32b03e7e77ecd7b4119eba486b0706fa59b490bcceb585f9b6ddec8a582082db", size = 268531 },
+ { url = "https://files.pythonhosted.org/packages/34/80/0df0cdb7b25a87346a493cedb20f6eeb0301e7fbc02ed23d9077df998291/PyQt5_sip-12.17.0-cp39-cp39-win32.whl", hash = "sha256:5b6c734f4ad28f3defac4890ed747d391d246af279200935d49953bc7d915b8c", size = 49005 },
+ { url = "https://files.pythonhosted.org/packages/30/f5/2fd274c4fe9513d750eecfbe0c39937a179534446e148d8b9db4255f429a/PyQt5_sip-12.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:855e8f5787d57e26a48d8c3de1220a8e92ab83be8d73966deac62fdae03ea2f9", size = 59076 },
+]
+
+[[package]]
+name = "pyqtprotractor"
+version = "0.1.0"
+source = { editable = "." }
+dependencies = [
+ { name = "pyqt5" },
+]
+
+[package.metadata]
+requires-dist = [{ name = "pyqt5", specifier = ">=5.15.11" }]