From 29c86ceb153f67f7929b7779dabf3eaa1b2322dd Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Fri, 29 Mar 2024 13:45:22 -0300 Subject: [PATCH] [Navigate Results][Added] A header and navigation bar Closes #582 --- CHANGELOG.md | 1 + docs/samples/generic_plot.kibot.yaml | 16 ++ docs/source/Changelog.rst | 1 + .../outputs/navigate_results.rst | 12 + docs/source/dependencies.rst | 1 + kibot/bom/html_writer.py | 8 +- kibot/misc.py | 20 +- kibot/out_navigate_results.py | 245 +++++++++++++++++- src/kibot-check | 9 +- 9 files changed, 293 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd24401fa..71968bebd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 3D/2D renderers: support for ranges in the `show_components` and `highlight` options. So one entry can be something like *R10-R20*. Can be disabled using the global option `allow_component_ranges`. (See yaqwsx/PcbDraw#159) +- Navigate results: A header and navigation bar (#582) ### Changed - CI/CD: we now filter some warnings that are always generated by docker diff --git a/docs/samples/generic_plot.kibot.yaml b/docs/samples/generic_plot.kibot.yaml index 549d034d4..e9941b271 100644 --- a/docs/samples/generic_plot.kibot.yaml +++ b/docs/samples/generic_plot.kibot.yaml @@ -1744,12 +1744,28 @@ outputs: type: 'navigate_results' dir: 'Example/navigate_results_dir' options: + # [boolean=true] Add a header containing information for the project + header: true # [string=''] The name of a file to create at the main output directory linking to the home page link_from_root: '' + # [string|boolean=''] PNG file to use as logo, use false to remove. + # The KiBot logo is used by default + logo: '' + # [string='https://github.com/INTI-CMNB/KiBot/'] Target link when clicking the logo + logo_url: 'https://github.com/INTI-CMNB/KiBot/' + # [boolean=true] Add a side navigation bar to quickly access to the outputs + nav_bar: true # [string='%f-%i%I%v.%x'] Filename for the output (%i=html, %x=navigate). Affected by global options output: '%f-%i%I%v.%x' # [boolean=false] Skip outputs with `run_by_default: false` skip_not_run: false + # [string=''] Title for the page, when empy KiBot will try using the schematic or PCB title. + # If they are empty the name of the project, schematic or PCB file is used. + # You can use %X values and KiCad variables here + title: '' + # [string|boolean=''] Target link when clicking the title, use false to remove. + # KiBot will try with the origin of the current git repo when empty + title_url: '' # Netlist: # The netlist can be generated in the classic format and in IPC-D-356 format, # useful for board testing diff --git a/docs/source/Changelog.rst b/docs/source/Changelog.rst index 1df649244..c422791f9 100644 --- a/docs/source/Changelog.rst +++ b/docs/source/Changelog.rst @@ -25,6 +25,7 @@ Added ``highlight`` options. So one entry can be something like *R10-R20*. Can be disabled using the global option ``allow_component_ranges``. (See yaqwsx/PcbDraw#159) +- Navigate results: A header and navigation bar (#582) Changed ~~~~~~~ diff --git a/docs/source/configuration/outputs/navigate_results.rst b/docs/source/configuration/outputs/navigate_results.rst index ce2594fd5..5bcb986ce 100644 --- a/docs/source/configuration/outputs/navigate_results.rst +++ b/docs/source/configuration/outputs/navigate_results.rst @@ -25,7 +25,19 @@ Parameters: - **link_from_root** :index:`: ` [string=''] The name of a file to create at the main output directory linking to the home page. - **output** :index:`: ` [string='%f-%i%I%v.%x'] Filename for the output (%i=html, %x=navigate). Affected by global options. + - ``header`` :index:`: ` [boolean=true] Add a header containing information for the project. + - ``logo`` :index:`: ` [string|boolean=''] PNG file to use as logo, use false to remove. + The KiBot logo is used by default. + + - ``logo_url`` :index:`: ` [string='https://github.com/INTI-CMNB/KiBot/'] Target link when clicking the logo. + - ``nav_bar`` :index:`: ` [boolean=true] Add a side navigation bar to quickly access to the outputs. - ``skip_not_run`` :index:`: ` [boolean=false] Skip outputs with `run_by_default: false`. + - ``title`` :index:`: ` [string=''] Title for the page, when empy KiBot will try using the schematic or PCB title. + If they are empty the name of the project, schematic or PCB file is used. + You can use %X values and KiCad variables here. + - ``title_url`` :index:`: ` [string|boolean=''] Target link when clicking the title, use false to remove. + KiBot will try with the origin of the current git repo when empty. + - **type** :index:`: ` 'navigate_results' - ``category`` :index:`: ` [string|list(string)=''] The category for this output. If not specified an internally defined category is used. diff --git a/docs/source/dependencies.rst b/docs/source/dependencies.rst index d62884624..6fbd856cf 100644 --- a/docs/source/dependencies.rst +++ b/docs/source/dependencies.rst @@ -86,6 +86,7 @@ - Compare with files in the repo for `diff` - Find commit hash and/or date for `kikit_present` - Compare with files in the repo for `kiri` + - Find origin url for `navigate_results` - Find commit hash and/or date for `pcb_replace` - Find commit hash and/or date for `sch_replace` - Find commit hash and/or date for `set_text_variables` diff --git a/kibot/bom/html_writer.py b/kibot/bom/html_writer.py index f393e10d3..52a23044b 100644 --- a/kibot/bom/html_writer.py +++ b/kibot/bom/html_writer.py @@ -10,9 +10,9 @@ """ import os from base64 import b64encode -from struct import unpack from .columnlist import ColumnList, BoMError from .kibot_logo import KIBOT_LOGO, KIBOT_LOGO_W, KIBOT_LOGO_H +from ..misc import read_png BG_GEN = "#DCF5E4" BG_KICAD = "#F5DCA9" @@ -273,11 +273,9 @@ def content_table(html, groups, headings, head_names, cfg, link_datasheet, link_ def embed_image(file): - with open(file, 'rb') as f: - s = f.read() - if not (s[:8] == b'\x89PNG\r\n\x1a\n' and (s[12:16] == b'IHDR')): + s, w, h = read_png(file) + if s is None: raise BoMError('Only PNG images are supported for the logo') - w, h = unpack('>LL', s[16:24]) return int(w), int(h), 'data:image/png;base64,'+b64encode(s).decode('ascii') diff --git a/kibot/misc.py b/kibot/misc.py index feb48f4ad..400a4babf 100644 --- a/kibot/misc.py +++ b/kibot/misc.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2020-2023 Salvador E. Tropea -# Copyright (c) 2020-2023 Instituto Nacional de Tecnología Industrial -# License: GPL-3.0 +# Copyright (c) 2020-2024 Salvador E. Tropea +# Copyright (c) 2020-2024 Instituto Nacional de Tecnología Industrial +# License: AGPL-3.0 # Project: KiBot (formerly KiPlot) """ Miscellaneous definitions """ -import re -import os from contextlib import contextmanager +import os +import re +from struct import unpack # Error levels @@ -442,3 +443,12 @@ def hide_stderr(): def version_str2tuple(ver): return tuple(map(int, ver.split('.'))) + + +def read_png(file): + with open(file, 'rb') as f: + s = f.read() + if not (s[:8] == b'\x89PNG\r\n\x1a\n' and (s[12:16] == b'IHDR')): + return None, None, None + w, h = unpack('>LL', s[16:24]) + return s, w, h diff --git a/kibot/out_navigate_results.py b/kibot/out_navigate_results.py index 712b3fa3a..ef8114194 100644 --- a/kibot/out_navigate_results.py +++ b/kibot/out_navigate_results.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2022-2023 Salvador E. Tropea -# Copyright (c) 2022-2023 Instituto Nacional de Tecnología Industrial -# License: GPL-3.0 +# Copyright (c) 2022-2024 Salvador E. Tropea +# Copyright (c) 2022-2024 Instituto Nacional de Tecnología Industrial +# License: AGPL-3.0 # Project: KiBot (formerly KiPlot) # The Assembly image is a composition from Pixlok and oNline Web Fonts # The rest are KiCad icons @@ -17,7 +17,10 @@ role: Create outputs preview - from: ImageMagick role: Create outputs preview + - from: Git + role: Find origin url """ +import base64 import os import subprocess import pprint @@ -25,10 +28,12 @@ from math import ceil from struct import unpack from tempfile import NamedTemporaryFile +from .bom.kibot_logo import KIBOT_LOGO, KIBOT_LOGO_W, KIBOT_LOGO_H +from .error import KiPlotConfigurationError from .gs import GS -from .optionable import BaseOptions -from .kiplot import config_output, get_output_dir -from .misc import W_NOTYET, W_MISSTOOL, W_NOOUTPUTS +from .optionable import Optionable, BaseOptions +from .kiplot import config_output, get_output_dir, run_command +from .misc import W_NOTYET, W_MISSTOOL, W_NOOUTPUTS, read_png from .registrable import RegOutput from .macros import macros, document, output_class # noqa: F401 from . import log, __version__ @@ -108,6 +113,7 @@ IMAGEABLES_SIMPLE = {'png', 'jpg'} IMAGEABLES_GS = {'pdf', 'eps', 'ps'} IMAGEABLES_SVG = {'svg'} +TITLE_HEIGHT = 30 STYLE = """ .cat-table { margin-left: auto; margin-right: auto; } .cat-table td { padding: 20px 24px; } @@ -139,6 +145,84 @@ .generator { text-align: right; font-size: 0.6em; } a:link, a:visited { text-decoration: none;} a:hover, a:active { text-decoration: underline;} +/* The side navigation menu */ +.sidenav { + height: 100%; /* 100% Full-height */ + width: 0; /* 0 width - change this with JavaScript */ + position: fixed; /* Stay in place */ + z-index: 1; /* Stay on top */ + top: 0; /* Stay at the top */ + left: 0; + background-color: #0e4e8e; /* Black*/ + overflow-x: hidden; /* Disable horizontal scroll */ + padding-top: 60px; /* Place content 60px from the top */ + transition: 0.5s; /* 0.5 second transition effect to slide in the sidenav */ +} +/* The navigation menu links */ +.sidenav a { + padding: 8px 8px 8px 8px; + text-decoration: none; + font-size: 20px; + color: #f1f1f1; + display: block; + transition: 0.3s; +} +/* When you mouse over the navigation links, change their color */ +.sidenav a:hover { + color: #ff0000; +} +/* Position and style the close button (top right corner) */ +.sidenav .closebtn { + position: absolute; + top: 0; + right: 8px; + font-size: 36px; + margin-left: 50px; +} +/* Style page content - use this if you want to push the page content to the right when you open the side navigation */ +#main { + transition: margin-left .5s; + padding: 20px; + margin-top: @TOP_MAR@px; +} +/* On smaller screens, where height is less than 450px, change the style of the sidenav (less padding and a smaller font + size) */ +@media screen and (max-height: 450px) { + .sidenav {padding-top: 15px;} + .sidenav a {font-size: 18px;} +} +ul { + display: block; + list-style-type: none; + margin-block-start: -1em; + margin-block-end: 0em; + margin-inline-start: 0px; + margin-inline-end: 0px; + padding-inline-start: 10px; +} +.topmenu { + overflow: hidden; + position: fixed; /* Set the navbar to fixed position */ + top: 0; /* Position the navbar at the top of the page */ + width: 100%; /* Full width */ +} +""" +SCRIPT = """ + """ @@ -174,10 +258,54 @@ def __init__(self): """ *The name of a file to create at the main output directory linking to the home page """ self.skip_not_run = False """ Skip outputs with `run_by_default: false` """ + self.logo = Optionable + """ [string|boolean=''] PNG file to use as logo, use false to remove. + The KiBot logo is used by default """ + self.logo_url = 'https://github.com/INTI-CMNB/KiBot/' + """ Target link when clicking the logo """ + self.title = '' + """ Title for the page, when empty KiBot will try using the schematic or PCB title. + If they are empty the name of the project, schematic or PCB file is used. + You can use %X values and KiCad variables here """ + self.title_url = Optionable + """ [string|boolean=''] Target link when clicking the title, use false to remove. + KiBot will try with the origin of the current git repo when empty """ + self.nav_bar = True + """ Add a side navigation bar to quickly access to the outputs """ + self.header = True + """ Add a header containing information for the project """ super().__init__() self._expand_id = 'navigate' self._expand_ext = 'html' + def config(self, parent): + super().config(parent) + # Logo + if isinstance(self.logo, type): + self.logo = '' + elif isinstance(self.logo, bool): + self.logo = '' if self.logo else None + elif self.logo: + self.logo = os.path.abspath(self.logo) + if not os.path.isfile(self.logo): + raise KiPlotConfigurationError('Missing logo file `{}`'.format(self.logo)) + self._logo_data, self._logo_w, self._logo_h = read_png(self.logo) + if self._logo_data is None: + raise KiPlotConfigurationError('Only PNG images are supported for the logo') + if self.logo == '': + # Internal logo + self._logo_w = int(KIBOT_LOGO_W/2) + self._logo_h = int(KIBOT_LOGO_H/2) + self._logo_data = base64.b64decode(KIBOT_LOGO) + elif self.logo is None: + self._logo_w = self._logo_h = 0 + self._logo_data = '' + # Title URL + if isinstance(self.title_url, type): + self.title_url = '' + elif isinstance(self.title_url, bool): + self.title_url = '' if self.title_url else None + def add_to_tree(self, cat, out, o_tree): # Add `out` to `o_tree` in the `cat` category cat = cat.split('/') @@ -336,8 +464,11 @@ def add_back_home(self, f, prev): format(self.home, self.home_img, MID_ICON, MID_ICON)) f.write(' ') f.write('') - f.write('

Generated by KiBot v{}

'. + f.write('

Generated by KiBot v{}

\n'. format(__version__)) + f.write('\n') + if self.nav_bar: + f.write(SCRIPT) def write_head(self, f, title): f.write('\n') @@ -349,6 +480,9 @@ def write_head(self, f, title): f.write(' \n') f.write('\n') f.write('\n') + f.write(self.navbar) + f.write(self.top_menu) + f.write('
\n') def generate_cat_page_for(self, name, node, prev, category): logger.debug('- Categories: '+str(node.keys())) @@ -381,7 +515,7 @@ def generate_outputs(self, f, node): for oname, out in node.items(): if isinstance(out, dict): continue - f.write('\n') + f.write(f'
\n') out_name = oname.replace(' ', '_') oname = oname.replace('_', ' ') oname = oname[0].upper()+oname[1:] @@ -485,6 +619,88 @@ def create_tree(self): self.add_to_tree(c, o, o_tree) return o_tree + def generate_navbar_one(self, node, lvl, name, ext): + """ Recursively create a menu containing all outputs. + Using ul and li items """ + indent = ' '+' '*lvl + code = indent+'
    \n' + indent += ' ' + for k, v in node.items(): + if isinstance(v, dict): + new_name = name+'_'+k + code += indent+f'
  • {k}
  • \n' + code += self.generate_navbar_one(v, lvl+1, new_name, ext) + else: + code += indent+f'
  • {v.name}
  • \n' + code += indent[:-1]+'
\n' + return code + + def generate_navbar(self, node, name): + name, ext = os.path.splitext(name) + code = '
\n' + code += '×\n' + code += self.generate_navbar_one(node, 0, name, ext) + code += '
\n' + return code + + def generate_top_menu(self): + # Div for the top info + fsize = f'{TITLE_HEIGHT}px' + code = '
\n' + code += '
\n' + code += ' \n' + code += ' \n' + code += ' \n' + code += ' \n' + code += ' \n' + code += '
\n' + if self.nav_bar: + code += f' \n' + code += ' \n' + if self.logo is not None and self.header: + img_name = os.path.join('images', 'logo.png') + if self.logo_url: + code += f' \n' + code += ' Logo\n' + if self.logo_url: + code += ' \n' + code += ' \n' + if self.header: + if self.title_url: + code += f' \n' + code += f' {self._solved_title}\n' + if self.title_url: + code += ' \n' + code += '
\n' + code += '
\n' + return code + + def solve_title(self): + base_title = None + if GS.sch: + base_title = GS.sch.get_title() + if GS.board and not base_title: + tb = GS.board.GetTitleBlock() + base_title = tb.GetTitle() + if not base_title: + base_title = GS.pro_basename or GS.sch_basename or GS.pcb_basename or 'Unknown' + text = self.expand_filename_sch(self.title if self.title else '+') + if text[0] == '+': + text = base_title+text[1:] + self._solved_title = text + # Now the URL + if self.title_url is not None and not self.title_url: + # Empty but not None + self._git_command = self.check_tool('Git') + if self._git_command: + res = '' + try: + res = run_command([self._git_command, 'remote', 'get-url', 'origin'], just_raise=True) + except subprocess.CalledProcessError: + pass + if res: + self.title_url = res + def run(self, name): self.out_dir = os.path.dirname(name) self.img_src_dir = GS.get_resource_path('images') @@ -499,7 +715,11 @@ def run(self, name): logger.warning(W_NOOUTPUTS+'No outputs for navigate results') return with open(os.path.join(self.out_dir, 'styles.css'), 'wt') as f: - f.write(STYLE) + if not self.header: + top_margin = 0 if not self.nav_bar else TITLE_HEIGHT + else: + top_margin = str(max(self._logo_h, TITLE_HEIGHT)) + f.write(STYLE.replace('@TOP_MAR@', str(top_margin))) self.rsvg_command = self.check_tool('rsvg1') self.convert_command = self.check_tool('ImageMagick') self.ps2img_avail = self.check_tool('Ghostscript') @@ -508,6 +728,13 @@ def run(self, name): self.back_img = self.copy('back', MID_ICON) self.home_img = self.copy('home', MID_ICON) copy2(os.path.join(self.img_src_dir, 'favicon.ico'), os.path.join(self.out_dir, 'favicon.ico')) + # Copy the logo image + if self.logo is not None and self.header: + with open(os.path.join(self.out_dir, 'images', 'logo.png'), 'wb') as f: + f.write(self._logo_data) + self.solve_title() + self.navbar = self.generate_navbar(o_tree, name) if self.nav_bar else '' + self.top_menu = self.generate_top_menu() if self.nav_bar or self.header else '' self.generate_page_for(o_tree, name) # Link it? if self.link_from_root: diff --git a/src/kibot-check b/src/kibot-check index f5a7c8f75..1ce5bc13d 100755 --- a/src/kibot-check +++ b/src/kibot-check @@ -187,7 +187,7 @@ deps = '{\ "extra_arch": null,\ "extra_deb": null,\ "help_option": "--version",\ - "importance": 6,\ + "importance": 7,\ "in_debian": true,\ "is_kicad_plugin": false,\ "is_python": false,\ @@ -219,6 +219,13 @@ deps = '{\ "output": "kiri",\ "version": null\ },\ + {\ + "desc": "Find origin url",\ + "mandatory": false,\ + "max_version": null,\ + "output": "navigate_results",\ + "version": null\ + },\ {\ "desc": "Find commit hash and/or date",\ "mandatory": false,\