diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 59baa53..fd99fee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,10 +50,6 @@ jobs: - name: Pre-commit hooks run: | pre-commit run --all-files - - name: PyLint - run: | - pylint $( find . -path './adafruit*.py' ) - ([[ ! -d "examples" ]] || pylint --disable=missing-docstring,invalid-name,bad-whitespace $( find . -path "./examples/*.py" )) - name: Build assets run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . - name: Archive bundles @@ -61,7 +57,12 @@ jobs: with: name: bundles path: ${{ github.workspace }}/bundles/ + - name: Check For docs folder + id: need-docs + run: | + echo ::set-output name=docs::$( find . -wholename './docs' ) - name: Build docs + if: contains(steps.need-docs.outputs.docs, 'docs') working-directory: docs run: sphinx-build -E -W -b html . _build/html - name: Check For setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb39186 --- /dev/null +++ b/.gitignore @@ -0,0 +1,341 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +# Created by https://www.toptal.com/developers/gitignore/api/macos +# Edit at https://www.toptal.com/developers/gitignore?templates=macos + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# End of https://www.toptal.com/developers/gitignore/api/macos + +# Created by https://www.toptal.com/developers/gitignore/api/windows +# Edit at https://www.toptal.com/developers/gitignore?templates=windows + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/windows + +# Created by https://www.toptal.com/developers/gitignore/api/linux +# Edit at https://www.toptal.com/developers/gitignore?templates=linux + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# End of https://www.toptal.com/developers/gitignore/api/linux + +# Created by https://www.toptal.com/developers/gitignore/api/jetbrains +# Edit at https://www.toptal.com/developers/gitignore?templates=jetbrains + +### JetBrains ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# End of https://www.toptal.com/developers/gitignore/api/jetbrains + +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# 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/ +pip-wheel-metadata/ +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/ +pytestdebug.log + +# 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/ +doc/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.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 + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +pythonenv* + +# 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/ + +# profiling data +.prof + +# End of https://www.toptal.com/developers/gitignore/api/python +.idea/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aab5f1c..354c761 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,11 +4,11 @@ repos: - repo: https://github.com/python/black - rev: stable + rev: 20.8b1 hooks: - id: black - repo: https://github.com/fsfe/reuse-tool - rev: latest + rev: v0.12.1 hooks: - id: reuse - repo: https://github.com/pre-commit/pre-commit-hooks @@ -17,3 +17,18 @@ repos: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace +- repo: https://github.com/pycqa/pylint + rev: pylint-2.7.1 + hooks: + - id: pylint + name: pylint (library code) + types: [python] + exclude: "^(docs/|examples/|setup.py$)" +- repo: local + hooks: + - id: pylint_examples + name: pylint (examples code) + description: Run pylint rules on "examples/*.py" files + entry: /usr/bin/env bash -c + args: ['([[ ! -d "examples" ]] || for example in $(find . -path "./examples/*.py"); do pylint --disable=missing-docstring,invalid-name $example; done)'] + language: system diff --git a/adafruit_progressbar.py b/adafruit_progressbar.py deleted file mode 100755 index 6a1d5ad..0000000 --- a/adafruit_progressbar.py +++ /dev/null @@ -1,149 +0,0 @@ -# SPDX-FileCopyrightText: 2020 Brent Rubell for Adafruit Industries -# -# SPDX-License-Identifier: MIT - -""" -`adafruit_progressbar` -================================================================================ - -Dynamic progress bar widget for CircuitPython displays - - -* Author(s): Brent Rubell - -Implementation Notes --------------------- - -**Software and Dependencies:** - -* Adafruit CircuitPython firmware for the supported boards: - https://github.com/adafruit/circuitpython/releases - -""" - -# imports -import displayio - -__version__ = "0.0.0-auto.0" -__repo__ = "https://github.com/brentru/Adafruit_CircuitPython_ProgressBar.git" - -# pylint: disable=too-many-arguments, too-few-public-methods -class ProgressBar(displayio.TileGrid): - """A dynamic progress bar widget. - - :param int x: The x-position of the top left corner. - :param int y: The y-position of the top left corner. - :param int width: The width of the progress bar. - :param int height: The height of the progress bar. - :param float progress: The percentage of the progress bar. - :param bar_color: The color of the progress bar. Can be a hex - value for color. - :param int outline_color: The outline of the progress bar. Can be a hex - value for color. - :param int stroke: Used for the outline_color - - """ - - # pylint: disable=invalid-name - def __init__( - self, - x, - y, - width, - height, - progress=0.0, - bar_color=0x00FF00, - outline_color=0xFFFFFF, - stroke=1, - ): - assert isinstance(progress, float), "Progress must be a floating point value." - self._bitmap = displayio.Bitmap(width, height, 3) - self._palette = displayio.Palette(3) - self._palette[0] = 0x0 - self._palette[1] = outline_color - self._palette[2] = bar_color - - # _width and _height are already in use for blinka TileGrid - self._bar_width = width - self._bar_height = height - - self._progress_val = 0.0 - self.progress = self._progress_val - self.progress = progress - - # draw outline rectangle - for _w in range(width): - for line in range(stroke): - self._bitmap[_w, line] = 1 - self._bitmap[_w, height - 1 - line] = 1 - for _h in range(height): - for line in range(stroke): - self._bitmap[line, _h] = 1 - self._bitmap[width - 1 - line, _h] = 1 - super().__init__(self._bitmap, pixel_shader=self._palette, x=x, y=y) - - @property - def progress(self): - """The percentage of the progress bar expressed as a - floating point number. - - """ - return self._progress_val - - @progress.setter - def progress(self, value): - """Draws the progress bar - - :param float value: Progress bar value. - """ - assert value <= 1.0, "Progress value may not be > 100%" - assert isinstance( - value, float - ), "Progress value must be a floating point value." - if self._progress_val > value: - # uncolorize range from width*value+margin to width-margin - # from right to left - _prev_pixel = max(2, int(self.width * self._progress_val - 2)) - _new_pixel = max(int(self.width * value - 2), 2) - for _w in range(_prev_pixel, _new_pixel - 1, -1): - for _h in range(2, self.height - 2): - self._bitmap[_w, _h] = 0 - else: - # fill from the previous x pixel to the new x pixel - _prev_pixel = max(2, int(self.width * self._progress_val - 3)) - _new_pixel = min(int(self.width * value - 2), int(self.width * 1.0 - 3)) - for _w in range(_prev_pixel, _new_pixel + 1): - for _h in range(2, self.height - 2): - self._bitmap[_w, _h] = 2 - self._progress_val = value - - @property - def fill(self): - """The fill of the progress bar. Can be a hex value for a color or ``None`` for - transparent. - - """ - return self._palette[0] - - @property - def width(self): - """The width of the progress bar. In pixels, includes the border.""" - return self._bar_width - - @property - def height(self): - """The height of the progress bar. In pixels, includes the border.""" - return self._bar_height - - @fill.setter - def fill(self, color): - """Sets the fill of the progress bar. Can be a hex value for a color or ``None`` for - transparent. - - """ - if color is None: - self._palette[2] = 0 - self._palette.make_transparent(0) - else: - self._palette[2] = color - self._palette.make_opaque(0) diff --git a/adafruit_progressbar/__init__.py b/adafruit_progressbar/__init__.py new file mode 100755 index 0000000..e833f27 --- /dev/null +++ b/adafruit_progressbar/__init__.py @@ -0,0 +1,593 @@ +# SPDX-FileCopyrightText: 2020 Brent Rubell for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`progressbar_base` +================================================================================ + +Dynamic progress bar widget for CircuitPython displays + + +* Author(s): Brent Rubell and Hugo Dahl + +Implementation Notes +-------------------- + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +# imports +try: + from typing import Tuple, Union, List +except ImportError: + pass # No harm if the module isn't located +import displayio + + +class ProgressBarBase(displayio.TileGrid): + """The base class for dynamic progress bar widgets. + + :param position: The coordinates (x, y) of the top left corner + :type position: Tuple[int, int] + :param size: The size (width, height) of the progress bar + :type size: Tuple[int, int] + :param bar_color: The color of the bar representing the value. This can + be a hexadecimal value for color (0x224466). + Default: 0x00FF00 (Solid green) + :type bar_color: int + :param border_color: The color of the border around the progress bar. This + can be a hexadecimal value for color (0x4488BB). + Default: 0xFFFFFF (White) + :type border_color: int + :param fill_color: The colour of the bar representing the remainder of the + value. i.e. if the current value is 42%, the 42 value + is represented by the bar_color parameter. The remainder, + 58%, will be displayed in this color. This can also + be a hexadecimal value for color (0xEE7755). + Default: 0x000000 (Black) + :type fill_color: int + :param margin_size: Specify whether a margin between the border of the widget and the bar + representing the value should be visible or not. + Default: True + :type margin_size: bool + :param value_range: Specify the range of allowed values for which the progress + should be displayed. When setting the "value" property, + this range is the one against which its progression will be determined. + Default: (0.0, 1.0) + :type value_range: Tuple[int, int] or Tuple[float, float] + """ + + # pylint: disable=too-many-arguments, too-many-instance-attributes + def __init__( + self, + position: Tuple[int, int], + size: Tuple[int, int], + value: Union[int, float] = 0, + bar_color=0x00FF00, + border_color=0xFFFFFF, + fill_color=0x000000, + border_thickness: int = 1, + margin_size: int = 1, + value_range: Union[Tuple[int, int], Tuple[float, float]] = (0, 100), + ) -> None: + + assert ( + value_range[0] < value_range[1] + ), "The minimum value must be less than the maximum value" + + assert ( + size[0] > 0 and size[1] > 0 + ), "The width and the height must be greater than zero" + + assert ( + value_range[0] <= value <= value_range[1] + ), "The starting value must be within the range of minimum to maximum" + + _edge_size = 2 * margin_size + 2 * border_thickness + + assert _edge_size < size[0], ( + "The size of the borders and margins combined must be " + "less than the width of the widget" + ) + + assert _edge_size < size[1], ( + "The size of the borders and margins combined must be " + "less than the height of the widget" + ) + + self._progress = 0.0 + self._widget_size = size + self._position = position + + self._bitmap = displayio.Bitmap(size[0], size[1], 3) + self._palette = displayio.Palette(3) + self._border_thickness = border_thickness + self._margin_size = margin_size + self._range = value_range + self._progress = 0.0 + + self._old_value = self.minimum + self._value = self.minimum + + self.fill = fill_color + self.bar_color = bar_color + self.border_color = border_color + + # Setup value and old_value to handle the change to the new + # initial value later. + self._value = self.minimum + self._old_value = self.minimum + + super().__init__( + self._bitmap, + pixel_shader=self._palette, + x=self._position[0], + y=self._position[1], + ) + + self._draw_outline() + self.value = value + + # _bitmap: displayio.Bitmap # The bitmap used for the bar/value + # _position: (int, int) # The (x,y) coordinates of the top-left corner + # _widget_size: (int, int) # The dimensions of the progress bar + # _palette: displayio.Palette(3) # The palette to be used + # _progress: float # The value to represent, between 0.0 and 100.0 + # _border_thickness: int # The thickness of the border around the control, in pixels + # _margin_size: bool # Whether we should display a margin between + # the border and the value/bar + # # The minimum and maximum values we can represent + # _range: (int, int) or (float, float) + + # Color palette index to property mapping: + # 0: Bar fill color + # 1: Border color + # 2: Background fill color + + @property + def widget_size(self) -> int: + """The size at the outer edge of the control, returned as a tuple (width, height) + + :rtype: int + """ + return self._widget_size + + @property + def widget_width(self) -> Tuple[int, int]: + """The total width of the widget, in pixels. Includes the border and margin. + + :rtype: Tuple[int, int] + """ + return self.widget_size[0] + + @property + def border_thickness(self) -> int: + """Gets the currently configured thickness of the border (in pixels) + + :rtype: int + """ + return self._border_thickness + + @property + def widget_height(self) -> int: + """The total height of the widget, in pixels. Includes the border and margin. + + :rtype: int + """ + return self.widget_size[1] + + @property + def border_color(self) -> int: + """Returns the currently configured value for the color of the + outline (border) of the widget. + + :rtype: int + """ + return self._border_color + + @border_color.setter + def border_color(self, color: Union[int, Tuple[int, int, int]]) -> None: + """Sets the color of the border of the widget. Set it to 'None' + if a border should still be part of the widget but not displayed. + + :param color: The color to be used for the border + :type int/None/Tuple[int, int, int]: + + :rtype: None + """ + + assert ( + isinstance(color, int) or color is None + ), "A color must be represented by a integer value" + + self._border_color = color + + if color is None: + self._palette[1] = 0x00 + self._palette.make_transparent(1) + else: + self._palette[1] = color + self._palette.make_opaque(1) + + @property + def fill(self) -> int: + """The fill of the progress bar. Can be a hex value for a color or ``None`` for + transparent. + + :rtype: int + """ + return self._fill_color + + @fill.setter + def fill(self, color: Union[int, Tuple[int, int, int]]) -> None: + """Sets the fill of the progress bar. Can be a hex value for a color or ``None`` for + transparent. + + :param color: The color to use for the widget's background + :type color: int/None/Tuple[int, int, int] + """ + self._fill_color = color + if color is None: + self._palette[0] = 0x00 + self._palette.make_transparent(0) + else: + self._palette[0] = color + self._palette.make_opaque(0) + + @property + def bar_color(self) -> int: + """The color of the bar's fill + + :rtype: int/None + """ + + return self._bar_color + + @bar_color.setter + def bar_color(self, color: Union[int, Tuple[int, int, int]]) -> None: + """Sets the color of the bar + + :param color: The color to use for the bar + :type color: int/None/Tuple[int, int, int] + + :rtype: None + """ + + self._bar_color = color + + if color is None: + self._palette[2] = 0x00 + self._palette.make_transparent(2) + else: + self._palette[2] = color + self._palette.make_opaque(2) + + @property + def value(self) -> Union[int, float]: + """ + The current value of the control, used to determine its progress/ratio + :rtype: int/float + """ + return self._value + + @value.setter + def value(self, value: Union[int, float]) -> None: + """Sets the current value of the progress within the min-max range + + :param value: The new value for the progress status + :type value: int/float + + :rtype: None + """ + + assert isinstance( + value, (int, float) + ), "The value to set must be either an integer or a float" + + assert ( + self.minimum <= value <= self.maximum + ), f"The value must be between minimum ({self.minimum}) and maximum ({self.maximum})" + + # Save off the previous value, so we can pass it in the + # call to "Render" + self._old_value = self._value + self._value = value + # Convert value to float since we may be dealing with + # integer types, and we can't work with integer division + # to get a ratio (position) of "value" within range. + self._set_progress(self.get_value_ratio(value)) + + @property + def progress(self) -> float: + """Gets the current displayed value of the widget. + + :return: The current progress ratio + :rtype: float + """ + return self._progress + + @progress.setter + def progress(self, value: float) -> None: + """Sets the current displayed value of the widget. This will update the + `value` property to an approximation based on the allowed range. The calculation + used to determine the approximate value is + `((self.minimum + (self.maximum - self.minimum)) * progress)`. + For the most accurate representation of a given value, it is recommended to set the + property "value" to the desired value. + + Example: If the range for the widget is 0-10, setting a progress value of "35" + will result in `value` being "3.5", since 3.5 is the 35% value of the range between + 0 and 10. The value determined from this method makes no assumptions or checks based on + the type of the "value" field. + + :param value: The new value which should be displayed by the progress + bar. Must be between 0.0-100.0 + :type value: float + + :rtype: None + """ + + assert [isinstance(value, (float, int)), "'progress' must be an int or a float"] + + assert 0.0 <= value <= 100.0, "'progress' must be between 0 and 100" + + self.value = (self.minimum + (self.maximum - self.minimum)) * (value * 0.01) + + # Bit of a hack to be able to work around the shim "ProgressBar" class + # to be able to handle values as it used to. + def _set_progress(self, value: float) -> None: + """Sets the value for the underlying variable _progress, then + calls self.render() with the appropriate values. + + :param value: The value to which self.progress should be set + :type value: float + :rtype: None + """ + + self._progress = round(value, 4) + self._render(self._old_value, self._value, value) + + @property + def range(self) -> Tuple[Union[int, float], Union[int, float]]: + """The range which can be handled as a Tuple(min,max) + + :rtype: Tuple(int/float, int/float) + """ + return self._range + + @property + def minimum(self) -> Union[int, float]: + """The minimum (lowest) value which can be displayed + + :rtype: int/float + """ + return self.range[0] + + @property + def maximum(self) -> Union[int, float]: + """The maximum (highest) value which can be displayed + + :rtype: int/float + """ + return self.range[1] + + def _draw_outline(self) -> None: + """Draws the outline (border) of the progressbar, with a thickness value + from self.border_thickness. + + :rtype None: + """ + stroke = self.border_thickness + + # draw outline rectangle + for _w in range(self.widget_width): + for line in range(stroke): + self._bitmap[_w, line] = 1 + self._bitmap[_w, self.widget_height - 1 - line] = 1 + for _h in range(self.widget_height): + for line in range(stroke): + self._bitmap[line, _h] = 1 + self._bitmap[self.widget_width - 1 - line, _h] = 1 + + def fill_width(self) -> int: + """Returns the amount of horizontal space within the widget + which can be used for value display. This is typically the + width of the widget as defined, minus any visually reserved space. + + :rtype: int + """ + + return self.widget_width - self._get_fill_border_size() + + def fill_height(self) -> int: + """Returns the amount of vertical space within the widget + which can be used for value display. This is typically the + width of the widget as defined, minus any visually reserved + space. + + :rtype: int + """ + + return self.widget_height - self._get_fill_border_size() + + def _get_fill_border_size(self) -> int: + """Determines any visual space reserved for the widget + based on the defined border thickness, and whether a margin + should be placed between the border and the bar. + The value is calculated as (2 x border_thickness) minus + (2 x margin_size). The value for margin_size is either 0 (zero) + or 1 (one) depending on the value of margin_size when the + widget was created. + + :rtype: int + """ + + return (2 * self.border_thickness) + (2 * self.margin_size) + + @property + def margin_size(self) -> int: + """Returns the size of the margin on a single side of the display + + :return int: + """ + return self._margin_size + + @margin_size.setter + def margin_size(self, value: int) -> None: + """Sets the new size of the margin to be used between the border + (if displayed) and the value bar. + + :param value: The new size of the margin between the border + and value bar on all sides of the widget. + :type value: int + + :rtype: None + """ + + assert isinstance(value, int), "The margin size must be an integer" + + margin_spacing = (2 * value) + (2 * self._border_thickness) + + assert margin_spacing < self.widget_width, ( + "The size of the borders and margins combined can total the same or more" + "than the widget's width." + ) + + assert margin_spacing < self.widget_height, ( + "The size of the borders and margins combined can total the same or more" + "than the widget's height." + ) + + self._margin_size = value + self._set_progress(self._progress) # For a render pass + + def get_value_ratio(self, value: Union[int, float]) -> float: + """Gets the ratio (percentage) of a given value within the + range of self.minimum and self.maximum. + + :param value: The value for which the ration should be calculated + :type value: int/float + + :return: The ratio of value:range + :rtype: float + """ + + if self.maximum == self.minimum: + return 0.0 + + return (float(value) - self.minimum) / (self.maximum - self.minimum) + + @classmethod + def _get_value_sizes(cls, _old_ratio: float, _new_ratio: float) -> Tuple[int, int]: + return 0, 0 + + @classmethod + def _get_max_fill_size(cls) -> int: + return 0 + + def _get_ratios( + self, _old_value: Union[int, float], _new_value: Union[int, float] + ) -> Tuple[float, float]: + return self.get_value_ratio(_old_value), self.get_value_ratio(_new_value) + + def _adjust_size_for_range_limits( + self, _new_value_size: int, _new_value: Union[int, float] + ) -> int: + # If we have *ANY* value other than "zero" (minimum), we should + # have at least one element showing + if _new_value_size == 0 and _new_value > self.minimum: + _new_value_size = 1 + + # Conversely, if we have *ANY* value other than 100% (maximum), + # we should NOT show a full bar. + if _new_value_size == self._get_max_fill_size() and _new_value < self.maximum: + _new_value_size -= 1 + + return _new_value_size + + def _get_sizes_min_max(self) -> Tuple[int, int]: + return 0, min(self.fill_width(), self.fill_height()) + + @classmethod + def _invert_fill_direction(cls) -> bool: + return False + + def _get_horizontal_fill( + self, _start: int, _end: int, _incr: int + ) -> Tuple[int, int, int]: + return 0, self.fill_width(), 1 # Subclass must return values + + def _get_vertical_fill( + self, _start: int, _end: int, _incr: int + ) -> Tuple[int, int, int]: + return 0, self.fill_height(), 1 # Subclass must return values + + # pylint: disable=too-many-locals + def _render( + self, + _old_value: Union[int, float], + _new_value: Union[int, float], + _progress_value: float, + ) -> None: + """ + Does the work of actually creating the graphical representation of + the value (percentage, aka "progress") to be displayed. + + :param _old_value: The previously displayed value + :type _old_value: int/float + :param _new_value: The new value to display + :type _new_value: int/float + :param _progress_value: The value to display, as a percentage, represented + by a float from 0.0 to 1.0 (0% to 100%) + :type _progress_value: float + :rtype: None + """ + + _prev_ratio, _new_ratio = self._get_ratios(_old_value, _new_value) + _old_value_size, _new_value_size = self._get_value_sizes( + _prev_ratio, _new_ratio + ) + + # Adjusts for edge cases, such as 0-width non-zero value, or 100% width + # non-maximum values + _new_value_size = self._adjust_size_for_range_limits( + _new_value_size, _new_value + ) + + # Default values for increasing value + _color = 2 + _incr = 1 + _start = max(_old_value_size, 0) + _end = max(_new_value_size, 0) + + if _old_value_size >= _new_value_size: + # Override defaults to be decreasing + _color = 0 # Clear + _incr = -1 # Iterate range downward + _start = max(_old_value_size, 0) - 1 + _end = max(_new_value_size, 0) - 1 + # If we're setting to minimum, make sure we're clearing by + # starting one "bar" further + if _new_value == self.minimum: + _start += 1 + + _render_offset = self.margin_size + self.border_thickness + + vert_start, vert_end, vert_incr = self._get_vertical_fill(_start, _end, _incr) + horiz_start, horiz_end, horiz_incr = self._get_horizontal_fill( + _start, _end, _incr + ) + + vert_start += _render_offset + vert_end += _render_offset + horiz_start += _render_offset + horiz_end += _render_offset + + for vertical_position in range(vert_start, vert_end, vert_incr): + for horizontal_position in range(horiz_start, horiz_end, horiz_incr): + self._bitmap[horizontal_position, vertical_position] = _color diff --git a/adafruit_progressbar/horizontalprogressbar.py b/adafruit_progressbar/horizontalprogressbar.py new file mode 100755 index 0000000..a82abdb --- /dev/null +++ b/adafruit_progressbar/horizontalprogressbar.py @@ -0,0 +1,169 @@ +# SPDX-FileCopyrightText: 2020 Brent Rubell for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`horizontalprogressbar` +================================================================================ + +Dynamic progress bar widget for CircuitPython displays + + +* Author(s): Brent Rubell, Hugo Dahl + +Implementation Notes +-------------------- + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +try: + from typing import Tuple, Union +except ImportError: + pass # Not needed for execution +from . import ProgressBarBase + + +# pylint: disable=too-few-public-methods +class HorizontalFillDirection: + """This enum is used to specify the direction in which the progress + bar's display bar should fill as the value represented increases.""" + + # pylint: disable=pointless-string-statement + """Fills from the left-hand side toward the right""" + LEFT_TO_RIGHT = 0 + # pylint: disable=pointless-string-statement + """Specifies the default fill direction (LEFT_TO_RIGHT)""" + DEFAULT = LEFT_TO_RIGHT + # pylint: disable=pointless-string-statement + """Fill from the right-hand side toward the left""" + RIGHT_TO_LEFT = 1 + + +class HorizontalProgressBar(ProgressBarBase): + """A dynamic progress bar widget. + + The anchor position is the position where the control would start if it + were being read visually or on paper, where the (0, 0) position is + the lower-left corner for ascending progress bars (fills from the bottom to + to the top in vertical bars, or from the left to the right in horizontal + progress bars), upper-left corner for descending progress bars (fills from + the top to the bottom). + + Using the diagrams below, the bar will fill in the following directions:: + + -------------------------------- + | Left-to-right | 1-3 to 2-4 | + -------------------------------- + | Right-to-left | 2-4 to 1-3 | + -------------------------------- + + Horizontal + + 1-----------------------2 + | | + | | + 3-----------------------4 + + + :param position: The coordinates of the top-left corner of progress bar. + :type position: Tuple[int, int] + :param size: The size in (width, height) of the progress bar, in pixels + :type size: Tuple[int, int] + :param min_value: The lowest value which can be displayed by the progress bar. + When the "value" property is set to the same value, no bar is displayed. + :type min_value: int, float + :param max_value: This highest value which can be displayed by the progress bar. + When the "value" property is set to the same value, the bar shows as full. + :type max_value: int, float + :param value: The starting value to be displayed. Must be between the values of + min_value and max_value, inclusively. + :type value: int, float + :param bar_color: The color of the value portion of the progress bar. + Can be a hex value for color (i.e. 0x225588). + :type bar_color: int, Tuple[byte, byte, byte] + :param outline_color: The colour for the outline of the progress bar. + Can be a hex value for color (i.e. 0x225588). + :type outline_color: int, Tuple[byte, byte, byte] + :param fill_color: The colour for the background within the progress bar. + Can be a hex value for color (i.e. 0x225588). + :type fill_color: int, Tuple[byte, byte, byte] + :param border_thickness: The thickness of the outer border of the widget. If it is + 1 or larger, will be displayed with the color of the "outline_color" parameter. + :type border_thickness: int + :param margin_size: The thickness (in pixels) of the margin between the border and + the bar. If it is 1 or larger, will be filled in by the color of the + "fill_color" parameter. + :type margin_size: int + :param direction: The direction of the fill + :type direction: HorizontalFillDirection + + """ + + # pylint: disable=too-many-arguments + def __init__( + self, + position: Tuple[int, int], + size: Tuple[int, int], + min_value: Union[int, float] = 0, + max_value: Union[int, float] = 100, + value: Union[int, float] = 0, + bar_color: Union[int, Tuple[int, int, int]] = 0x00FF00, + outline_color: Union[int, Tuple[int, int, int]] = 0xFFFFFF, + fill_color: Union[int, Tuple[int, int, int]] = 0x444444, + border_thickness: int = 1, + margin_size: int = 1, + direction: HorizontalFillDirection = HorizontalFillDirection.DEFAULT, + ) -> None: + + # Store the "direction" value locally. While they may appear to + # "relate" with the values of the vertical bar, their handling + # is too different to be stored in the same underlying property, + # which could lead to confusion + self._direction = direction + + super().__init__( + position, + size, + value, + bar_color, + outline_color, + fill_color, + border_thickness, + margin_size, + (min_value, max_value), + ) + + def _get_sizes_min_max(self) -> Tuple[int, int]: + return 0, self.fill_width() + + def _get_value_sizes(self, _old_ratio: float, _new_ratio: float) -> Tuple[int, int]: + return int(_old_ratio * self.fill_width()), int(_new_ratio * self.fill_width()) + + def _get_horizontal_fill( + self, _start: int, _end: int, _incr: int + ) -> Tuple[int, int, int]: + if not self._invert_fill_direction(): + return _start, _end, _incr + + base_offset = self.fill_width() - 1 + + return base_offset - _start, base_offset - _end, _incr * -1 + + # pylint: disable=protected-access + def _get_vertical_fill( + self, _start: int, _end: int, _incr: int + ) -> Tuple[int, int, int]: + return ProgressBarBase._get_vertical_fill(self, _start, _end, _incr) + + # pylint: enable=protected-access + + def _invert_fill_direction(self) -> bool: + return self._direction == HorizontalFillDirection.RIGHT_TO_LEFT + + def _get_max_fill_size(self): + return self.fill_width() diff --git a/adafruit_progressbar/progressbar.py b/adafruit_progressbar/progressbar.py new file mode 100755 index 0000000..5f1eeb3 --- /dev/null +++ b/adafruit_progressbar/progressbar.py @@ -0,0 +1,105 @@ +# SPDX-FileCopyrightText: 2020 Brent Rubell for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`progressbar` +================================================================================ + +Dynamic progress bar widget for CircuitPython displays + + +* Author(s): Brent Rubell + +Implementation Notes +-------------------- + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +# imports +from adafruit_progressbar.horizontalprogressbar import HorizontalProgressBar + + +# pylint: disable=too-many-arguments, too-few-public-methods +class ProgressBar(HorizontalProgressBar): + """A dynamic progress bar widget. + + NOTE: This class is made available for backward compatibility with v1.x of + the adafruit_progressbar library. New uses should not use this class, but + instead, use its successor, HorizontalProgressBar. + + :param x: The x-position of the top left corner. + :type x: int + :param y: The y-position of the top left corner. + :type y: int + :param width: The width of the progress bar. + :type width: int + :param height: The height of the progress bar. + :type height: int + :param bar_color: The color of the progress bar. Can be a hex + value for color. + :param outline_color: The outline of the progress bar. Can be a hex + value for color. + :type outline_color: int + :param stroke: Used for the outline_color + :type stroke: int + """ + + # pylint: disable=invalid-name + def __init__( + self, + x: int, + y: int, + width: int, + height: int, + progress: float = 0.0, + bar_color=0x00FF00, + outline_color=0xFFFFFF, + stroke: int = 1, + ) -> None: + + # This needs to remain for backward compatibility, the default ProgressBar class + # should only be able to handle values of type "float" + assert isinstance(progress, float), "Progress must be a floating point value." + + super().__init__( + (x, y), + (width, height), + 0.0, + 1.0, + progress, + bar_color, + outline_color, + 0x000000, + border_thickness=stroke, + ) + + # Override the base "progress" property to correctly handle values + # in the v1 range of 0.0-1.0 + @property + def progress(self) -> float: + """Gets the progress value displayed + + :rtype float: + """ + return self._progress + + @progress.setter + def progress(self, value: float) -> None: + """Sets the progress value for display + + :param value: The progress value to be set, between 0.0 and 1.0 + :type value: float + + :rtype: None + """ + + # Disable pylint since the property "value" is defined in the + # base class "ProgressBarBase" + # pylint: disable=access-member-before-definition + self.value = value diff --git a/adafruit_progressbar/verticalprogressbar.py b/adafruit_progressbar/verticalprogressbar.py new file mode 100755 index 0000000..31c5927 --- /dev/null +++ b/adafruit_progressbar/verticalprogressbar.py @@ -0,0 +1,135 @@ +# SPDX-FileCopyrightText: Copyright (c) 2020-2021 Brent Rubell for Adafruit Industries, Hugo Dahl +# +# SPDX-License-Identifier: MIT + +""" +`verticalprogressbar` +================================================================================ + +Dynamic progress bar widget for CircuitPython displays + + +* Author(s): Brent Rubell, Hugo Dahl + +Implementation Notes +-------------------- + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +try: + from typing import Tuple +except ImportError: + pass # Not needed for execution +from . import ProgressBarBase +from .horizontalprogressbar import HorizontalProgressBar + + +# pylint: disable=too-few-public-methods +class VerticalFillDirection: + """This enum is used to specify the direction in which the progress + bar's display bar should fill as the value represented increases.""" + + # pylint: disable=pointless-string-statement + """Fills from the bottom up toward the top""" + BOTTOM_TO_TOP = 0 + # pylint: disable=pointless-string-statement + """Default fill direction (BOTTOM_TO_TOP)""" + DEFAULT = BOTTOM_TO_TOP + # pylint: disable=pointless-string-statement + """Fills from the top down toward the bottom""" + TOP_TO_BOTTOM = 1 + + +# pylint: disable=too-many-arguments, too-few-public-methods, too-many-instance-attributes +class VerticalProgressBar(HorizontalProgressBar): + """A dynamic progress bar widget. + + The anchor position is the position where the control would start if it + were being read visually or on paper, where the (0, 0) position is + the lower-left corner for ascending progress bars (fills from the bottom to + to the top in vertical bars, or from the left to the right in horizontal + progress bars), upper-left corner for descending progress bars (fills from + the top to the bottom). + + Using the diagrams below, the bar will fill in the following directions:: + + -------------------------------- + | Bottom-to-top | 3-4 to 1-2 | + -------------------------------- + | Top-to-bottom | 1-2 to 3-4 | + -------------------------------- + + 1--2 + | | + | | + | | + | | + 3--4 + + :param position: The coordinates of the top-left corner of progress bar. + :type position: Tuple[int, int] + :param size: The size in (width, height) of the progress bar, in pixels + :type size: Tuple[int, int] + :param min_value: The lowest value which can be displayed by the progress bar. + When the "value" property is set to the same value, no bar is displayed. + :type min_value: int, float + :param max_value: This highest value which can be displayed by the progress bar. + When the "value" property is set to the same value, the bar shows as full. + :type max_value: int, float + :param value: The starting value to be displayed. Must be between the values of + min_value and max_value, inclusively. + :type value: int, float + :param bar_color: The color of the value portion of the progress bar. + Can be a hex value for color (i.e. 0x225588). + :type bar_color: int, Tuple[byte, byte, byte] + :param outline_color: The colour for the outline of the progress bar. + Can be a hex value for color (i.e. 0x225588). + :type outline_color: int, Tuple[byte, byte, byte] + :param fill_color: The colour for the background within the progress bar. + Can be a hex value for color (i.e. 0x225588). + :type fill_color: int, Tuple[byte, byte, byte] + :param border_thickness: The thickness of the outer border of the widget. If it is + 1 or larger, will be displayed with the color of the "outline_color" parameter. + :type border_thickness: int + :param margin_size: The thickness (in pixels) of the margin between the border and + the bar. If it is 1 or larger, will be filled in by the color of the + "fill_color" parameter. + :type margin_size: int + :param direction: The direction of the fill + :type direction: VerticalFillDirection + + """ + + def _get_sizes_min_max(self) -> Tuple[int, int]: + return 0, self.fill_height() + + def _get_value_sizes(self, _old_ratio: float, _new_ratio: float) -> Tuple[int, int]: + return int(_old_ratio * self.fill_height()), int( + _new_ratio * self.fill_height() + ) + + # pylint: disable=protected-access + def _get_horizontal_fill( + self, _start: int, _end: int, _incr: int + ) -> Tuple[int, int, int]: + return ProgressBarBase._get_horizontal_fill(self, _start, _end, _incr) + + # pylint: enable=protected-access + + def _get_vertical_fill( + self, _start: int, _end: int, _incr: int + ) -> Tuple[int, int, int]: + if not self._invert_fill_direction(): + return _start, _end, _incr + + base_offset = self.fill_height() - 1 + + return base_offset - _start, base_offset - _end, _incr * -1 + + def _invert_fill_direction(self) -> bool: + return self._direction == VerticalFillDirection.BOTTOM_TO_TOP diff --git a/docs/api.rst b/docs/api.rst index 7a3376a..c25e47d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,8 +1,22 @@ +API Definition +--------------------- .. If you created a package, create one automodule per module in the package. .. If your library file(s) are nested in a directory (e.g. /adafruit_foo/foo.py) .. use this format as the module name: "adafruit_foo.foo" -.. automodule:: adafruit_progressbar +.. automodule:: adafruit_progressbar.horizontalprogressbar :members: + :show-inheritance: + :inherited-members: + +.. automodule:: adafruit_progressbar.progressbar + :members: + :show-inheritance: + :inherited-members: + +.. automodule:: adafruit_progressbar.verticalprogressbar + :members: + :show-inheritance: + :inherited-members: diff --git a/examples/progressbar_combined.py b/examples/progressbar_combined.py new file mode 100644 index 0000000..ce3c2a1 --- /dev/null +++ b/examples/progressbar_combined.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: 2021 Hugo Dahl for Adafruit Industries +# SPDX-License-Identifier: MIT + +# Before you can run this example, you will need to install the +# required libraries identifies in the `requirements.txt` file. +# You can do so automatically by using the "pip" utility. + +import time +import board +import displayio +from adafruit_progressbar.horizontalprogressbar import HorizontalProgressBar +from adafruit_progressbar.verticalprogressbar import VerticalProgressBar + +# Make the display context +splash = displayio.Group(max_size=10) +board.DISPLAY.show(splash) + +# set horizontal progress bar width and height relative to board's display +h_width = board.DISPLAY.width - 40 +h_height = 30 + +v_width = 30 +v_height = 140 + +h_x = 20 +h_y = 20 + +v_x = 60 +v_y = 70 + +# Create a new progress_bar objects at their x, y locations +progress_bar = HorizontalProgressBar((h_x, h_y), (h_width, h_height), 0, 100) +vert_progress_bar = VerticalProgressBar((v_x, v_y), (v_width, v_height), 0, 200) + +# Append progress_bars to the splash group +splash.append(progress_bar) +splash.append(vert_progress_bar) + +current_progress = 0.0 +while True: + # range end is exclusive so we need to use 1 bigger than max number that we want + for current_progress in range(0, 101, 1): + print("Progress: {}%".format(current_progress)) + progress_bar.value = current_progress + vert_progress_bar.value = current_progress * 2 + time.sleep(0.01) + time.sleep(0.3) + # reset to empty + progress_bar.value = 0 + vert_progress_bar.value = 0 + time.sleep(0.3) diff --git a/examples/progressbar_displayio_blinka.py b/examples/progressbar_displayio_blinka.py new file mode 100644 index 0000000..a4e5739 --- /dev/null +++ b/examples/progressbar_displayio_blinka.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: 2021 Hugo Dahl for Adafruit Industries +# SPDX-License-Identifier: MIT + +# Before you can run this example, you will need to install the +# required libraries identifies in the `requirements.txt` file. +# You can do so automatically by using the "pip" utility. + +import time +import sys +import displayio +from blinka_displayio_pygamedisplay import PyGameDisplay +from adafruit_progressbar.progressbar import ProgressBar +from adafruit_progressbar.horizontalprogressbar import ( + HorizontalProgressBar, + HorizontalFillDirection, +) +from adafruit_progressbar.verticalprogressbar import ( + VerticalProgressBar, + VerticalFillDirection, +) + +display = PyGameDisplay(width=320, height=240, auto_refresh=False) +splash = displayio.Group(max_size=10) +display.show(splash) + +color_bitmap = displayio.Bitmap(display.width, display.height, 1) +color_palette = displayio.Palette(1) +color_palette[0] = 0x2266AA # Teal-ish-kinda + +bg_sprite = displayio.TileGrid(color_bitmap, pixel_shader=color_palette, x=0, y=0) +splash.append(bg_sprite) + +progress_bar = ProgressBar( + width=180, + height=40, + x=10, + y=20, + progress=0.0, + bar_color=0x1100FF, + outline_color=0x777777, +) +splash.append(progress_bar) + +horizontal_bar = HorizontalProgressBar( + (10, 80), + (180, 40), + fill_color=0x778822, + outline_color=0x0000FF, + bar_color=0x00FF00, + direction=HorizontalFillDirection.LEFT_TO_RIGHT, +) +splash.append(horizontal_bar) + +horizontal_thermometer = HorizontalProgressBar( + (10, 140), + (180, 40), + value=-10, + min_value=(-40), + max_value=130, + fill_color=0x00FF00, + outline_color=0x0000FF, + bar_color=0x22BB88, + direction=HorizontalFillDirection.RIGHT_TO_LEFT, +) +splash.append(horizontal_thermometer) + +vertical_bar = VerticalProgressBar( + (200, 30), + (32, 180), + direction=VerticalFillDirection.TOP_TO_BOTTOM, +) +splash.append(vertical_bar) + +vertical_thermometer = VerticalProgressBar( + (260, 30), + (32, 180), + min_value=-40, + max_value=130, + direction=VerticalFillDirection.BOTTOM_TO_TOP, +) +splash.append(vertical_thermometer) + +test_value_range_1 = [99, 100, 99, 1, 0, 1] +test_value_range_2 = [120, 130, 129, -20, -39, -40, -28] +delay = 2 +_incr = 1 + +# Must check display.running in the main loop! +while display.running: + + print("\nDemonstration of legacy functionality and syntax, increment by 0.01") + for val in range(0, 101): + if not display.running: + sys.exit(0) + _use_value = round((val * 0.01), 2) + if (val % 10) == 0: + print(f"Value has reached {_use_value:2}") + progress_bar.progress = round(_use_value, 2) + display.refresh() + time.sleep(0.05) + + print("\nDemonstration of values between 0 and 100 - Horizontal and vertical") + for val in test_value_range_1: + if not display.running: + sys.exit(0) + print(f"Setting value to {val}") + vertical_bar.value = val + horizontal_bar.value = val + display.refresh() + time.sleep(delay) + + print("\nDemonstration of Fahrenheit range -40 and 130 - Horizontal and vertical") + for val in test_value_range_2: + if not display.running: + sys.exit(0) + print(f"Setting value to {val}") + vertical_thermometer.value = val + horizontal_thermometer.value = val + display.refresh() + time.sleep(delay) diff --git a/examples/progressbar_magtag_simpletest.py b/examples/progressbar_magtag_simpletest.py index 4930902..f002b79 100644 --- a/examples/progressbar_magtag_simpletest.py +++ b/examples/progressbar_magtag_simpletest.py @@ -9,7 +9,7 @@ import board import displayio import digitalio -from adafruit_progressbar import ProgressBar +from adafruit_progressbar.progressbar import HorizontalProgressBar # use built in display (PyPortal, PyGamer, PyBadge, CLUE, etc.) # see guide for setting up external displays (TFT / OLED breakouts, RGB matrices, etc.) @@ -17,10 +17,15 @@ display = board.DISPLAY time.sleep(display.time_to_refresh) -# a button will be used to advance the progress -a_btn = digitalio.DigitalInOut(board.BUTTON_A) -a_btn.direction = digitalio.Direction.INPUT -a_btn.pull = digitalio.Pull.UP +# B/up button will be used to increase the progress +up_btn = digitalio.DigitalInOut(board.BUTTON_B) +up_btn.direction = digitalio.Direction.INPUT +up_btn.pull = digitalio.Pull.UP + +# C/down button will be used to increase the progress +down_btn = digitalio.DigitalInOut(board.BUTTON_C) +down_btn.direction = digitalio.Direction.INPUT +down_btn.pull = digitalio.Pull.UP # Make the display context splash = displayio.Group(max_size=10) @@ -34,33 +39,57 @@ y = display.height // 3 # Create a new progress_bar object at (x, y) -progress_bar = ProgressBar( - x, y, BAR_WIDTH, BAR_HEIGHT, 1.0, bar_color=0x666666, outline_color=0xFFFFFF +progress_bar = HorizontalProgressBar( + (x, y), + (BAR_WIDTH, BAR_HEIGHT), + bar_color=0xFFFFFF, + outline_color=0xAAAAAA, + fill_color=0x777777, ) # Append progress_bar to the splash group splash.append(progress_bar) -current_progress = (time.monotonic() % 101) / 100.0 +# Get a random starting value within our min/max range +current_progress = time.monotonic() % 101 print(current_progress) -progress_bar.progress = current_progress +progress_bar.value = current_progress # refresh the display display.refresh() -prev_a = a_btn.value +value_incrementor = 3 + +prev_up = up_btn.value +prev_down = down_btn.value while True: - cur_a = a_btn.value - # if a_btn was just pressed down - if not cur_a and prev_a: - current_progress += 0.20 - if current_progress > 1.0: - current_progress = 0.0 + cur_up = up_btn.value + cur_down = down_btn.value + do_refresh = False + # if up_btn was just pressed down + if not cur_up and prev_up: + current_progress += value_incrementor + # Wrap if we get over the maximum value + if current_progress > progress_bar.maximum: + current_progress = progress_bar.minimum + + do_refresh = True + + if not cur_down and prev_down: + current_progress -= value_incrementor + # Wrap if we get below the minimum value + if current_progress < progress_bar.minimum: + current_progress = progress_bar.maximum + + do_refresh = True + + if do_refresh: print(current_progress) - progress_bar.progress = current_progress + progress_bar.value = current_progress time.sleep(display.time_to_refresh) display.refresh() time.sleep(display.time_to_refresh) - prev_a = cur_a + prev_up = cur_up + prev_down = cur_down diff --git a/examples/progressbar_matrixportal.py b/examples/progressbar_matrixportal.py new file mode 100644 index 0000000..ab69eac --- /dev/null +++ b/examples/progressbar_matrixportal.py @@ -0,0 +1,185 @@ +# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# SPDX-License-Identifier: MIT + +# Source: https://github.com/ajs256/matrixportal-weather-display + +# ############## IMPORTS ############### + +# HARDWARE +import time +import board + +# DISPLAY +import displayio # Main display library +import framebufferio # For showing things on the display +import rgbmatrix # For talking to matrices specifically + +# CONTROLS + +import digitalio + +from adafruit_progressbar.horizontalprogressbar import ( + HorizontalProgressBar, + HorizontalFillDirection, +) +from adafruit_progressbar.verticalprogressbar import ( + VerticalProgressBar, + VerticalFillDirection, +) + +# ############## DISPLAY SETUP ############### + +# If there was a display before (protomatter, LCD, or E-paper), release it so +# we can create ours +displayio.release_displays() + +print("Setting up RGB matrix") + +# This next call creates the RGB Matrix object itself. It has the given width +# and height. +# +# These lines are for the Matrix Portal. If you're using a different board, +# check the guide to find the pins and wiring diagrams for your board. +# If you have a matrix with a different width or height, change that too. +matrix = rgbmatrix.RGBMatrix( + width=64, + height=32, + bit_depth=3, + rgb_pins=[ + board.MTX_R1, + board.MTX_G1, + board.MTX_B1, + board.MTX_R2, + board.MTX_G2, + board.MTX_B2, + ], + addr_pins=[board.MTX_ADDRA, board.MTX_ADDRB, board.MTX_ADDRC, board.MTX_ADDRD], + clock_pin=board.MTX_CLK, + latch_pin=board.MTX_LAT, + output_enable_pin=board.MTX_OE, +) + +# Associate the RGB matrix with a Display so that we can use displayio features +display = framebufferio.FramebufferDisplay(matrix) + +print("Adding display group") +group = displayio.Group(max_size=5) # Create a group to hold all our labels +display.show(group) + +print("Creating progress bars and adding to group") + +# A horizontal percentage progress bar, valued at 60% +progress_bar = HorizontalProgressBar((2, 8), (40, 8), value=60) +group.insert(0, progress_bar) + +# Another progress bar, with explicit range and fill from the right +ranged_bar = HorizontalProgressBar( + (2, 20), + (40, 8), + value=40, + min_value=0, + max_value=100, + direction=HorizontalFillDirection.RIGHT_TO_LEFT, +) +group.insert(1, ranged_bar) + +# Sample thermometer from -40C to 50C, with a value of +15C +vertical_bar = VerticalProgressBar( + (44, 4), + (8, 24), + min_value=-40, + max_value=50, + value=15, + bar_color=0x1111FF, + fill_color=None, + margin_size=0, + outline_color=0x2222AA, +) +group.insert(2, vertical_bar) + +vertical_bar2 = VerticalProgressBar( + (54, 4), + (8, 24), + min_value=-40, + max_value=50, + value=15, + bar_color=0x1111FF, + fill_color=None, + margin_size=0, + outline_color=0x2222AA, + direction=VerticalFillDirection.TOP_TO_BOTTOM, +) +group.insert(3, vertical_bar2) + +# Countdown to the start of the bars demo +countdown_bar = HorizontalProgressBar( + (2, 2), + (20, 5), + 0, + 5, + value=5, + bar_color=0x11FF11, + fill_color=0x333333, + border_thickness=0, + margin_size=0, +) + +countdown_end_color = 0xFF1111 + +group.insert(4, countdown_bar) +# group.insert(0, countdown_bar) + +print("Progress bars added. Starting demo...") + +print("Using countdown bar") + +for timer in range(countdown_bar.maximum, countdown_bar.minimum, -1): + bar_color_to_set = (0x20 * (6 - timer) + 20, (0x20 * (timer - 1)) + 20, 0x10) + countdown_bar.bar_color = bar_color_to_set + countdown_bar.value = timer + time.sleep(1) + +print("Removing countdown bar") + +countdown_bar.value = 0 +group.remove(countdown_bar) + +progress_bar_value = 0.0 +progress_bar_incr = 3.0 + +button1 = digitalio.DigitalInOut(board.BUTTON_UP) +button1.switch_to_input(digitalio.Pull.UP) +button2 = digitalio.DigitalInOut(board.BUTTON_DOWN) +button2.switch_to_input(digitalio.Pull.UP) + + +print("Start forever loop") +while True: + + print("Setting progress bar value to", progress_bar_value) + + progress_bar.value = progress_bar_value + ranged_bar.value = progress_bar_value + progress_bar_value += progress_bar_incr + + if not (button1.value and button2.value): + + if not button1.value: # "UP" button pushed + print("UP button pressed. Increasing vertical bars by 3") + vertical_bar.value = min(vertical_bar.maximum, vertical_bar.value + 3) + vertical_bar2.value = min(vertical_bar2.maximum, vertical_bar2.value + 3) + + if not button2.value: # "DOWN" button pushed + print("DOWN button pressed. Decreasing vertical bars by 3") + vertical_bar.value = max(vertical_bar.minimum, vertical_bar.value - 3) + vertical_bar2.value = max(vertical_bar2.minimum, vertical_bar2.value - 3) + + if progress_bar_value > progress_bar.maximum: + progress_bar_value = progress_bar.maximum + progress_bar_incr *= -1 + + if progress_bar_value < progress_bar.minimum: + progress_bar_value = progress_bar.minimum + progress_bar_incr *= -1 + + time.sleep(0.5) diff --git a/examples/progressbar_simpletest.py b/examples/progressbar_simpletest.py index ae0c817..de3cb90 100644 --- a/examples/progressbar_simpletest.py +++ b/examples/progressbar_simpletest.py @@ -4,7 +4,10 @@ import time import board import displayio -from adafruit_progressbar import ProgressBar +from adafruit_progressbar.horizontalprogressbar import ( + HorizontalProgressBar, + HorizontalFillDirection, +) # Make the display context splash = displayio.Group(max_size=10) @@ -18,18 +21,20 @@ y = board.DISPLAY.height // 3 # Create a new progress_bar object at (x, y) -progress_bar = ProgressBar(x, y, width, height, 1.0) +progress_bar = HorizontalProgressBar( + (x, y), (width, height), direction=HorizontalFillDirection.LEFT_TO_RIGHT +) # Append progress_bar to the splash group splash.append(progress_bar) -current_progress = 0.0 +current_value = progress_bar.minimum while True: # range end is exclusive so we need to use 1 bigger than max number that we want - for current_progress in range(0, 101, 1): - print("Progress: {}%".format(current_progress)) - progress_bar.progress = current_progress / 100 # convert to decimal + for current_value in range(progress_bar.minimum, progress_bar.maximum + 1, 1): + print("Progress: {}%".format(current_value)) + progress_bar.value = current_value time.sleep(0.01) time.sleep(0.3) - progress_bar.progress = 0.0 + progress_bar.value = progress_bar.minimum time.sleep(0.3) diff --git a/examples/progressbar_vertical_simpletest.py b/examples/progressbar_vertical_simpletest.py new file mode 100644 index 0000000..8018ed0 --- /dev/null +++ b/examples/progressbar_vertical_simpletest.py @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# SPDX-License-Identifier: MIT + +import time +import board +import displayio +from adafruit_progressbar.verticalprogressbar import ( + VerticalProgressBar, + VerticalFillDirection, +) + +# Make the display context +splash = displayio.Group(max_size=10) +board.DISPLAY.show(splash) + +# set progress bar width and height relative to board's display +width = 10 +height = board.DISPLAY.height - 40 + +x = width * 2 +y = 10 + +# Create a new VerticalProgressBar object at (x, y) +vertical_progress_bar = VerticalProgressBar( + (x, y), (width, height), direction=VerticalFillDirection.TOP_TO_BOTTOM +) + +# Append progress_bar to the splash group +splash.append(vertical_progress_bar) + +x = x * 2 +# Create a second VerticalProgressBar object at (x+20, y) +vertical_progress_bar2 = VerticalProgressBar( + (x, y), (width, height), direction=VerticalFillDirection.BOTTOM_TO_TOP +) + +# Append progress_bar to the splash group +splash.append(vertical_progress_bar2) + + +current_progress = 0.0 +while True: + # range end is exclusive so we need to use 1 bigger than max number that we want + for current_progress in range(0, 101, 1): + print("Progress: {}%".format(current_progress)) + vertical_progress_bar.value = current_progress + vertical_progress_bar2.value = current_progress + time.sleep(0.05) + time.sleep(0.3) + vertical_progress_bar.value = vertical_progress_bar.minimum + vertical_progress_bar2.value = vertical_progress_bar.minimum + time.sleep(0.3) diff --git a/examples/requirements.txt b/examples/requirements.txt new file mode 100644 index 0000000..ad621e7 --- /dev/null +++ b/examples/requirements.txt @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +# We're using a separate "requirements.txt" file for the +# examples, to prevent the circup tool from attempting to +# copy the library to the device. + +Adafruit-Blinka +adafruit-blinka-displayio +blinka-displayio-pygamedisplay diff --git a/requirements.txt b/requirements.txt index 17a850d..9e78ada 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,8 @@ # SPDX-License-Identifier: Unlicense Adafruit-Blinka +adafruit-blinka-displayio + +# This library is only needed for use with Blinka. Should not +# be retrieved or used for boards through Circup +# blinka-displayio-pygamedisplay diff --git a/setup.py b/setup.py index 4a7b577..f564714 100644 --- a/setup.py +++ b/setup.py @@ -54,5 +54,5 @@ # simple. Or you can use find_packages(). # TODO: IF LIBRARY FILES ARE A PACKAGE FOLDER, # CHANGE `py_modules=['...']` TO `packages=['...']` - py_modules=["adafruit_progressbar"], + packages=["adafruit_progressbar"], )