diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..6c8db4c --- /dev/null +++ b/.clang-format @@ -0,0 +1,117 @@ +# Copyright (C) 2016 Olivier Goffart +# SPDX-License-Identifier: BSD-3-Clause + +# This is the clang-format configuration style to be used by Qt, +# based on the rules from https://wiki.qt.io/Qt_Coding_Style and +# https://wiki.qt.io/Coding_Conventions + +--- +# Webkit style was loosely based on the Qt style +BasedOnStyle: WebKit + +Standard: c++17 + +# Column width is limited to 100 in accordance with Qt Coding Style. +# https://wiki.qt.io/Qt_Coding_Style +# Note that this may be changed at some point in the future. +ColumnLimit: 100 +# How much weight do extra characters after the line length limit have. +# PenaltyExcessCharacter: 4 + +# Disable reflow of some specific comments +# qdoc comments: indentation rules are different. +# Translation comments and SPDX license identifiers are also excluded. +# REUSE-IgnoreStart +CommentPragmas: "^!|^:|^ SPDX-License-Identifier:" +# REUSE-IgnoreEnd + +# We want a space between the type and the star for pointer types. +PointerBindsToType: false + +# We generally use "template <" with space. +SpaceAfterTemplateKeyword: true + +# We want to break before the operators, but not before a '='. +BreakBeforeBinaryOperators: NonAssignment + +# Braces are usually attached, but not after functions or class declarations. +BreakBeforeBraces: Custom +BraceWrapping: + AfterClass: true + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: true + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + +# When constructor initializers do not fit on one line, put them each on a new line. +ConstructorInitializerAllOnOneLineOrOnePerLine: true +# Indent initializers by 4 spaces +ConstructorInitializerIndentWidth: 4 + +# Indent width for line continuations. +ContinuationIndentWidth: 8 + +# No indentation for namespaces. +NamespaceIndentation: None + +# Allow indentation for preprocessing directives (if/ifdef/endif). https://reviews.llvm.org/rL312125 +IndentPPDirectives: AfterHash +# We only indent with 2 spaces for preprocessor directives +PPIndentWidth: 2 + +# Horizontally align arguments after an open bracket. +# The coding style does not specify the following, but this is what gives +# results closest to the existing code. +AlignAfterOpenBracket: true +AlwaysBreakTemplateDeclarations: true + +# Ideally we should also allow less short function in a single line, but +# clang-format does not handle that. +AllowShortFunctionsOnASingleLine: Inline + +# The coding style specifies some include order categories, but also tells to +# separate categories with an empty line. It does not specify the order within +# the categories. Since the SortInclude feature of clang-format does not +# re-order includes separated by empty lines, the feature is not used. +SortIncludes: false + +# macros for which the opening brace stays attached. +ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH, forever, Q_FOREVER, QBENCHMARK, QBENCHMARK_ONCE ] + +# Break constructor initializers before the colon and after the commas. +BreakConstructorInitializers: BeforeColon + +# Add "// namespace " comments on closing brace for a namespace +# Ignored for namespaces that qualify as a short namespace, +# see 'ShortNamespaceLines' +FixNamespaceComments: true + +# Definition of how short a short namespace is, default 1 +ShortNamespaceLines: 1 + +# When escaping newlines in a macro attach the '\' as far left as possible, e.g. +##define a \ +# something; \ +# other; \ +# thelastlineislong; +AlignEscapedNewlines: Left + +# Avoids the addition of a space between an identifier and the +# initializer list in list-initialization. +SpaceBeforeCpp11BracedList: false + +# Do not reflow any comment +ReflowComments: false + +--- +# Use the Google-based style for .proto files. +Language: Proto +BasedOnStyle: Google +IndentWidth: 4 +ColumnLimit: 100 diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml new file mode 100644 index 0000000..1c662ce --- /dev/null +++ b/.github/workflows/ci-linux.yml @@ -0,0 +1,32 @@ +name: CI Linux + +on: + push: + branches: [ main, master ] + pull_request: + +jobs: + build-and-test: + # Ubuntu 24.04 ships Qt 6.4, which is the minimum the CMakeLists asks for. + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 # full history + tags, for the version from git describe + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + cmake ninja-build g++ \ + qt6-base-dev \ + libgtest-dev + + - name: Configure + run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build build + + - name: Test + run: ctest --test-dir build --output-on-failure diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml new file mode 100644 index 0000000..2a3ed81 --- /dev/null +++ b/.github/workflows/ci-macos.yml @@ -0,0 +1,35 @@ +name: CI (macOS) + +on: + push: + branches: [ main, master ] + pull_request: + +jobs: + build-and-test: + # macos-14 is Apple Silicon; the clang_64 Qt kit is a universal build, so it works there. + runs-on: macos-14 + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 # full history + tags, for the version from git describe + + - name: Install Qt + uses: jurplel/install-qt-action@v4 + with: + version: 6.8.3 + host: mac + arch: clang_64 + cache: true + + - name: Install build tools + run: brew install ninja googletest + + - name: Configure + run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build build + + - name: Test + run: ctest --test-dir build --output-on-failure diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml new file mode 100644 index 0000000..91eefca --- /dev/null +++ b/.github/workflows/ci-windows.yml @@ -0,0 +1,47 @@ +name: CI (Windows) + +on: + push: + branches: [ main, master ] + pull_request: + +jobs: + build-and-test: + runs-on: windows-2022 + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 # full history + tags, for the version from git describe + + - name: Install Qt + uses: jurplel/install-qt-action@v4 + with: + version: 6.8.3 + host: windows + arch: win64_mingw + tools: tools_mingw1310 + cache: true + + # The runner's own gcc would not match the ABI of Qt's MinGW kit, so put the kit's first. + - name: Use Qt's MinGW toolchain + shell: bash + run: echo "$(cygpath -u "$QT_ROOT_DIR/../../Tools/mingw1310_64/bin")" >> "$GITHUB_PATH" + + # No packaged GTest for Qt's MinGW kit, so build one with it. + - name: Install GTest + shell: bash + run: | + git clone --depth 1 --branch v1.15.2 https://github.com/google/googletest.git "$RUNNER_TEMP/googletest" + cmake -S "$RUNNER_TEMP/googletest" -B "$RUNNER_TEMP/googletest/build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$RUNNER_TEMP/gtest" + cmake --build "$RUNNER_TEMP/googletest/build" + cmake --install "$RUNNER_TEMP/googletest/build" + echo "GTest_ROOT=$RUNNER_TEMP/gtest" >> "$GITHUB_ENV" + + - name: Configure + run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build build + + - name: Test + run: ctest --test-dir build --output-on-failure diff --git a/.gitignore b/.gitignore index b9d6bd9..163301c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,215 +1,61 @@ -################# -## Eclipse -################# - -*.pydevproject -.project -.metadata -bin/ -tmp/ -*.tmp -*.bak -*.swp -*~.nib -local.properties -.classpath -.settings/ -.loadpath - -# External tool builders -.externalToolBuilders/ - -# Locally stored "Eclipse launch configurations" -*.launch - -# CDT-specific -.cproject - -# PDT-specific -.buildpath - - -################# -## Visual Studio -################# - -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.sln.docstates - -# Build results - -[Dd]ebug/ -[Rr]elease/ -x64/ -build/ -[Bb]in/ -[Oo]bj/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -*_i.c -*_p.c -*.ilk -*.meta +# Build directories (out-of-source: cmake -B build) +/build/ +/build-*/ +/cmake-build-*/ +/out/ + +# In-source CMake build leftovers +/CMakeCache.txt +/CMakeFiles/ +/CTestTestfile.cmake +/cmake_install.cmake +/Makefile +/install_manifest.txt +/compile_commands.json +/Testing/ +/_deps/ +*_autogen/ +/.qt/ + +# Built binaries (in-source builds) +/Registerer +/Registerer.exe +/bitformat_test +/bitformat_test.exe + +# Qt +CMakeLists.txt.user* +*.pro.user* +*.autosave +*.qm +moc_*.cpp +moc_*.h +qrc_*.cpp +ui_*.h +.qmake.stash + +# Compiled objects and libraries +*.o *.obj -*.pch +*.a +*.lib +*.so +*.so.* +*.dylib +*.dll +*.exe *.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.log -*.scc - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -*.ncrunch* -.*crunch*.local.xml - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.Publish.xml -*.pubxml - -# NuGet Packages Directory -## TODO: If you have NuGet Package Restore enabled, uncomment the next line -#packages/ - -# Windows Azure Build Output -csx -*.build.csdef - -# Windows Store app package directory -AppPackages/ +*.ilk -# Others -sql/ -*.Cache -ClientBin/ -[Ss]tyle[Cc]op.* -~$* +# IDEs and editors +.vs/ +.vscode/ +.idea/ +.cache/ +*.swp *~ -*.dbmdl -*.[Pp]ublish.xml -*.pfx -*.publishsettings - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file to a newer -# Visual Studio version. Backup files are not needed, because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -# SQL Server files -App_Data/*.mdf -App_Data/*.ldf - -############# -## Windows detritus -############# - -# Windows image file caches +# OS junk +.DS_Store Thumbs.db -ehthumbs.db - -# Folder config file Desktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Mac crap -.DS_Store - - -############# -## Python -############# - -*.py[co] - -# Packages -*.egg -*.egg-info -dist/ -build/ -eggs/ -parts/ -var/ -sdist/ -develop-eggs/ -.installed.cfg - -# Installer logs -pip-log.txt - -# Unit test / coverage reports -.coverage -.tox - -#Translations -*.mo - -#Mr Developer -.mr.developer.cfg diff --git a/CMakeLists.txt b/CMakeLists.txt index 505657f..1e0a710 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,23 +1,34 @@ -cmake_minimum_required(VERSION 3.5.1) +cmake_minimum_required(VERSION 3.16) -project(Registerer) +project(Registerer LANGUAGES CXX) -# Find includes in corresponding build directories -set(CMAKE_INCLUDE_CURRENT_DIR ON) -# Instruct CMake to run moc and uic automatically when needed. -set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTOUIC ON) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Find the QtWidgets library -find_package(Qt5 COMPONENTS Widgets) +find_package(Qt6 6.4 REQUIRED COMPONENTS Widgets) -# Tell CMake to create the helloworld executable -add_executable(Registerer WIN32 mainwindow.ui mainwindow.cpp main.cpp) +# Turns on AUTOMOC/AUTOUIC/AUTORCC and the usual output layout. +qt_standard_project_setup() -# Use the Widgets module from Qt 5. -target_link_libraries(Registerer Qt5::Widgets) +qt_add_executable(Registerer WIN32 + main.cpp + mainwindow.cpp + mainwindow.h + mainwindow.ui + bitformat.h +) + +target_link_libraries(Registerer PRIVATE Qt6::Widgets) install( TARGETS Registerer RUNTIME DESTINATION bin ) + +# Tests for the Qt-free display logic; needs no Qt and no display. +enable_testing() +find_package(GTest REQUIRED) +add_executable(bitformat_test bitformat_test.cpp) +target_link_libraries(bitformat_test PRIVATE GTest::gtest_main) +include(GoogleTest) +gtest_discover_tests(bitformat_test) diff --git a/README b/README deleted file mode 100644 index d13a271..0000000 --- a/README +++ /dev/null @@ -1,50 +0,0 @@ -Registerer -======= - -Registerer (c) 2013 Mike Bergmann - - - -Registerer is a Hex/Bin/Dec converter aimed at embedded developers who have to deal with hardware -registers a lot. It's developed using the Qt Framework. - -License: - - Registerer is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 2 of the License, or - (at your option) any later version. - - Registerer is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Registerer. If not, see . - - -Usage: - (is pretty simple) - - Enter a number in the number field and press Enter. - Prefix the number with 0b in case you enter a binary, or 0x if you enter a hex number. - CTRL-d (or the d[isplay] button) will toggle through dec-hex-bin. - - You may show a bit-position pointer by entering the position in the pos field. - CTRL-t (or the t[oggle] button) will toggle the bit at the position pointed by 'Pos'. - -Examples: - - Convert binary 10101010 to hex: - Enter 0b10101010 into 'Number' and press CTRL-d twice. - - Create a hex number with bit 31 set to one: - - Enter 0x0 into 'Number' to indicate you want a hex. - Enter 31 into 'Pos' and press CTRL-t (or the t button). - - Create a binary number with bit 15 set to one: - - Enter 0b0 into 'Number' to indicate you want a binary. - Enter 15 into 'Pos' and press CTRL-t (or the t button). diff --git a/README.md b/README.md index aa7b955..83a2df3 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,12 @@ registers a lot. It's developed using the Qt Framework. ![Screenshot](./registerer.png) +**Build:** + +Needs Qt 6.3 or newer and a C++20 compiler. + + cmake -B build && cmake --build build && ctest --test-dir build + **License:** Registerer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. diff --git a/Registerer.pro b/Registerer.pro deleted file mode 100644 index 01d19f7..0000000 --- a/Registerer.pro +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2013 Mike Bergmann, Bones AG -# -# This file is part of Registerer. -# -# Registerer is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# Registerer is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Registerer. If not, see . - -QT += core gui -greaterThan(QT_MAJOR_VERSION, 4): QT += widgets - -TARGET = Registerer -TEMPLATE = app - -SOURCES += main.cpp\ - mainwindow.cpp - -HEADERS += mainwindow.h - -FORMS += mainwindow.ui diff --git a/bitformat.h b/bitformat.h new file mode 100644 index 0000000..9e8b493 --- /dev/null +++ b/bitformat.h @@ -0,0 +1,131 @@ +/* + Copyright 2013-2026 Mike Bergmann + + This file is part of Registerer. + + Registerer is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Registerer is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Registerer. If not, see . +*/ + +// Pure number <-> text logic + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace registerer { + +inline constexpr int kBits = 64; + +enum class Base : int { Bin = 2, Dec = 10, Hex = 16 }; + +struct Value +{ + std::uint64_t number = 0; + Base base = Base::Dec; + + friend bool operator==(const Value &, const Value &) = default; +}; + +inline std::string digits(std::uint64_t n, Base base) +{ + char buf[kBits + 1]; + const auto [end, ec] = std::to_chars(buf, buf + sizeof buf, n, static_cast(base)); + return std::string(buf, end); // to_chars cannot overflow a 65 byte buffer +} + +inline std::string format(Value v) +{ + const std::string_view prefix = v.base == Base::Bin ? "0b" : v.base == Base::Hex ? "0x" : ""; + return std::string(prefix) + digits(v.number, v.base); +} + +inline std::optional parse(std::string_view text) +{ + constexpr std::string_view kSpace = " \t"; + const auto last = text.find_last_not_of(kSpace); + if (last == std::string_view::npos) + return std::nullopt; // empty or all blanks + const auto first = text.find_first_not_of(kSpace); + text = text.substr(first, last - first + 1); + + Base base = Base::Dec; + if (text.starts_with("0b") || text.starts_with("0B")) { + base = Base::Bin; + text.remove_prefix(2); + } else if (text.starts_with("0x") || text.starts_with("0X")) { + base = Base::Hex; + text.remove_prefix(2); + } + + std::uint64_t n = 0; + const auto [end, ec] = + std::from_chars(text.data(), text.data() + text.size(), n, static_cast(base)); + if (ec != std::errc{} || end != text.data() + text.size()) + return std::nullopt; + return Value{ .number = n, .base = base }; +} + +constexpr Base next(Base base) +{ + return base == Base::Dec ? Base::Hex : base == Base::Hex ? Base::Bin : Base::Dec; +} + +constexpr std::uint64_t mask(int bit) +{ + return bit >= 0 && bit < kBits ? std::uint64_t{ 1 } << bit : 0; +} + +constexpr std::uint64_t toggle(std::uint64_t n, int bit) +{ + return n ^ mask(bit); +} + +struct Rows +{ + std::string bin, pos, hex, dec; +}; + +inline Rows layout(std::uint64_t value, int caret) +{ + constexpr std::string_view kDigits = "0123456789abcdef"; + const int nibbles = std::max(1, (std::bit_width(value) + 3) / 4); + + Rows r; + for (int n = nibbles - 1; n >= 0; --n) { + for (int b = 3; b >= 0; --b) { + const int bit = 4 * n + b; + r.bin += static_cast('0' + ((value >> bit) & 1)); + r.pos += bit == caret ? '^' : ' '; + r.hex += b == 0 ? kDigits[(value >> (4 * n)) & 0xF] : ' '; + } + r.bin += ' '; + r.pos += ' '; + r.hex += ' '; + } + + r.dec = digits(value, Base::Dec); + const std::size_t width = r.bin.size() - 1; // ignore the trailing group gap + if (r.dec.size() < width) + r.dec.insert(0, width - r.dec.size(), ' '); + return r; +} + +} // namespace registerer diff --git a/bitformat_test.cpp b/bitformat_test.cpp new file mode 100644 index 0000000..eb4e4e2 --- /dev/null +++ b/bitformat_test.cpp @@ -0,0 +1,104 @@ +/* + Copyright 2013-2026 Mike Bergmann + + This file is part of Registerer. + + Registerer is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Registerer is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Registerer. If not, see . +*/ + +#include "bitformat.h" + +#include + +using namespace registerer; + +TEST(Bitformat, Parse) +{ + EXPECT_TRUE(parse("0b1010") == (Value{ .number = 10, .base = Base::Bin })); + EXPECT_TRUE(parse("0xFF") == (Value{ .number = 255, .base = Base::Hex })); + EXPECT_TRUE(parse(" 42 ") == (Value{ .number = 42, .base = Base::Dec })); + EXPECT_TRUE(parse("0B11") == (Value{ .number = 3, .base = Base::Bin })); +} + +TEST(Bitformat, ParseRejectsInvalid) +{ + EXPECT_FALSE(parse("")); + EXPECT_FALSE(parse(" ")); + EXPECT_FALSE(parse("0b12")); // 2 is not a binary digit + EXPECT_FALSE(parse("12ab")); // trailing garbage + EXPECT_FALSE(parse("-1")); // unsigned only + EXPECT_FALSE(parse("0x1ffffffffffffffff")); // 65 bits: refused, not truncated +} + +TEST(Bitformat, FullRangeRoundTrips) +{ + const Value max{ .number = UINT64_MAX, .base = Base::Hex }; + EXPECT_EQ(format(max), "0xffffffffffffffff"); + EXPECT_TRUE(parse(format(max)) == max); +} + +TEST(Bitformat, BaseCycle) +{ + EXPECT_EQ(next(Base::Dec), Base::Hex); + EXPECT_EQ(next(Base::Hex), Base::Bin); + EXPECT_EQ(next(Base::Bin), Base::Dec); +} + +TEST(Bitformat, Toggle) +{ + EXPECT_EQ(format({ .number = toggle(0, 31), .base = Base::Hex }), "0x80000000"); + EXPECT_EQ(format({ .number = toggle(0, 15), .base = Base::Bin }), "0b1000000000000000"); + EXPECT_EQ(toggle(toggle(0xF0, 3), 3), 0xF0u); + EXPECT_EQ(mask(63), std::uint64_t{ 1 } << 63); + EXPECT_EQ(mask(64), 0u); + EXPECT_EQ(mask(-1), 0u); +} + +TEST(Bitformat, Layout) +{ + const Rows r = layout(0xAA, 3); + EXPECT_EQ(r.bin, "1010 1010 "); + EXPECT_EQ(r.pos, " ^ "); + EXPECT_EQ(r.hex, " a a "); + EXPECT_EQ(r.dec, " 170"); + EXPECT_EQ(r.dec.size(), r.bin.size() - 1); +} + +TEST(Bitformat, LayoutEdgeCases) +{ + EXPECT_EQ(layout(0, 0).bin, "0000 "); + EXPECT_EQ(layout(0, 0).pos, " ^ "); + + EXPECT_EQ(layout(0x1FF, 8).bin, "0001 1111 1111 "); + EXPECT_EQ(layout(0x1FF, 8).pos, " ^ "); + + EXPECT_EQ(layout(1, 40).pos, " "); +} + +TEST(Bitformat, LayoutWidest) +{ + // Widest case: every column stays aligned. + const Rows wide = layout(UINT64_MAX, 63); + EXPECT_EQ(wide.bin.size(), 16u * 5); + EXPECT_EQ(wide.pos.size(), wide.bin.size()); + EXPECT_EQ(wide.hex.size(), wide.bin.size()); + EXPECT_EQ(wide.pos.front(), '^'); + EXPECT_EQ(wide.hex, + std::string(3, ' ') + + "f f f f f f f f" + " f f f f f f f" + " f "); + EXPECT_EQ(wide.dec.size(), wide.bin.size() - 1); + EXPECT_TRUE(wide.dec.ends_with("18446744073709551615")); +} diff --git a/main.cpp b/main.cpp index fa17883..5bb06c2 100644 --- a/main.cpp +++ b/main.cpp @@ -1,5 +1,5 @@ /* - Copyright 2013 Mike Bergmann, Bones AG + Copyright 2013-2026 Mike Bergmann This file is part of Registerer. diff --git a/mainwindow.cpp b/mainwindow.cpp index 98835f1..bdf3e4c 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -1,5 +1,5 @@ /* - Copyright 2013 Mike Bergmann, Bones AG + Copyright 2013-2026 Mike Bergmann This file is part of Registerer. @@ -18,157 +18,92 @@ */ #include "mainwindow.h" + +#include "bitformat.h" #include "ui_mainwindow.h" -#include -#include -MainWindow::MainWindow(QWidget *parent) : - QMainWindow(parent), - ui(new Ui::MainWindow) +#include +#include +#include + +namespace { + +QString toQt(const std::string &s) { - ui->setupUi(this); - ui->lVersion->setText(tr("Version") + " " + __DATE__ + " " + __TIME__); - ui->lLink->setOpenExternalLinks(true); + return QString::fromStdString(s); } -MainWindow::~MainWindow() +} // namespace + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent), ui(std::make_unique()) { - delete ui; + ui->setupUi(this); + ui->lVersion->setText(tr("Version") + " " __DATE__ " " __TIME__); + ui->lLink->setOpenExternalLinks(true); + + // The rows only line up under a fixed-width font, and the "Courier New" of + // the .ui file is not installed everywhere. + const QFont mono = QFontDatabase::systemFont(QFontDatabase::FixedFont); + for (QLabel *row : { ui->lBinary, ui->lPos, ui->lHex, ui->lDec }) + row->setFont(mono); + + refresh(); } +MainWindow::~MainWindow() = default; + void MainWindow::changeEvent(QEvent *e) { - QMainWindow::changeEvent(e); - switch (e->type()) { - case QEvent::LanguageChange: - ui->retranslateUi(this); - break; - default: - break; - } + QMainWindow::changeEvent(e); + if (e->type() == QEvent::LanguageChange) + ui->retranslateUi(this); } -void MainWindow::on_lineEdit_editingFinished() +void MainWindow::refresh() { - bool ok; - unsigned long long number; - - if(ui->lineEdit->text().startsWith("0b")) { - number = ui->lineEdit->text().right(ui->lineEdit->text().length()-2).toULongLong(&ok, 2); - ui->lBitAsHex->setText(QString("0b")+QString::number((unsigned long long)qPow(2,ui->spinBox->value()),2)); - } else if(ui->lineEdit->text().startsWith("0x")) { - number = ui->lineEdit->text().right(ui->lineEdit->text().length()-2).toULongLong(&ok, 16); - ui->lBitAsHex->setText(QString("0x")+QString::number((unsigned long long)qPow(2,ui->spinBox->value()),16)); - } else { - number = ui->lineEdit->text().toULongLong(&ok, 10); - ui->lBitAsHex->setText(QString::number((unsigned long long)qPow(2,ui->spinBox->value()))); - } - - ui->lBinary->setText(""); - ui->lPos->setText(""); - ui->lHex->setText(""); - - QString bin = QString().setNum(number, 2); - QString hex = QString().setNum(number, 16); - QString zero("00000"); - - if(bin.length()%4) - bin.insert(0,zero.data(),4-bin.length()%4); - - QString pos; - pos.fill(' ',bin.length()); - - if(ui->spinBox->value() < bin.length()) - pos[pos.length() - ui->spinBox->value() - 1] = '^'; - - for(int i=0;ilBinary->setText(ui->lBinary->text()+bin.at(i)); - ui->lPos->setText(ui->lPos->text()+pos.at(i)); - if((i+1)%4==0) { - ui->lHex->setText(ui->lHex->text()+hex.at((i-3)/4)); - } else { - ui->lHex->setText(ui->lHex->text()+" "); - } + const auto value = registerer::parse(ui->lineEdit->text().toStdString()); + const int bit = ui->spinBox->value(); - if((i+1)%4==0) { - ui->lBinary->setText(ui->lBinary->text()+QString(" ")); - ui->lPos->setText(ui->lPos->text()+QString(" ")); - ui->lHex->setText(ui->lHex->text()+QString(" ")); + if (!value) { + for (QLabel *row : { ui->lBinary, ui->lPos, ui->lHex, ui->lDec, ui->lBitAsHex }) + row->clear(); + return; } - } - QString dec; - dec.fill(' ',ui->lBinary->text().length()-QString().setNum(number, 10).length()-1); - dec.append(QString().setNum(number, 10)); + ui->lBitAsHex->setText( + toQt(registerer::format({ .number = registerer::mask(bit), .base = value->base }))); - ui->lDec->setText(dec); + const registerer::Rows rows = registerer::layout(value->number, bit); + ui->lBinary->setText(toQt(rows.bin)); + ui->lPos->setText(toQt(rows.pos)); + ui->lHex->setText(toQt(rows.hex)); + ui->lDec->setText(toQt(rows.dec)); +} +void MainWindow::on_lineEdit_editingFinished() +{ + refresh(); } void MainWindow::on_spinBox_valueChanged(int) { - on_lineEdit_editingFinished(); + refresh(); } void MainWindow::on_tbToggle_clicked() { - QString tmp = ui->lBinary->text().remove(" "); - - if(ui->spinBox->value() >= tmp.length()) { - tmp.fill('0',ui->spinBox->value()-tmp.length()+1); - tmp.append(ui->lBinary->text().remove(" ")); - } - - if(ui->spinBox->value() < tmp.length()) { - - if(tmp[tmp.length() - 1 - ui->spinBox->value()] == QChar('0')) - tmp[tmp.length() - 1 - ui->spinBox->value()] = QChar('1'); - else - tmp[tmp.length() - 1 - ui->spinBox->value()] = QChar('0'); - - bool ok; - unsigned long long num = tmp.toULongLong(&ok,2); - if(ok) { - if(ui->lineEdit->text().startsWith("0b")) { - ui->lineEdit->setText(QString("0b")+QString::number(num,2)); - } else if(ui->lineEdit->text().startsWith("0x")) { - ui->lineEdit->setText(QString("0x")+QString::number(num,16)); - } else { - ui->lineEdit->setText(QString::number(num)); - } - on_lineEdit_editingFinished(); - } - } + if (const auto v = registerer::parse(ui->lineEdit->text().toStdString())) + ui->lineEdit->setText(toQt( + registerer::format({ .number = registerer::toggle(v->number, ui->spinBox->value()), + .base = v->base }))); + refresh(); } void MainWindow::on_tbDisplay_clicked() { - unsigned long long number; - bool ok; - - if(ui->lineEdit->text().startsWith("0b")) { - number = ui->lineEdit->text().right(ui->lineEdit->text().length()-2).toULongLong(&ok, 2); - } else if(ui->lineEdit->text().startsWith("0x")) { - number = ui->lineEdit->text().right(ui->lineEdit->text().length()-2).toULongLong(&ok, 16); - } else { - number = ui->lineEdit->text().toULongLong(&ok, 10); - } - - if(ui->lineEdit->text().startsWith("0b")) { - ui->lineEdit->setText(QString::number(number, 10)); - } else if(ui->lineEdit->text().startsWith("0x")) { - ui->lineEdit->setText(QString("0b")+QString::number(number,2)); - } else { - ui->lineEdit->setText(QString("0x")+QString::number(number,16)); - } - - if(ui->lineEdit->text().startsWith("0b")) { - ui->lBitAsHex->setText(QString("0b")+QString::number((unsigned long long)qPow(2,ui->spinBox->value()),2)); - } else if(ui->lineEdit->text().startsWith("0x")) { - ui->lBitAsHex->setText(QString("0x")+QString::number((unsigned long long)qPow(2,ui->spinBox->value()),16)); - } else { - ui->lBitAsHex->setText(QString::number((unsigned long long)qPow(2,ui->spinBox->value()))); - } - - on_lineEdit_editingFinished(); + if (const auto v = registerer::parse(ui->lineEdit->text().toStdString())) + ui->lineEdit->setText( + toQt(registerer::format({ .number = v->number, .base = next(v->base) }))); + refresh(); } diff --git a/mainwindow.h b/mainwindow.h index 5d7c70e..9182c80 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -1,5 +1,5 @@ /* - Copyright 2013 Mike Bergmann, Bones AG + Copyright 2013-2026 Mike Bergmann This file is part of Registerer. @@ -17,26 +17,29 @@ along with Registerer. If not, see . */ -#ifndef MAINWINDOW_H -#define MAINWINDOW_H +#pragma once #include +#include + +QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } +QT_END_NAMESPACE class MainWindow : public QMainWindow { - Q_OBJECT - + Q_OBJECT + public: - explicit MainWindow(QWidget *parent = 0); - ~MainWindow(); - + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow() override; + protected: - void changeEvent(QEvent *e); - + void changeEvent(QEvent *e) override; + private slots: void on_lineEdit_editingFinished(); void on_spinBox_valueChanged(int); @@ -45,7 +48,7 @@ private slots: void on_tbDisplay_clicked(); private: - Ui::MainWindow *ui; -}; + void refresh(); -#endif // MAINWINDOW_H + std::unique_ptr ui; +}; diff --git a/mainwindow.ui b/mainwindow.ui index 1fed5cd..c878dab 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -246,8 +246,8 @@ <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:7.8pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">© 2012-13 Bones AG, Mike Bergmann</p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a href="http://blog.bones-embedded.ch"><span style=" text-decoration: underline; color:#0000ff;">http://blog.bones-embedded.ch</span></a></p></body></html> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">© 2013-2026 Mike Bergmann</p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a href="http://blog.mdb977.de"><span style=" text-decoration: underline; color:#0000ff;">http://blog.mdb977.de</span></a></p></body></html> Qt::RichText