diff --git a/.github/workflows/linux-ubuntu.yml b/.github/workflows/linux-ubuntu.yml index e007a40c..412ef416 100644 --- a/.github/workflows/linux-ubuntu.yml +++ b/.github/workflows/linux-ubuntu.yml @@ -45,7 +45,7 @@ jobs: # os: [ubuntu-22.04, ubuntu-24.04] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 @@ -111,17 +111,14 @@ jobs: run: | make - # - # On Linux, currently CMake - # - #- name: Test (via CMake) - # working-directory: ${{github.workspace}}/build - # shell: bash - # env: - # CTEST_OUTPUT_ON_FAILURE: TRUE - # QT_QPA_PLATFORM: minimal - # run: | - # make test + - name: Test (via CMake) + working-directory: ${{github.workspace}}/build + shell: bash + env: + CTEST_OUTPUT_ON_FAILURE: TRUE + QT_QPA_PLATFORM: minimal + run: | + make test # Note that, although we continue to support CMake for local builds and installs, we no longer support packaging # with CPack/CMake. The bt build script packaging gives us better control over the packaging process. diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index a532f689..8554861e 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -36,7 +36,7 @@ jobs: build-mac: runs-on: macos-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 # diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 1dae4f1c..bf4b99c1 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -52,7 +52,7 @@ jobs: ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: path: temp fetch-depth: 0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 60ca2934..fca6f76c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,7 +91,7 @@ if(APPLE AND NOT CMAKE_OSX_ARCHITECTURES) # Per https://cmake.org/cmake/help/latest/variable/CMAKE_OSX_ARCHITECTURES.html, "the value of this variable should # be set prior to the first project() or enable_language() command invocation because it may influence configuration # of the toolchain and flags". - set(CMAKE_OSX_ARCHITECTURES x86_64) # Build intel 64-bit binary. + set(CMAKE_OSX_ARCHITECTURES x86_64, arm64) # Build both x86_64 and arm64 for Apple silicon. #set(CMAKE_OSX_ARCHITECTURES i386 x86_64) # Build intel binary. #set(CMAKE_OSX_ARCHITECTURES ppc i386 ppc64 x86_64) # Build universal binary. endif() @@ -107,7 +107,8 @@ message(STATUS "PROJECT_SOURCE_DIR is ${PROJECT_SOURCE_DIR}") # Sometimes we do need the capitalised version of the project name set(capitalisedProjectName Brewken) # We use this in the program, to tell users where to get support. -set(CONFIG_HOMEPAGE_URL "https://github.com/${capitalisedProjectName}/${projectName}") +set(CONFIG_GITHUB_URL "https://github.com/${capitalisedProjectName}/${projectName}") +set(CONFIG_WEBSITE_URL "https://www.brewken.com/") set(CONFIG_ORGANIZATION_DOMAIN "brewken.com") #======================================================================================================================= @@ -801,7 +802,7 @@ configure_file(src/config.in src/config.h) #======================================================================================================================= # We don't need to list the embedded resource files themselves here, just the "Resource Collection File" that lists what # they are. -set(filesToCompile_qrc "${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}.qrc") +set(filesToCompile_qrc "${CMAKE_CURRENT_SOURCE_DIR}/resources.qrc") #======================================================================================================================= #=========================================== Files included with the program =========================================== @@ -816,7 +817,8 @@ set(filesToInstall_docs ${repoDir}/README.md) set(filesToInstall_data ${repoDir}/data/default_db.sqlite ${repoDir}/data/DefaultContent001-OriginalDefaultData.xml ${repoDir}/data/DefaultContent002-BJCP_2021_Styles.json - ${repoDir}/data/DefaultContent003-Ingredients-Hops-Yeasts.json) + ${repoDir}/data/DefaultContent003-Ingredients-Hops-Yeasts.json + ${repoDir}/data/DefaultContent004-MoreYeasts.json) # Desktop files to install. set(filesToInstall_desktop ${repoDir}/linux/${PROJECT_NAME}.desktop) diff --git a/meson.build b/meson.build index a1371a39..7b9402f2 100644 --- a/meson.build +++ b/meson.build @@ -767,7 +767,6 @@ commonSourceFiles = files([ 'src/sortFilterProxyModels/YeastSortFilterProxyModel.cpp', 'src/tableModels/BoilStepTableModel.cpp', 'src/tableModels/BtTableModel.cpp', - 'src/tableModels/BtTableModelInventory.cpp', 'src/tableModels/EquipmentTableModel.cpp', 'src/tableModels/FermentableTableModel.cpp', 'src/tableModels/FermentationStepTableModel.cpp', @@ -794,6 +793,7 @@ commonSourceFiles = files([ 'src/utils/EnumStringMapping.cpp', 'src/utils/FileSystemHelpers.cpp', 'src/utils/Fonts.cpp', + 'src/utils/FuzzyCompare.cpp', 'src/utils/ImportRecordCount.cpp', 'src/utils/MetaTypes.cpp', 'src/utils/OStreamWriterForQFile.cpp', @@ -1212,7 +1212,7 @@ installSubDir_translations = installSubDir_data + '/translations_qm' #======================================================================================================================= # Compile Qt's resources collection files (.qrc) into C++ files for compilation -generatedFromQrc = qt.compile_resources(sources : projectName + '.qrc') +generatedFromQrc = qt.compile_resources(sources : 'resources.qrc') # Compile Qt's ui files (.ui) into header files. generatedFromUi = qt.compile_ui(sources : uiFiles) @@ -1338,7 +1338,8 @@ exportedVariables = { 'CONFIG_ORGANIZATION_NAME' : 'The ' + capitalisedProjectName + ' Team', # Similarly, installers often want a URL link. We also use this in the program, to tell users where to get support. - 'CONFIG_HOMEPAGE_URL' : 'https://github.com/' + capitalisedProjectName + '/' + projectName, + 'CONFIG_GITHUB_URL' : 'https://github.com/' + capitalisedProjectName + '/' + projectName, + 'CONFIG_WEBSITE_URL' : 'https://www.brewken.com/', 'CONFIG_ORGANIZATION_DOMAIN' : 'brewken.com', # diff --git a/packaging/darwin/Info.plist.in b/packaging/darwin/Info.plist.in index 8645b66b..89344d85 100644 --- a/packaging/darwin/Info.plist.in +++ b/packaging/darwin/Info.plist.in @@ -2,7 +2,7 @@ CFBundleDevelopmentRegion en @@ -42,25 +49,41 @@ --> CFBundleIconFile @CONFIG_INSTALLER_APPLICATION_ICON@ + CFBundleIdentifier - com.brewtarget.Brewtarget + com.brewken.Brewken CFBundleInfoDictionaryVersion 6.0 - CFBundleLongVersionString - CFBundleName @CONFIG_APPLICATION_NAME_UC@ + CFBundlePackageType APPL CFBundleShortVersionString @CONFIG_VERSION_STRING@ - CFBundleSignature - ???? CFBundleVersion @CONFIG_VERSION_STRING@ + CSResourcesFileMapped NSHumanReadableCopyright @CONFIG_COPYRIGHT_STRING@ + + LSApplicationCategoryType + public.app-category.productivity diff --git a/packaging/linux/control.in b/packaging/linux/control.in index 6e8dc2f6..63f8a5fd 100644 --- a/packaging/linux/control.in +++ b/packaging/linux/control.in @@ -1,5 +1,5 @@ #----------------------------------------------------------------------------------------------------------------------- -# packaging/linux/control.in is part of Brewken, and is copyright the following authors 2023: +# packaging/linux/control.in is part of Brewken, and is copyright the following authors 2023-2024: # • Matt Young # # Brewken is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License @@ -121,4 +121,4 @@ Description: GUI beer brewing software # # Homepage (Optional) # -Homepage: @CONFIG_HOMEPAGE_URL@ +Homepage: @CONFIG_WEBSITE_URL@ diff --git a/packaging/linux/rpm.spec.in b/packaging/linux/rpm.spec.in index 1562d5fa..bc0f0d79 100644 --- a/packaging/linux/rpm.spec.in +++ b/packaging/linux/rpm.spec.in @@ -1,5 +1,5 @@ #----------------------------------------------------------------------------------------------------------------------- -# packaging/linux/rpm.spec.in is part of Brewken, and is copyright the following authors 2023: +# packaging/linux/rpm.spec.in is part of Brewken, and is copyright the following authors 2023-2024: # • Matt Young # # Brewken is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License @@ -43,7 +43,7 @@ Group : Applications/Productivity Summary : GUI beer brewing software # URL supplying further information about the package, typically upstream website. -URL : @CONFIG_HOMEPAGE_URL@ +URL : @CONFIG_WEBSITE_URL@ Vendor : @CONFIG_ORGANIZATION_NAME@ # Specifies the architecture which the resulting binary package will run on. Typically this is a CPU architecture. diff --git a/packaging/windows/NsisInstallerScript.nsi.in b/packaging/windows/NsisInstallerScript.nsi.in index 5fa00640..92d32c5f 100644 --- a/packaging/windows/NsisInstallerScript.nsi.in +++ b/packaging/windows/NsisInstallerScript.nsi.in @@ -1,5 +1,5 @@ #----------------------------------------------------------------------------------------------------------------------- -# packaging/windows/NsisInstallerScript.nsi.in is part of Brewken, and is copyright the following authors 2023: +# packaging/windows/NsisInstallerScript.nsi.in is part of Brewken, and is copyright the following authors 2023-2024: # • Matt Young # # Brewken is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License @@ -119,7 +119,7 @@ RequestExecutionLevel admin # # Other variables injected from Meson # -# Similarly, although we could use other injected variables directly, we don't to avoid another gotcha. When Meson +# Similarly, although we could use other injected variables directly, we don't, to avoid another gotcha. When Meson # is processing the file to do @BLAH@ substitutions, if it sees a backslash followed by an @, then it will think you're # escaping the first @ symbol, so, eg "C:\Blah\@CONFIG_APPLICATION_NAME_UC@" will not get converted to # "C:\Blah\Brewtarget" or "C:\Blah\Brewken". Instead, we take the injected variable into an NSIS compile-time constant @@ -127,7 +127,7 @@ RequestExecutionLevel admin # !define INJECTED_APPLICATION_NAME_UC "@CONFIG_APPLICATION_NAME_UC@" # and then we can write "C:\Blah\${INJECTED_APPLICATION_NAME_UC}" and the right substitutions will happen. (The # alternative, of adding an extra slash, eg "C:\Blah\\@CONFIG_APPLICATION_NAME_UC@", would work but seems a bit less -# robust. +# robust.) # !define INJECTED_APPLICATION_NAME_UC "@CONFIG_APPLICATION_NAME_UC@" !define INJECTED_APPLICATION_NAME_LC "@CONFIG_APPLICATION_NAME_LC@" @@ -136,7 +136,7 @@ RequestExecutionLevel admin !define INJECTED_DESCRIPTION_STRING "@CONFIG_DESCRIPTION_STRING@" !define INJECTED_COPYRIGHT_STRING "@CONFIG_COPYRIGHT_STRING@" !define INJECTED_ORGANIZATION_NAME "@CONFIG_ORGANIZATION_NAME@" -!define INJECTED_HOMEPAGE_URL "@CONFIG_HOMEPAGE_URL@" +!define INJECTED_HOMEPAGE_URL "@CONFIG_WEBSITE_URL@" #======================================================================================================================= #==================================================== Our Constants ==================================================== diff --git a/brewken.qrc b/resources.qrc similarity index 100% rename from brewken.qrc rename to resources.qrc diff --git a/scripts/buildTool.py b/scripts/buildTool.py index aedd0d95..ab7fcfca 100755 --- a/scripts/buildTool.py +++ b/scripts/buildTool.py @@ -897,8 +897,27 @@ def installDependencies(): # 'xerces-c' ] for packageToInstall in installListBrew: - log.debug('Installing ' + packageToInstall + ' via Homebrew') - btUtils.abortOnRunFail(subprocess.run(['brew', 'install', packageToInstall])) + # + # If we try to install a Homebrew package that is already installed, we'll get a warning. This isn't + # horrendous, but it looks a bit bad on the GitHub automated builds (because a lot of things are already + # installed by the time this script runs). As explained at + # https://apple.stackexchange.com/questions/284379/with-homebrew-how-to-check-if-a-software-package-is-installed, + # the simplest (albeit perhaps not the most elegant) way to check whether a package is already installed is + # to run `brew list`, throw away the output, and look at the return code, which will be 0 if the package is + # already installed and 1 if it is not. In the shell, we can use the magic of short-circuit evaluation + # (https://en.wikipedia.org/wiki/Short-circuit_evaluation) to, at a small legibility cost, do the whole + # check-and-install, in a single line. But in Python, it's easier to do it in two steps. + # + log.debug('Checking ' + packageToInstall) + brewListResult = subprocess.run(['brew', 'list', packageToInstall], + stdout = subprocess.DEVNULL, + stderr = subprocess.DEVNULL, + capture_output = False) + if (brewListResult.returncode == 0): + log.debug('Homebrew reports ' + packageToInstall + ' already installed') + else: + log.debug('Installing ' + packageToInstall + ' via Homebrew') + btUtils.abortOnRunFail(subprocess.run(['brew', 'install', packageToInstall])) # # By default, even once Qt5 is installed, Meson will not find it # @@ -1428,6 +1447,8 @@ def doPackage(): # # Copy the Debian Binary package control file to where it belongs # + # The meson build will have generated this file from packaging/linux/control.in + # log.debug('Copying deb package control file') copyWithoutCommentsOrFolds(dir_build.joinpath('control').as_posix(), dir_packages_deb_control.joinpath('control').as_posix()) @@ -2022,6 +2043,8 @@ def doPackage(): # [projectName]_[versionNumber].app # └── Contents # ├── Info.plist ❇ <── "Information property list" file = required configuration information (in XML) + # │ This includes things such as Bundle ID. It is generated by the Meson build + # │ from packaging/darwin/Info.plist.in # ├── Frameworks <── Contains any private shared libraries and frameworks used by the executable # │ ├── QtCore.framework * NB: Directory and its contents * 🟢 # │ ├── [Other Qt .framework directories and their contents] 🟢 @@ -2311,6 +2334,13 @@ def doPackage(): # a lot simpler to let macdeployqt create the disk image, and we currently don't think we need to do further # fix-up work after it's run. A custom icon on the disk image would be nice, but is far from essential. # + # .:TBD:. Ideally we would sign our application here using the `-codesign=` command line option to + # macdeployqt. For the GitHub builds, we would have to import a code signing certificate using + # https://github.com/Apple-Actions/import-codesign-certs. + # + # However, getting an identity and certificate with which to sign is a bit complicated. For a start, + # Apple pretty much require you to sign up to their $99/year developer program. + # log.debug('Running macdeployqt') os.chdir(dir_packages_platform) btUtils.abortOnRunFail( diff --git a/src/AboutDialog.cpp b/src/AboutDialog.cpp index bdc313ac..0346fe5e 100644 --- a/src/AboutDialog.cpp +++ b/src/AboutDialog.cpp @@ -196,7 +196,7 @@ AboutDialog::AboutDialog(QWidget * parent) : "

" "" ) - .arg(CONFIG_APPLICATION_NAME_UC, CONFIG_VERSION_STRING, CONFIG_HOMEPAGE_URL) + .arg(CONFIG_APPLICATION_NAME_UC, CONFIG_VERSION_STRING, CONFIG_GITHUB_URL) ); return; } diff --git a/src/Application.cpp b/src/Application.cpp index 28dda386..f61a9fe8 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include "Algorithms.h" #include "BtSplashScreen.h" @@ -237,33 +238,43 @@ namespace { QJsonObject jsonObject = jsonDocument.object(); - QString remoteVersion = jsonObject.value("name").toString(); - qDebug() << Q_FUNC_INFO << "Latest release is" << remoteVersion << "; this release is" << CONFIG_VERSION_STRING; - + QString remoteVersion = jsonObject.value("tag_name").toString(); // Version names are usually "v3.0.2" etc, so we want to strip the 'v' off the front if (remoteVersion.startsWith("v", Qt::CaseInsensitive)) { remoteVersion.remove(0, 1); } + // + // We used to just compare if the remote version is the same as the current one, but it then gets annoying if you + // are running the nightly build and it keeps asking if you want to, eg, download 4.0.0 because you're "only" + // running 4.0.1. So now we do it properly, letting QVersionNumber do the heavy lifting for us. + // + QVersionNumber const currentlyRunning{QVersionNumber::fromString(CONFIG_VERSION_STRING)}; + QVersionNumber const latestRelease {QVersionNumber::fromString(remoteVersion)}; + + qInfo() << + Q_FUNC_INFO << "Latest release is" << remoteVersion << "(parsed as" << latestRelease << ") ; " + "currently running" << CONFIG_VERSION_STRING << "(parsed as" << currentlyRunning << ")"; + // If the remote version is newer... - if (!remoteVersion.startsWith(CONFIG_VERSION_STRING)) { + if (latestRelease > currentlyRunning) { // ...and the user wants to download the new version... - if( QMessageBox::information(&MainWindow::instance(), - QObject::tr("New Version"), - QObject::tr("Version %1 is now available. Download it?").arg(remoteVersion), - QMessageBox::Yes | QMessageBox::No, - QMessageBox::Yes) == QMessageBox::Yes ) { + if(QMessageBox::information(&MainWindow::instance(), + QObject::tr("New Version"), + QObject::tr("Version %1 is now available. Download it?").arg(remoteVersion), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::Yes) == QMessageBox::Yes) { // ...take them to the website. - static QString const releasesPage = QString{"%1/releases"}.arg(CONFIG_HOMEPAGE_URL); + static QString const releasesPage = QString{"%1/releases"}.arg(CONFIG_GITHUB_URL); QDesktopServices::openUrl(QUrl(releasesPage)); } else { // ... and the user does NOT want to download the new version... // ... and they want us to stop bothering them... - if( QMessageBox::question(&MainWindow::instance(), - QObject::tr("New Version"), - QObject::tr("Stop bothering you about new versions?"), - QMessageBox::Yes | QMessageBox::No, - QMessageBox::Yes) == QMessageBox::Yes) { + if(QMessageBox::question(&MainWindow::instance(), + QObject::tr("New Version"), + QObject::tr("Stop bothering you about new versions?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::Yes) == QMessageBox::Yes) { // ... make a note to stop bothering the user about the new version. setCheckVersion(false); } @@ -298,7 +309,7 @@ namespace { // Since Qt5, you can connect signals to simple functions (see https://wiki.qt.io/New_Signal_Slot_Syntax) QObject::connect(responseToCheckForNewVersion, &QNetworkReply::finished, mw, &finishCheckForNewVersion); qDebug() << - Q_FUNC_INFO << "Sending request to check for new version (request running =" << + Q_FUNC_INFO << "Sending request to" << url << "to check for new version (request running =" << responseToCheckForNewVersion->isRunning() << ")"; return; } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 89ad55cf..4789a9e5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -211,7 +211,6 @@ set(filesToCompile_cpp ${repoDir}/src/sortFilterProxyModels/YeastSortFilterProxyModel.cpp ${repoDir}/src/tableModels/BoilStepTableModel.cpp ${repoDir}/src/tableModels/BtTableModel.cpp - ${repoDir}/src/tableModels/BtTableModelInventory.cpp ${repoDir}/src/tableModels/EquipmentTableModel.cpp ${repoDir}/src/tableModels/FermentableTableModel.cpp ${repoDir}/src/tableModels/FermentationStepTableModel.cpp @@ -238,6 +237,7 @@ set(filesToCompile_cpp ${repoDir}/src/utils/EnumStringMapping.cpp ${repoDir}/src/utils/FileSystemHelpers.cpp ${repoDir}/src/utils/Fonts.cpp + ${repoDir}/src/utils/FuzzyCompare.cpp ${repoDir}/src/utils/ImportRecordCount.cpp ${repoDir}/src/utils/MetaTypes.cpp ${repoDir}/src/utils/OStreamWriterForQFile.cpp diff --git a/src/HelpDialog.cpp b/src/HelpDialog.cpp index d9aa62c0..76eaf79a 100644 --- a/src/HelpDialog.cpp +++ b/src/HelpDialog.cpp @@ -58,8 +58,8 @@ class HelpDialog::impl { * Set the text. This is a separate function because we want to be able to redisplay in a different language. */ void setText(HelpDialog & helpDialog) { - static QString const wikiUrl = QString{"%1/wiki" }.arg(CONFIG_HOMEPAGE_URL); - static QString const issuesUrl = QString{"%1/issues"}.arg(CONFIG_HOMEPAGE_URL); + static QString const wikiUrl = QString{"%1/wiki" }.arg(CONFIG_GITHUB_URL); + static QString const issuesUrl = QString{"%1/issues"}.arg(CONFIG_GITHUB_URL); QString mainText; QTextStream mainTextAsStream{&mainText}; mainTextAsStream << @@ -70,8 +70,9 @@ class HelpDialog::impl { "" "" "" - "

" << CONFIG_APPLICATION_NAME_UC << "

" - "version " << CONFIG_VERSION_STRING << " " << HelpDialog::tr("for") << " " << QSysInfo::prettyProductName() << + "

" << CONFIG_APPLICATION_NAME_UC << "

" << + HelpDialog::tr("version %1 for %2").arg(CONFIG_VERSION_STRING).arg(QSysInfo::prettyProductName()) << + "

" << CONFIG_ORGANIZATION_DOMAIN << " website

" "

" << HelpDialog::tr("Online Help") << "

" "

" << HelpDialog::tr("

The %1 wiki is at " diff --git a/src/Logging.cpp b/src/Logging.cpp index c4fd5be0..23e5de32 100644 --- a/src/Logging.cpp +++ b/src/Logging.cpp @@ -524,6 +524,10 @@ void Logging::terminateLogging() { } QString Logging::getStackTrace() { + // + // TBD: Once all our compilers have full C++23 support, we should look at switching to in the standard + // library. + // std::ostringstream stacktrace; stacktrace << boost::stacktrace::stacktrace(); QString returnValue; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 6be61703..e97d60d8 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -228,7 +228,6 @@ namespace { return; } - } // This private implementation class holds all private non-virtual members of MainWindow @@ -376,7 +375,9 @@ class MainWindow::impl { m_hopEditor = std::make_unique(&m_self); m_mashEditor = std::make_unique(&m_self); m_mashStepEditor = std::make_unique(&m_self); + m_boilEditor = std::make_unique(&m_self); m_boilStepEditor = std::make_unique(&m_self); + m_fermentationEditor = std::make_unique(&m_self); m_fermentationStepEditor = std::make_unique(&m_self); m_mashWizard = std::make_unique(&m_self); m_miscCatalog = std::make_unique(&m_self); @@ -469,24 +470,28 @@ class MainWindow::impl { /** * \brief Use this for adding \c RecipeAdditionHop etc + * + * \param ra The recipe addition object - eg \c RecipeAdditionFermentable, \c RecipeAdditionHop, etc */ - template - void doRecipeAddition(std::shared_ptr ne) { - Q_ASSERT(ne); + template + void doRecipeAddition(std::shared_ptr ra) { + Q_ASSERT(ra); this->m_self.doOrRedoUpdate( - newUndoableAddOrRemove(*this->m_self.m_recipeObs, - &Recipe::addAddition, - ne, - &Recipe::removeAddition, - QString(tr("Add %1 to recipe")).arg(NE::localisedName())) + newUndoableAddOrRemove(*this->m_recipeObs, + &Recipe::addAddition, + ra, + &Recipe::removeAddition, + QString(tr("Add %1 to recipe")).arg(RA::localisedName())) ); + // // Since we just added an ingredient, switch the focus to the tab that lists that type of ingredient. We rely here // on the individual tabs following a naming convention (recipeHopTab, recipeFermentableTab, etc) // Note that we want the untranslated class name because this is not for display but to refer to a QWidget inside // tabWidget_ingredients - auto const widgetName = QString("recipe%1Tab").arg(NE::staticMetaObject.className()); + // + auto const widgetName = QString("recipe%1Tab").arg(RA::IngredientClass::staticMetaObject.className()); qDebug() << Q_FUNC_INFO << widgetName; QWidget * widget = this->m_self.tabWidget_ingredients->findChild(widgetName); Q_ASSERT(widget); @@ -519,7 +524,7 @@ class MainWindow::impl { for (auto item : itemsToRemove) { this->m_self.doOrRedoUpdate( - newUndoableAddOrRemove(*this->m_self.m_recipeObs, + newUndoableAddOrRemove(*this->m_recipeObs, &Recipe::removeAddition, item, &Recipe::addAddition, @@ -578,19 +583,19 @@ class MainWindow::impl { // Here we have a parameter anyway, so we can just use overloading directly void setStepOwner(std::shared_ptr stepOwner) { - this->m_self.m_recipeObs->setMash(stepOwner); + this->m_recipeObs->setMash(stepOwner); this->m_mashStepTableModel->setMash(stepOwner); this->m_self.mashButton->setMash(stepOwner); return; } void setStepOwner(std::shared_ptr stepOwner) { - this->m_self.m_recipeObs->setBoil(stepOwner); + this->m_recipeObs->setBoil(stepOwner); this->m_boilStepTableModel->setBoil(stepOwner); this->m_self.boilButton->setBoil(stepOwner); return; } void setStepOwner(std::shared_ptr stepOwner) { - this->m_self.m_recipeObs->setFermentation(stepOwner); + this->m_recipeObs->setFermentation(stepOwner); this->m_fermentationStepTableModel->setFermentation(stepOwner); this->m_self.fermentationButton->setFermentation(stepOwner); return; @@ -605,12 +610,12 @@ class MainWindow::impl { template void newStep() { - if (!this->m_self.m_recipeObs) { + if (!this->m_recipeObs) { return; } std::shared_ptr stepOwner = - this->m_self.m_recipeObs->get(); + this->m_recipeObs->get(); if (!stepOwner) { auto defaultStepOwner = std::make_shared(); ObjectStoreWrapper::insert(defaultStepOwner); @@ -645,12 +650,12 @@ class MainWindow::impl { template void removeSelectedStep() { - if (!this->m_self.m_recipeObs) { + if (!this->m_recipeObs) { return; } std::shared_ptr stepOwner = - this->m_self.m_recipeObs->get(); + this->m_recipeObs->get(); if (!stepOwner) { return; } @@ -706,11 +711,11 @@ class MainWindow::impl { template void editSelectedStep() { - if (!this->m_self.m_recipeObs) { + if (!this->m_recipeObs) { return; } - auto stepOwner = this->m_self.m_recipeObs->get(); + auto stepOwner = this->m_recipeObs->get(); if (!stepOwner) { return; } @@ -731,6 +736,149 @@ class MainWindow::impl { return; } + //! \brief Fix pixel dimensions according to dots-per-inch (DPI) of screen we're on. + void setSizesInPixelsBasedOnDpi() { + // + // Default icon sizes are fine for low DPI monitors, but need changing on high-DPI systems. + // + // Fortunately, the icons are already SVGs, so we don't need to do anything more complicated than tell Qt what size + // in pixels to render them. + // + // For the moment, we assume we don't need to change the icon size after set-up. (In theory, it would be nice + // to detect, on a multi-monitor system, whether we have moved from a high DPI to a low DPI screen or vice versa. + // See https://doc.qt.io/qt-5/qdesktopwidget.html#screen-geometry for more on this. + // But, for now, TBD how important a use case that is. Perhaps a future enhancement...) + // + // Low DPI monitors are 72 or 96 DPI typically. High DPI monitors can be 168 DPI (as reported by logicalDpiX(), + // logicalDpiX()). Default toolbar icon size of 22×22 looks fine on low DPI monitor. So it seems 1/4-inch is a + // good width and height for these icons. Therefore divide DPI by 4 to get icon size. + // + auto const dpiX = this->m_self.logicalDpiX(); + auto const dpiY = this->m_self.logicalDpiY(); + qDebug() << QString("Logical DPI: %1,%2. Physical DPI: %3,%4") + .arg(dpiX) + .arg(dpiY) + .arg(this->m_self.physicalDpiX()) + .arg(this->m_self.physicalDpiY()); + auto const defaultToolBarIconSize = this->m_self.toolBar->iconSize(); + qDebug() << + Q_FUNC_INFO << "Default toolbar icon size:" << defaultToolBarIconSize.width() << "×" << + defaultToolBarIconSize.height(); + this->m_self.toolBar->setIconSize(QSize(dpiX/4,dpiY/4)); + + // + // Historically, tab icon sizes were, by default, smaller (16×16), but it seems more logical for them to be the same + // size as the toolbar ones. + // + auto defaultTabIconSize = this->m_self.tabWidget_Trees->iconSize(); + qDebug() << + Q_FUNC_INFO << "Default tab icon size:" << defaultTabIconSize.width() << "×" << defaultTabIconSize.height(); + this->m_self.tabWidget_Trees->setIconSize(QSize(dpiX/4,dpiY/4)); + + // + // Default logo size is 100×30 pixels, which is actually the wrong aspect ratio for the underlying image (currently + // 265 × 66 - ie aspect ratio of 4.015:1). + // + // Setting height to be 1/3 inch seems plausible for the default size, but looks a bit wrong in practice. Using 1/2 + // height looks better. Then width 265/66 × height. (Note that we actually put the fraction in double literals to + // avoid premature rounding.) + // + // This is a bit more work to implement because its a PNG image in a QLabel object + // + qDebug() << + Q_FUNC_INFO << "Logo default size:" << this->m_self.label_Brewken->width() << "×" << + this->m_self.label_Brewken->height(); + this->m_self.label_Brewken->setScaledContents(true); + this->m_self.label_Brewken->setFixedSize((265.0/66.0) * dpiX/2, // width = 265/66 × height = 265/66 × half an inch = (265/66) × (dpiX/2) + dpiY/2); // height = half an inch = dpiY/2 + qDebug() << + Q_FUNC_INFO << "Logo new size:" << this->m_self.label_Brewken->width() << "×" << + this->m_self.label_Brewken->height(); + + return; + } + + //! \brief Find an open brewnote tab, if it is open + BrewNoteWidget * findBrewNoteWidget(BrewNote * b) { + for (int ii = 0; ii < this->m_self.tabWidget_recipeView->count(); ++ii) { + if (this->m_self.tabWidget_recipeView->widget(ii)->objectName() == "BrewNoteWidget") { + BrewNoteWidget* ni = qobject_cast(this->m_self.tabWidget_recipeView->widget(ii)); + if (ni->isBrewNote(b)) { + return ni; + } + } + } + return nullptr; + } + + void setBrewNoteByIndex(const QModelIndex &index) { + + auto bNote = this->m_self.treeView_recipe->getItem(index); + if (!bNote) { + return; + } + + // HERE + // This is some clean up work. REMOVE FROM HERE TO THERE + if ( bNote->projPoints() < 15 ) + { + double pnts = bNote->projPoints(); + bNote->setProjPoints(pnts); + } + if ( bNote->effIntoBK_pct() < 10 ) + { + bNote->calculateEffIntoBK_pct(); + bNote->calculateBrewHouseEff_pct(); + } + // THERE + + Recipe* parent = ObjectStoreWrapper::getByIdRaw(bNote->recipeId()); + QModelIndex pNdx = this->m_self.treeView_recipe->parent(index); + + // This gets complex. Versioning means we can't just clear the open brewnote tabs out. + if (parent != this->m_recipeObs) { + if (!this->m_recipeObs->isMyAncestor(*parent)) { + this->m_self.setRecipe(parent); + } else if (this->m_self.treeView_recipe->ancestorsAreShowing(pNdx)) { + this->m_self.tabWidget_recipeView->setCurrentIndex(0); + // Start closing from the right (highest index) down. Anything else dumps + // core in the most unpleasant of fashions + int tabs = this->m_self.tabWidget_recipeView->count() - 1; + for (int i = tabs; i >= 0; --i) { + if (this->m_self.tabWidget_recipeView->widget(i)->objectName() == "BrewNoteWidget") + this->m_self.tabWidget_recipeView->removeTab(i); + } + this->m_self.setRecipe(parent); + } + } + + BrewNoteWidget * ni = this->findBrewNoteWidget(bNote); + if (!ni) { + ni = new BrewNoteWidget(this->m_self.tabWidget_recipeView); + ni->setBrewNote(bNote); + } + + this->m_self.tabWidget_recipeView->addTab(ni,bNote->brewDate_short()); + this->m_self.tabWidget_recipeView->setCurrentWidget(ni); + return; + } + + void setBrewNote(BrewNote * bNote) { +/// QString tabname; + BrewNoteWidget* ni = this->findBrewNoteWidget(bNote); + if (ni) { + this->m_self.tabWidget_recipeView->setCurrentWidget(ni); + return; + } + + ni = new BrewNoteWidget(this->m_self.tabWidget_recipeView); + ni->setBrewNote(bNote); + + this->m_self.tabWidget_recipeView->addTab(ni, bNote->brewDate_short()); + this->m_self.tabWidget_recipeView->setCurrentWidget(ni); + return; + } + //================================================ MEMBER VARIABLES ================================================= MainWindow & m_self; @@ -738,6 +886,10 @@ class MainWindow::impl { // Undo / Redo, using the Qt Undo framework std::unique_ptr m_undoStack; + + Recipe * m_recipeObs = nullptr; + + // all things tables should go here. std::unique_ptr m_boilStepTableModel ; std::unique_ptr m_fermentationStepTableModel ; @@ -803,6 +955,9 @@ class MainWindow::impl { std::unique_ptr m_namedMashEditor; std::unique_ptr m_singleNamedMashEditor; + QString highSS, lowSS, goodSS, boldSS; // Palette replacements +/// QPrinter * printer = nullptr; + }; @@ -823,7 +978,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), pimpl{std::make_u SMART_FIELD_INIT(MainWindow, label_targetBatchSize, lineEdit_batchSize , Recipe, PropertyNames::Recipe::batchSize_l , 2); SMART_FIELD_INIT(MainWindow, label_targetBoilSize , lineEdit_boilSize , Boil , PropertyNames::Boil::preBoilSize_l , 2); SMART_FIELD_INIT(MainWindow, label_efficiency , lineEdit_efficiency, Recipe, PropertyNames::Recipe::efficiency_pct, 1); - SMART_FIELD_INIT(MainWindow, label_boilTime , lineEdit_boilTime , Boil , PropertyNames::Boil::boilTime_mins , 1); + SMART_FIELD_INIT(MainWindow, label_boilTime , lineEdit_boilTime , Boil , PropertyNames::Boil::boilTime_mins , 0); SMART_FIELD_INIT(MainWindow, label_boilSg , lineEdit_boilSg , Recipe, PropertyNames::Recipe::boilGrav , 3); SMART_FIELD_INIT_NO_SF(MainWindow, oGLabel , Recipe, PropertyNames::Recipe::og ); @@ -833,7 +988,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), pimpl{std::make_u SMART_FIELD_INIT_NO_SF(MainWindow, label_boilSize , Boil , PropertyNames::Boil::preBoilSize_l); // Stop things looking ridiculously tiny on high DPI displays - this->setSizesInPixelsBasedOnDpi(); + this->pimpl->setSizesInPixelsBasedOnDpi(); // Horizontal tabs, please -- even on Mac OS, as the tabs contain square icons tabWidget_Trees->tabBar()->setStyle(new BtHorizontalTabs(true)); @@ -872,7 +1027,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), pimpl{std::make_u tr("The program may not work if you ignore this error.\n\n" "See logs for more details.\n\n" "If you need help, please open an issue " - "at %1").arg(CONFIG_HOMEPAGE_URL) + "at %1").arg(CONFIG_GITHUB_URL) ); dataLoadErrorMessageBox.setStandardButtons(QMessageBox::Ignore | QMessageBox::Close); dataLoadErrorMessageBox.setDefaultButton(QMessageBox::Close); @@ -903,15 +1058,15 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), pimpl{std::make_u this->actionAbout->setToolTip(tr("About %1").arg(CONFIG_APPLICATION_NAME_UC)); // Null out the recipe - m_recipeObs = nullptr; + this->pimpl->m_recipeObs = nullptr; - // Set up the printer - printer = new QPrinter; -#if QT_VERSION < QT_VERSION_CHECK(5,15,0) - printer->setPageSize(QPrinter::Letter); -#else - printer->setPageSize(QPageSize(QPageSize::Letter)); -#endif +/// // Set up the printer +/// this->pimpl->printer = new QPrinter; +///#if QT_VERSION < QT_VERSION_CHECK(5,15,0) +/// this->pimpl->printer->setPageSize(QPrinter::Letter); +///#else +/// this->pimpl->printer->setPageSize(QPageSize(QPageSize::Letter)); +///#endif return; } @@ -1003,66 +1158,6 @@ void MainWindow::DeleteMainWindow() { mainWindowInstance = nullptr; } -void MainWindow::setSizesInPixelsBasedOnDpi() -{ - // - // Default icon sizes are fine for low DPI monitors, but need changing on high-DPI systems. - // - // Fortunately, the icons are already SVGs, so we don't need to do anything more complicated than tell Qt what size - // in pixels to render them. - // - // For the moment, we assume we don't need to change the icon size after set-up. (In theory, it would be nice - // to detect, on a multi-monitor system, whether we have moved from a high DPI to a low DPI screen or vice versa. - // See https://doc.qt.io/qt-5/qdesktopwidget.html#screen-geometry for more on this. - // But, for now, TBD how important a use case that is. Perhaps a future enhancement...) - // - // Low DPI monitors are 72 or 96 DPI typically. High DPI monitors can be 168 DPI (as reported by logicalDpiX(), - // logicalDpiX()). Default toolbar icon size of 22×22 looks fine on low DPI monitor. So it seems 1/4-inch is a - // good width and height for these icons. Therefore divide DPI by 4 to get icon size. - // - auto dpiX = this->logicalDpiX(); - auto dpiY = this->logicalDpiY(); - qDebug() << QString("Logical DPI: %1,%2. Physical DPI: %3,%4") - .arg(dpiX) - .arg(dpiY) - .arg(this->physicalDpiX()) - .arg(this->physicalDpiY()); - auto defaultToolBarIconSize = this->toolBar->iconSize(); - qDebug() << QString("Default toolbar icon size: %1,%2") - .arg(defaultToolBarIconSize.width()) - .arg(defaultToolBarIconSize.height()); - this->toolBar->setIconSize(QSize(dpiX/4,dpiY/4)); - - // - // Historically, tab icon sizes were, by default, smaller (16×16), but it seems more logical for them to be the same - // size as the toolbar ones. - // - auto defaultTabIconSize = this->tabWidget_Trees->iconSize(); - qDebug() << QString("Default tab icon size: %1,%2") - .arg(defaultTabIconSize.width()) - .arg(defaultTabIconSize.height()); - this->tabWidget_Trees->setIconSize(QSize(dpiX/4,dpiY/4)); - - // - // Default logo size is 100×30 pixels, which is actually the wrong aspect ratio for the underlying image (currently - // 265 × 66 - ie aspect ratio of 4.015:1). - // - // Setting height to be 1/3 inch seems plausible for the default size, but looks a bit wrong in practice. Using 1/2 - // height looks better. Then width 265/66 × height. (Note that we actually put the fraction in double literals to - // avoid premature rounding.) - // - // This is a bit more work to implement because its a PNG image in a QLabel object - // - qDebug() << QString("Logo default size: %1,%2").arg(this->label_Brewken->width()).arg(this->label_Brewken->height()); - this->label_Brewken->setScaledContents(true); - this->label_Brewken->setFixedSize((265.0/66.0) * dpiX/2, // width = 265/66 × height = 265/66 × half an inch = (265/66) × (dpiX/2) - dpiY/2); // height = half an inch = dpiY/2 - qDebug() << QString("Logo new size: %1,%2").arg(this->label_Brewken->width()).arg(this->label_Brewken->height()); - - return; -} - - // Setup the keyboard shortcuts void MainWindow::setupShortCuts() { @@ -1079,8 +1174,7 @@ void MainWindow::setUpStateChanges() } // Any manipulation of CSS for the MainWindow should be in here -void MainWindow::setupCSS() -{ +void MainWindow::setupCSS() { // Different palettes for some text. This is all done via style sheets now. QColor wPalette = tabWidget_recipeView->palette().color(QPalette::Active,QPalette::Base); @@ -1090,13 +1184,13 @@ void MainWindow::setupCSS() // dramatically wrong on some displays. The simple solution is instead to use points (which are device independent) // to specify font size. // - goodSS = QString( "QLineEdit:read-only { color: #008800; background: %1 }").arg(wPalette.name()); - lowSS = QString( "QLineEdit:read-only { color: #0000D0; background: %1 }").arg(wPalette.name()); - highSS = QString( "QLineEdit:read-only { color: #D00000; background: %1 }").arg(wPalette.name()); - boldSS = QString( "QLineEdit:read-only { font: bold 10pt; color: #000000; background: %1 }").arg(wPalette.name()); + this->pimpl->goodSS = QString("QLineEdit:read-only { color: #008800; background: %1 }").arg(wPalette.name()); + this->pimpl->lowSS = QString("QLineEdit:read-only { color: #0000D0; background: %1 }").arg(wPalette.name()); + this->pimpl->highSS = QString("QLineEdit:read-only { color: #D00000; background: %1 }").arg(wPalette.name()); + this->pimpl->boldSS = QString("QLineEdit:read-only { font: bold 10pt; color: #000000; background: %1 }").arg(wPalette.name()); // The bold style sheet doesn't change, so set it here once. - lineEdit_boilSg->setStyleSheet(boldSS); + lineEdit_boilSg->setStyleSheet(this->pimpl->boldSS); // Disabled fields should change color, but not become unreadable. Mucking // with the css seems the most reasonable way to do that. @@ -1126,7 +1220,7 @@ void MainWindow::setupRanges() { // definitely cheating, but I don't feel like making a whole subclass just to support this // or the next. - rangeWidget_batchSize->setRange(0, m_recipeObs == nullptr ? 19.0 : m_recipeObs->batchSize_l()); + rangeWidget_batchSize->setRange(0, this->pimpl->m_recipeObs == nullptr ? 19.0 : this->pimpl->m_recipeObs->batchSize_l()); rangeWidget_batchSize->setPrecision(1); rangeWidget_batchSize->setTickMarks(2,5); @@ -1134,7 +1228,7 @@ void MainWindow::setupRanges() { rangeWidget_batchSize->setPreferredRangeBrush(QColor(55,138,251)); rangeWidget_batchSize->setMarkerBrush(QBrush(Qt::NoBrush)); - rangeWidget_boilsize->setRange(0, m_recipeObs == nullptr? 24.0 : m_recipeObs->boilVolume_l()); + rangeWidget_boilsize->setRange(0, this->pimpl->m_recipeObs == nullptr? 24.0 : this->pimpl->m_recipeObs->boilVolume_l()); rangeWidget_boilsize->setPrecision(1); rangeWidget_boilsize->setTickMarks(2,5); @@ -1205,10 +1299,10 @@ void MainWindow::restoreSavedState() { } } if ( key > -1 ) { - this->m_recipeObs = ObjectStoreWrapper::getByIdRaw(key); - QModelIndex rIdx = treeView_recipe->findElement(m_recipeObs); + this->pimpl->m_recipeObs = ObjectStoreWrapper::getByIdRaw(key); + QModelIndex rIdx = treeView_recipe->findElement(this->pimpl->m_recipeObs); - setRecipe(m_recipeObs); + setRecipe(this->pimpl->m_recipeObs); setTreeSelection(rIdx); } @@ -1344,37 +1438,47 @@ void MainWindow::setupTriggers() { // pushbuttons with a SIGNAL of clicked() should go in here. void MainWindow::setupClicks() { - connect(this->equipmentButton , &QAbstractButton::clicked, this , &MainWindow::showEquipmentEditor ); - connect(this->styleButton , &QAbstractButton::clicked, this , &MainWindow::showStyleEditor ); - connect(this->mashButton , &QAbstractButton::clicked, this->pimpl->m_mashEditor , &MashEditor::showEditor); - connect(this->pushButton_addFerm , &QAbstractButton::clicked, this->pimpl->m_fermCatalog , &QWidget::show ); - connect(this->pushButton_addHop , &QAbstractButton::clicked, this->pimpl->m_hopCatalog , &QWidget::show ); - connect(this->pushButton_addMisc , &QAbstractButton::clicked, this->pimpl->m_miscCatalog , &QWidget::show ); - connect(this->pushButton_addYeast , &QAbstractButton::clicked, this->pimpl->m_yeastCatalog , &QWidget::show ); - connect(this->pushButton_removeFerm , &QAbstractButton::clicked, this , &MainWindow::removeSelectedFermentableAddition); - connect(this->pushButton_removeHop , &QAbstractButton::clicked, this , &MainWindow::removeSelectedHopAddition ); - connect(this->pushButton_removeMisc , &QAbstractButton::clicked, this , &MainWindow::removeSelectedMiscAddition ); - connect(this->pushButton_removeYeast , &QAbstractButton::clicked, this , &MainWindow::removeSelectedYeastAddition ); - connect(this->pushButton_editFerm , &QAbstractButton::clicked, this , &MainWindow::editFermentableOfSelectedFermentableAddition); - connect(this->pushButton_editMisc , &QAbstractButton::clicked, this , &MainWindow::editMiscOfSelectedMiscAddition ); - connect(this->pushButton_editHop , &QAbstractButton::clicked, this , &MainWindow::editHopOfSelectedHopAddition ); - connect(this->pushButton_editYeast , &QAbstractButton::clicked, this , &MainWindow::editYeastOfSelectedYeastAddition ); - - connect(this->pushButton_editMash , &QAbstractButton::clicked, this->pimpl->m_mashEditor , &MashEditor::showEditor ); - connect(this->pushButton_addMashStep , &QAbstractButton::clicked, this , &MainWindow::addMashStep ); - connect(this->pushButton_removeMashStep, &QAbstractButton::clicked, this , &MainWindow::removeSelectedMashStep ); - connect(this->pushButton_editMashStep , &QAbstractButton::clicked, this , &MainWindow::editSelectedMashStep ); - connect(this->pushButton_mashWizard , &QAbstractButton::clicked, this->pimpl->m_mashWizard , &MashWizard::show ); - connect(this->pushButton_saveMash , &QAbstractButton::clicked, this , &MainWindow::saveMash ); + // + // Note that, if the third parameter to connect is null, we'll get a warning log along the lines of + // `QObject::connect(QPushButton, Unknown): invalid nullptr parameter`. Assuming this is the only (or at least the + // first) warning, a quick way to diagnose these is to set the environment variable QT_FATAL_WARNINGS and re-run the + // program. This will force a core dump when the warning occurs, and then, from the core file, you can see the call + // stack. + // + connect(this->equipmentButton , &QAbstractButton::clicked, this , &MainWindow::showEquipmentEditor); + connect(this->styleButton , &QAbstractButton::clicked, this , &MainWindow::showStyleEditor ); + connect(this->mashButton , &QAbstractButton::clicked, this->pimpl->m_mashEditor , &MashEditor::showEditor); + // TODO: Make these buttons! +// connect(this->boilButton , &QAbstractButton::clicked, this->pimpl->m_boilEditor , &BoilEditor::showEditor); +// connect(this->fermentationButton , &QAbstractButton::clicked, this->pimpl->m_fermentationEditor, &FermentationEditor::showEditor); + connect(this->pushButton_addFerm , &QAbstractButton::clicked, this->pimpl->m_fermCatalog , &QWidget::show ); + connect(this->pushButton_addHop , &QAbstractButton::clicked, this->pimpl->m_hopCatalog , &QWidget::show ); + connect(this->pushButton_addMisc , &QAbstractButton::clicked, this->pimpl->m_miscCatalog , &QWidget::show ); + connect(this->pushButton_addYeast , &QAbstractButton::clicked, this->pimpl->m_yeastCatalog, &QWidget::show ); + connect(this->pushButton_removeFerm , &QAbstractButton::clicked, this , &MainWindow::removeSelectedFermentableAddition); + connect(this->pushButton_removeHop , &QAbstractButton::clicked, this , &MainWindow::removeSelectedHopAddition ); + connect(this->pushButton_removeMisc , &QAbstractButton::clicked, this , &MainWindow::removeSelectedMiscAddition ); + connect(this->pushButton_removeYeast , &QAbstractButton::clicked, this , &MainWindow::removeSelectedYeastAddition ); + connect(this->pushButton_editFerm , &QAbstractButton::clicked, this , &MainWindow::editFermentableOfSelectedFermentableAddition); + connect(this->pushButton_editMisc , &QAbstractButton::clicked, this , &MainWindow::editMiscOfSelectedMiscAddition ); + connect(this->pushButton_editHop , &QAbstractButton::clicked, this , &MainWindow::editHopOfSelectedHopAddition ); + connect(this->pushButton_editYeast , &QAbstractButton::clicked, this , &MainWindow::editYeastOfSelectedYeastAddition ); + + connect(this->pushButton_editMash , &QAbstractButton::clicked, this->pimpl->m_mashEditor, &MashEditor::showEditor ); + connect(this->pushButton_addMashStep , &QAbstractButton::clicked, this , &MainWindow::addMashStep ); + connect(this->pushButton_removeMashStep, &QAbstractButton::clicked, this , &MainWindow::removeSelectedMashStep ); + connect(this->pushButton_editMashStep , &QAbstractButton::clicked, this , &MainWindow::editSelectedMashStep ); + connect(this->pushButton_mashWizard , &QAbstractButton::clicked, this->pimpl->m_mashWizard, &MashWizard::show ); + connect(this->pushButton_saveMash , &QAbstractButton::clicked, this , &MainWindow::saveMash ); connect(this->pushButton_mashDes , &QAbstractButton::clicked, this->pimpl->m_mashDesigner, &MashDesigner::show ); - connect(this->pushButton_mashUp , &QAbstractButton::clicked, this , &MainWindow::moveSelectedMashStepUp ); - connect(this->pushButton_mashDown , &QAbstractButton::clicked, this , &MainWindow::moveSelectedMashStepDown ); - connect(this->pushButton_mashRemove , &QAbstractButton::clicked, this , &MainWindow::removeMash ); - - connect(this->pushButton_editBoil , &QAbstractButton::clicked, this->pimpl->m_boilEditor , &BoilEditor::showEditor ); - connect(this->pushButton_addBoilStep , &QAbstractButton::clicked, this , &MainWindow::addBoilStep ); - connect(this->pushButton_removeBoilStep, &QAbstractButton::clicked, this , &MainWindow::removeSelectedBoilStep ); - connect(this->pushButton_editBoilStep , &QAbstractButton::clicked, this , &MainWindow::editSelectedBoilStep ); + connect(this->pushButton_mashUp , &QAbstractButton::clicked, this , &MainWindow::moveSelectedMashStepUp ); + connect(this->pushButton_mashDown , &QAbstractButton::clicked, this , &MainWindow::moveSelectedMashStepDown ); + connect(this->pushButton_mashRemove , &QAbstractButton::clicked, this , &MainWindow::removeMash ); + + connect(this->pushButton_editBoil , &QAbstractButton::clicked, this->pimpl->m_boilEditor, &BoilEditor::showEditor ); + connect(this->pushButton_addBoilStep , &QAbstractButton::clicked, this , &MainWindow::addBoilStep ); + connect(this->pushButton_removeBoilStep, &QAbstractButton::clicked, this , &MainWindow::removeSelectedBoilStep ); + connect(this->pushButton_editBoilStep , &QAbstractButton::clicked, this , &MainWindow::editSelectedBoilStep ); // connect(this->pushButton_saveBoil , &QAbstractButton::clicked, this , &MainWindow::saveBoil ); TODO! connect(this->pushButton_boilUp , &QAbstractButton::clicked, this , &MainWindow::moveSelectedBoilStepUp ); connect(this->pushButton_boilDown , &QAbstractButton::clicked, this , &MainWindow::moveSelectedBoilStepDown ); @@ -1405,8 +1509,7 @@ void MainWindow::setupActivate() { void MainWindow::setupTextEdit() { connect(this->lineEdit_name , &QLineEdit::editingFinished, this, &MainWindow::updateRecipeName); connect(this->lineEdit_batchSize , &SmartLineEdit::textModified, this, &MainWindow::updateRecipeBatchSize); - connect(this->lineEdit_boilSize , &SmartLineEdit::textModified, this, &MainWindow::updateRecipeBoilSize); - connect(this->lineEdit_boilTime , &SmartLineEdit::textModified, this, &MainWindow::updateRecipeBoilTime); +/// connect(this->lineEdit_boilSize , &SmartLineEdit::textModified, this, &MainWindow::updateRecipeBoilSize); connect(this->lineEdit_efficiency, &SmartLineEdit::textModified, this, &MainWindow::updateRecipeEfficiency); return; } @@ -1560,7 +1663,7 @@ void MainWindow::treeActivated(const QModelIndex &index) { } break; case TreeNode::Type::BrewNote: - setBrewNoteByIndex(index); + this->pimpl->setBrewNoteByIndex(index); break; case TreeNode::Type::Folder: // default behavior is fine, but no warning break; @@ -1579,99 +1682,11 @@ void MainWindow::treeActivated(const QModelIndex &index) { return; } -void MainWindow::setBrewNoteByIndex(const QModelIndex &index) -{ - BrewNoteWidget* ni; - - auto bNote = treeView_recipe->getItem(index); - - if ( ! bNote ) - return; - // HERE - // This is some clean up work. REMOVE FROM HERE TO THERE - if ( bNote->projPoints() < 15 ) - { - double pnts = bNote->projPoints(); - bNote->setProjPoints(pnts); - } - if ( bNote->effIntoBK_pct() < 10 ) - { - bNote->calculateEffIntoBK_pct(); - bNote->calculateBrewHouseEff_pct(); - } - // THERE - - Recipe* parent = ObjectStoreWrapper::getByIdRaw(bNote->recipeId()); - QModelIndex pNdx = treeView_recipe->parent(index); - - // this gets complex. Versioning means we can't just clear the open - // brewnote tabs out. - if ( parent != this->m_recipeObs ) { - if ( ! this->m_recipeObs->isMyAncestor(*parent) ) { - setRecipe(parent); - } - else if ( treeView_recipe->ancestorsAreShowing(pNdx) ) { - tabWidget_recipeView->setCurrentIndex(0); - // Start closing from the right (highest index) down. Anything else dumps - // core in the most unpleasant of fashions - int tabs = tabWidget_recipeView->count() - 1; - for (int i = tabs; i >= 0; --i) { - if (tabWidget_recipeView->widget(i)->objectName() == "BrewNoteWidget") - tabWidget_recipeView->removeTab(i); - } - setRecipe(parent); - } - } - - ni = findBrewNoteWidget(bNote); - if ( ! ni ) - { - ni = new BrewNoteWidget(tabWidget_recipeView); - ni->setBrewNote(bNote); - } - - tabWidget_recipeView->addTab(ni,bNote->brewDate_short()); - tabWidget_recipeView->setCurrentWidget(ni); - -} - -BrewNoteWidget* MainWindow::findBrewNoteWidget(BrewNote* b) -{ - for (int i = 0; i < tabWidget_recipeView->count(); ++i) - { - if (tabWidget_recipeView->widget(i)->objectName() == "BrewNoteWidget") - { - BrewNoteWidget* ni = qobject_cast(tabWidget_recipeView->widget(i)); - if ( ni->isBrewNote(b) ) - return ni; - } - } - return nullptr; -} - -void MainWindow::setBrewNote(BrewNote* bNote) -{ - QString tabname; - BrewNoteWidget* ni = findBrewNoteWidget(bNote); - - if ( ni ) { - tabWidget_recipeView->setCurrentWidget(ni); - return; - } - - ni = new BrewNoteWidget(tabWidget_recipeView); - ni->setBrewNote(bNote); - - this->tabWidget_recipeView->addTab(ni,bNote->brewDate_short()); - this->tabWidget_recipeView->setCurrentWidget(ni); - return; -} - void MainWindow::setAncestor() { Recipe* rec; - if ( this->m_recipeObs ) { - rec = this->m_recipeObs; + if ( this->pimpl->m_recipeObs ) { + rec = this->pimpl->m_recipeObs; } else { QModelIndexList indexes = treeView_recipe->selectionModel()->selectedRows(); rec = treeView_recipe->getItem(indexes[0]); @@ -1694,10 +1709,14 @@ void MainWindow::setRecipe(Recipe* recipe) { int tabs = 0; // Make sure this MainWindow is paying attention... - if (this->m_recipeObs) { - disconnect(this->m_recipeObs, nullptr, this, nullptr); + if (this->pimpl->m_recipeObs) { + disconnect(this->pimpl->m_recipeObs, nullptr, this, nullptr); + auto boil = this->pimpl->m_recipeObs->boil(); + if (boil) { + disconnect(boil.get(), nullptr, this, nullptr); + } } - this->m_recipeObs = recipe; + this->pimpl->m_recipeObs = recipe; this->displayRangesEtcForCurrentRecipeStyle(); @@ -1706,9 +1725,9 @@ void MainWindow::setRecipe(Recipe* recipe) { this->pimpl->m_hopAdditionsTableModel->observeRecipe(recipe); this->pimpl->m_miscAdditionsTableModel->observeRecipe(recipe); this->pimpl->m_yeastAdditionsTableModel->observeRecipe(recipe); - this->pimpl->m_mashStepTableModel->setMash(m_recipeObs->mash()); - this->pimpl->m_boilStepTableModel->setBoil(m_recipeObs->boil()); - this->pimpl->m_fermentationStepTableModel->setFermentation(m_recipeObs->fermentation()); + this->pimpl->m_mashStepTableModel->setMash(this->pimpl->m_recipeObs->mash()); + this->pimpl->m_boilStepTableModel->setBoil(this->pimpl->m_recipeObs->boil()); + this->pimpl->m_fermentationStepTableModel->setFermentation(this->pimpl->m_recipeObs->fermentation()); // Clean out any brew notes tabWidget_recipeView->setCurrentIndex(0); @@ -1737,11 +1756,17 @@ void MainWindow::setRecipe(Recipe* recipe) { this->pimpl->m_styleEditor->setEditItem(recipe->style()); } - this->pimpl->m_mashEditor->setMash(m_recipeObs->mash()); - this->pimpl->m_mashEditor->setRecipe(m_recipeObs); + this->pimpl->m_mashEditor->setMash(this->pimpl->m_recipeObs->mash()); + this->pimpl->m_mashEditor->setRecipe(this->pimpl->m_recipeObs); + + this->pimpl->m_boilEditor->setEditItem(this->pimpl->m_recipeObs->boil()); + this->pimpl->m_boilEditor->setRecipe(this->pimpl->m_recipeObs); - mashButton->setMash(m_recipeObs->mash()); - this->pimpl->m_recipeScaler->setRecipe(m_recipeObs); + this->pimpl->m_fermentationEditor->setEditItem(this->pimpl->m_recipeObs->fermentation()); + this->pimpl->m_fermentationEditor->setRecipe(this->pimpl->m_recipeObs); + + mashButton->setMash(this->pimpl->m_recipeObs->mash()); + this->pimpl->m_recipeScaler->setRecipe(this->pimpl->m_recipeObs); // Set the locked flag as required checkBox_locked->setCheckState( recipe->locked() ? Qt::Checked : Qt::Unchecked ); @@ -1767,27 +1792,32 @@ void MainWindow::setRecipe(Recipe* recipe) { // If you don't connect this late, every previous set of an attribute // causes this signal to be slotted, which then causes showChanges() to be // called. - connect(this->m_recipeObs, &NamedEntity::changed, this, &MainWindow::changed); - showChanges(); + connect(this->pimpl->m_recipeObs, &NamedEntity::changed, this, &MainWindow::changed); + auto boil = this->pimpl->m_recipeObs->boil(); + if (boil) { + connect(boil.get(), &NamedEntity::changed, this, &MainWindow::changed); + } + this->showChanges(); + return; } // When a recipe is locked, many fields need to be disabled. void MainWindow::lockRecipe(int state) { - if (!this->m_recipeObs) { + if (!this->pimpl->m_recipeObs) { return; } // If I am locking a recipe (lock == true ), I want to disable fields // (enable == false). If I am unlocking (lock == false), I want to enable // fields (enable == true). This just makes that easy - bool const lockIt = state == Qt::Checked; - bool const enabled = ! lockIt; + bool const lockIt {state == Qt::Checked}; + bool const enabled {!lockIt}; // Lock/unlock the recipe, then disable/enable the fields. I am leaving the // name field as editable. I may regret that, but if you are defining an // inheritance tree, you may want to remove strings from the ancestoral // names - this->m_recipeObs->setLocked(lockIt); + this->pimpl->m_recipeObs->setLocked(lockIt); // I could disable tab_recipe, but would not prevent you from unlocking the // recipe because that field would also be disabled @@ -1796,7 +1826,6 @@ void MainWindow::lockRecipe(int state) { lineEdit_batchSize->setEnabled(enabled); lineEdit_boilSize->setEnabled(enabled); lineEdit_efficiency->setEnabled(enabled); - lineEdit_boilTime->setEnabled(enabled); // locked recipes cannot be deleted actionDeleteSelected->setEnabled(enabled); @@ -1830,7 +1859,7 @@ void MainWindow::lockRecipe(int state) { this->pimpl->m_hopCatalog ->setEnableAddToRecipe(enabled); this->pimpl->m_miscCatalog ->setEnableAddToRecipe(enabled); this->pimpl->m_yeastCatalog->setEnableAddToRecipe(enabled); - // mashes still need dealing with + // TODO: mashes still need dealing with // return; } @@ -1839,10 +1868,10 @@ void MainWindow::changed(QMetaProperty prop, [[maybe_unused]] QVariant val) { QString propName(prop.name()); if (propName == PropertyNames::Recipe::equipment) { - auto equipment = this->m_recipeObs->equipment(); + auto equipment = this->pimpl->m_recipeObs->equipment(); this->pimpl->m_equipEditor->setEditItem(equipment); } else if (propName == PropertyNames::Recipe::style) { - auto style = this->m_recipeObs->style(); + auto style = this->pimpl->m_recipeObs->style(); this->pimpl->m_styleEditor->setEditItem(style); } @@ -1851,79 +1880,76 @@ void MainWindow::changed(QMetaProperty prop, [[maybe_unused]] QVariant val) { } void MainWindow::showChanges(QMetaProperty* prop) { - if (m_recipeObs == nullptr) { + if (this->pimpl->m_recipeObs == nullptr) { return; } bool updateAll = (prop == nullptr); QString propName; - if (prop) { propName = prop->name(); } - // May St. Stevens preserve me - this->lineEdit_name ->setText (this->m_recipeObs->name ()); - this->lineEdit_batchSize ->setQuantity(this->m_recipeObs->batchSize_l ()); + this->lineEdit_name ->setText (this->pimpl->m_recipeObs->name ()); + this->lineEdit_batchSize ->setQuantity(this->pimpl->m_recipeObs->batchSize_l ()); // TODO: One day we'll want to do some work to properly handle no-boil recipes.... - double const boilSize = this->m_recipeObs->boil() ? this->m_recipeObs->boil()->preBoilSize_l().value_or(0.0) : 0.0; + std::optional const boilSize = this->pimpl->m_recipeObs->boil() ? this->pimpl->m_recipeObs->boil()->preBoilSize_l() : std::nullopt; this->lineEdit_boilSize ->setQuantity(boilSize); - this->lineEdit_efficiency->setQuantity(this->m_recipeObs->efficiency_pct()); - this->lineEdit_boilTime ->setQuantity(this->m_recipeObs->boil() ? this->m_recipeObs->boil()->boilTime_mins() : 0.0); + this->lineEdit_efficiency->setQuantity(this->pimpl->m_recipeObs->efficiency_pct()); + this->lineEdit_boilTime ->setQuantity(this->pimpl->m_recipeObs->boil()->boilTime_mins()); this->lineEdit_name ->setCursorPosition(0); this->lineEdit_batchSize ->setCursorPosition(0); this->lineEdit_boilSize ->setCursorPosition(0); this->lineEdit_efficiency->setCursorPosition(0); - this->lineEdit_boilTime ->setCursorPosition(0); /* - lineEdit_calcBatchSize->setText(m_recipeObs); - lineEdit_calcBoilSize->setText(m_recipeObs); + lineEdit_calcBatchSize->setText(this->pimpl->m_recipeObs); + lineEdit_calcBoilSize->setText(this->pimpl->m_recipeObs); */ // Color manipulation /* - if( 0.95*m_recipeObs->batchSize_l() <= m_recipeObs->finalVolume_l() && m_recipeObs->finalVolume_l() <= 1.05*m_recipeObs->batchSize_l() ) - lineEdit_calcBatchSize->setStyleSheet(goodSS); - else if( m_recipeObs->finalVolume_l() < 0.95*m_recipeObs->batchSize_l() ) - lineEdit_calcBatchSize->setStyleSheet(lowSS); + if( 0.95*this->pimpl->m_recipeObs->batchSize_l() <= this->pimpl->m_recipeObs->finalVolume_l() && this->pimpl->m_recipeObs->finalVolume_l() <= 1.05*this->pimpl->m_recipeObs->batchSize_l() ) + lineEdit_calcBatchSize->setStyleSheet(this->pimpl->goodSS); + else if( this->pimpl->m_recipeObs->finalVolume_l() < 0.95*this->pimpl->m_recipeObs->batchSize_l() ) + lineEdit_calcBatchSize->setStyleSheet(this->pimpl->lowSS); else - lineEdit_calcBatchSize->setStyleSheet(highSS); + lineEdit_calcBatchSize->setStyleSheet(this->pimpl->highSS); - if( 0.95*m_recipeObs->boilSize_l() <= m_recipeObs->boilVolume_l() && m_recipeObs->boilVolume_l() <= 1.05*m_recipeObs->boilSize_l() ) - lineEdit_calcBoilSize->setStyleSheet(goodSS); - else if( m_recipeObs->boilVolume_l() < 0.95* m_recipeObs->boilSize_l() ) - lineEdit_calcBoilSize->setStyleSheet(lowSS); + if( 0.95*this->pimpl->m_recipeObs->boilSize_l() <= this->pimpl->m_recipeObs->boilVolume_l() && this->pimpl->m_recipeObs->boilVolume_l() <= 1.05*this->pimpl->m_recipeObs->boilSize_l() ) + lineEdit_calcBoilSize->setStyleSheet(this->pimpl->goodSS); + else if( this->pimpl->m_recipeObs->boilVolume_l() < 0.95* this->pimpl->m_recipeObs->boilSize_l() ) + lineEdit_calcBoilSize->setStyleSheet(this->pimpl->lowSS); else - lineEdit_calcBoilSize->setStyleSheet(highSS); + lineEdit_calcBoilSize->setStyleSheet(this->pimpl->highSS); */ - this->lineEdit_boilSg->setQuantity(this->m_recipeObs->boilGrav()); + this->lineEdit_boilSg->setQuantity(this->pimpl->m_recipeObs->boilGrav()); - auto style = this->m_recipeObs->style(); + auto style = this->pimpl->m_recipeObs->style(); if (style) { updateDensitySlider(*this->styleRangeWidget_og, *this->oGLabel, style->ogMin(), style->ogMax(), 1.120); } - this->styleRangeWidget_og->setValue(this->oGLabel->getAmountToDisplay(m_recipeObs->og())); + this->styleRangeWidget_og->setValue(this->oGLabel->getAmountToDisplay(this->pimpl->m_recipeObs->og())); if (style) { updateDensitySlider(*this->styleRangeWidget_fg, *this->fGLabel, style->fgMin(), style->fgMax(), 1.030); } - this->styleRangeWidget_fg->setValue(this->fGLabel->getAmountToDisplay(m_recipeObs->fg())); + this->styleRangeWidget_fg->setValue(this->fGLabel->getAmountToDisplay(this->pimpl->m_recipeObs->fg())); - this->styleRangeWidget_abv->setValue(m_recipeObs->ABV_pct()); - this->styleRangeWidget_ibu->setValue(m_recipeObs->IBU()); + this->styleRangeWidget_abv->setValue(this->pimpl->m_recipeObs->ABV_pct()); + this->styleRangeWidget_ibu->setValue(this->pimpl->m_recipeObs->IBU()); this->rangeWidget_batchSize->setRange (0, - this->label_batchSize->getAmountToDisplay(this->m_recipeObs->batchSize_l())); + this->label_batchSize->getAmountToDisplay(this->pimpl->m_recipeObs->batchSize_l())); this->rangeWidget_batchSize->setPreferredRange(0, - this->label_batchSize->getAmountToDisplay(this->m_recipeObs->finalVolume_l())); - this->rangeWidget_batchSize->setValue (this->label_batchSize->getAmountToDisplay(this->m_recipeObs->finalVolume_l())); + this->label_batchSize->getAmountToDisplay(this->pimpl->m_recipeObs->finalVolume_l())); + this->rangeWidget_batchSize->setValue (this->label_batchSize->getAmountToDisplay(this->pimpl->m_recipeObs->finalVolume_l())); this->rangeWidget_boilsize->setRange (0, - this->label_boilSize->getAmountToDisplay(boilSize)); + this->label_boilSize->getAmountToDisplay(boilSize.value_or(0.0))); this->rangeWidget_boilsize->setPreferredRange(0, - this->label_boilSize->getAmountToDisplay(this->m_recipeObs->boilVolume_l())); - this->rangeWidget_boilsize->setValue (this->label_boilSize->getAmountToDisplay(this->m_recipeObs->boilVolume_l())); + this->label_boilSize->getAmountToDisplay(this->pimpl->m_recipeObs->boilVolume_l())); + this->rangeWidget_boilsize->setValue (this->label_boilSize->getAmountToDisplay(this->pimpl->m_recipeObs->boilVolume_l())); /* Colors need the same basic treatment as gravity */ if (style) { @@ -1932,21 +1958,21 @@ void MainWindow::showChanges(QMetaProperty* prop) { style->colorMin_srm(), style->colorMax_srm()); } - this->styleRangeWidget_srm->setValue(this->colorSRMLabel->getAmountToDisplay(this->m_recipeObs->color_srm())); + this->styleRangeWidget_srm->setValue(this->colorSRMLabel->getAmountToDisplay(this->pimpl->m_recipeObs->color_srm())); // In some, incomplete, recipes, OG is approximately 1.000, which then makes GU close to 0 and thus IBU/GU insanely // large. Besides being meaningless, such a large number takes up a lot of space. So, where gravity units are // below 1, we just show IBU on the IBU/GU slider. - auto gravityUnits = (m_recipeObs->og()-1)*1000; + auto gravityUnits = (this->pimpl->m_recipeObs->og()-1)*1000; if (gravityUnits < 1) { gravityUnits = 1; } - ibuGuSlider->setValue(m_recipeObs->IBU()/gravityUnits); + ibuGuSlider->setValue(this->pimpl->m_recipeObs->IBU()/gravityUnits); label_calories->setText( QString("%1").arg( Measurement::getDisplayUnitSystem(Measurement::PhysicalQuantity::Volume) == Measurement::UnitSystems::volume_Metric ? - m_recipeObs->caloriesPer33cl() : m_recipeObs->caloriesPerUs12oz(), + this->pimpl->m_recipeObs->caloriesPer33cl() : this->pimpl->m_recipeObs->caloriesPerUs12oz(), 0, 'f', 0 @@ -1954,33 +1980,36 @@ void MainWindow::showChanges(QMetaProperty* prop) { ); // See if we need to change the mash in the table. - if (this->m_recipeObs->mash() && (updateAll || propName == PropertyNames::Recipe::mash)) { - this->pimpl->m_mashStepTableModel->setMash(m_recipeObs->mash()); + if (this->pimpl->m_recipeObs->mash() && (updateAll || propName == PropertyNames::Recipe::mash)) { + this->pimpl->m_mashStepTableModel->setMash(this->pimpl->m_recipeObs->mash()); } // See if we need to change the boil in the table. - if (this->m_recipeObs->boil() && (updateAll || propName == PropertyNames::Recipe::boil)) { - this->pimpl->m_boilStepTableModel->setBoil(m_recipeObs->boil()); + if (this->pimpl->m_recipeObs->boil() && + (updateAll || + propName == PropertyNames::Recipe::boil || + propName == PropertyNames::Boil::boilSteps)) { + this->pimpl->m_boilStepTableModel->setBoil(this->pimpl->m_recipeObs->boil()); } // See if we need to change the fermentation in the table. - if (this->m_recipeObs->fermentation() && (updateAll || propName == PropertyNames::Recipe::fermentation)) { - this->pimpl->m_fermentationStepTableModel->setFermentation(m_recipeObs->fermentation()); + if (this->pimpl->m_recipeObs->fermentation() && (updateAll || propName == PropertyNames::Recipe::fermentation)) { + this->pimpl->m_fermentationStepTableModel->setFermentation(this->pimpl->m_recipeObs->fermentation()); } // Not sure about this, but I am annoyed that modifying the hop usage // modifiers isn't automatically updating my display if (updateAll) { - m_recipeObs->recalcIfNeeded(Hop::staticMetaObject.className()); + this->pimpl->m_recipeObs->recalcIfNeeded(Hop::staticMetaObject.className()); this->pimpl->m_hopAdditionsTableProxy->invalidate(); } return; } void MainWindow::updateRecipeName() { - if (m_recipeObs == nullptr || ! lineEdit_name->isModified()) { + if (this->pimpl->m_recipeObs == nullptr || ! lineEdit_name->isModified()) { return; } - this->doOrRedoUpdate(*this->m_recipeObs, + this->doOrRedoUpdate(*this->pimpl->m_recipeObs, TYPE_INFO(Recipe, NamedEntity, name), lineEdit_name->text(), tr("Change Recipe Name")); @@ -1988,11 +2017,11 @@ void MainWindow::updateRecipeName() { } void MainWindow::displayRangesEtcForCurrentRecipeStyle() { - if ( this->m_recipeObs == nullptr ) { + if ( this->pimpl->m_recipeObs == nullptr ) { return; } - auto style = this->m_recipeObs->style(); + auto style = this->pimpl->m_recipeObs->style(); if (!style) { return; } @@ -2011,8 +2040,12 @@ void MainWindow::displayRangesEtcForCurrentRecipeStyle() { return; } +// +// TODO: Would be good to harmonise how these updatRecipeFoo and dropRecipeFoo functions work +// + void MainWindow::updateRecipeStyle() { - if (m_recipeObs == nullptr) { + if (this->pimpl->m_recipeObs == nullptr) { return; } @@ -2021,9 +2054,9 @@ void MainWindow::updateRecipeStyle() { auto selected = ObjectStoreWrapper::getSharedFromRaw(this->pimpl->m_styleListModel->at(sourceIndex.row())); if (selected) { this->doOrRedoUpdate( - newRelationalUndoableUpdate(*this->m_recipeObs, + newRelationalUndoableUpdate(*this->pimpl->m_recipeObs, &Recipe::setStyle, - this->m_recipeObs->style(), + this->pimpl->m_recipeObs->style(), selected, &MainWindow::displayRangesEtcForCurrentRecipeStyle, tr("Change Recipe Style")) @@ -2033,7 +2066,7 @@ void MainWindow::updateRecipeStyle() { } void MainWindow::updateRecipeMash() { - if (this->m_recipeObs == nullptr) { + if (this->pimpl->m_recipeObs == nullptr) { return; } @@ -2042,9 +2075,9 @@ void MainWindow::updateRecipeMash() { ); if (selectedMash) { // The Recipe will decide whether it needs to make a copy of the Mash, hence why we don't reuse "selectedMash" below - this->m_recipeObs->setMash(selectedMash); - this->pimpl->m_mashEditor->setMash(this->m_recipeObs->mash()); - mashButton->setMash(this->m_recipeObs->mash()); + this->pimpl->m_recipeObs->setMash(selectedMash); + this->pimpl->m_mashEditor->setMash(this->pimpl->m_recipeObs->mash()); + mashButton->setMash(this->pimpl->m_recipeObs->mash()); } return; } @@ -2055,14 +2088,14 @@ void MainWindow::updateRecipeEquipment() { } void MainWindow::updateEquipmentButton() { - if (this->m_recipeObs != nullptr) { - this->equipmentButton->setEquipment(this->m_recipeObs->equipment()); + if (this->pimpl->m_recipeObs != nullptr) { + this->equipmentButton->setEquipment(this->pimpl->m_recipeObs->equipment()); } return; } void MainWindow::droppedRecipeEquipment(Equipment * kitRaw) { - if (m_recipeObs == nullptr) { + if (this->pimpl->m_recipeObs == nullptr) { return; } @@ -2074,15 +2107,15 @@ void MainWindow::droppedRecipeEquipment(Equipment * kitRaw) { auto kit = ObjectStoreWrapper::getSharedFromRaw(kitRaw); // We need to hang on to this QUndoCommand pointer because there might be other updates linked to it - see below - auto equipmentUpdate = newRelationalUndoableUpdate(*this->m_recipeObs, + auto equipmentUpdate = newRelationalUndoableUpdate(*this->pimpl->m_recipeObs, &Recipe::setEquipment, - this->m_recipeObs->equipment(), + this->pimpl->m_recipeObs->equipment(), kit, &MainWindow::updateEquipmentButton, tr("Change Recipe Kit")); // Keep the mash tun weight and specific heat up to date. - auto mash = m_recipeObs->mash(); + auto mash = this->pimpl->m_recipeObs->mash(); if (mash) { new SimpleUndoableUpdate(*mash, TYPE_INFO(Mash, mashTunWeight_kg ), kit->mashTunWeight_kg() , tr("Change Tun Weight") , equipmentUpdate); new SimpleUndoableUpdate(*mash, TYPE_INFO(Mash, mashTunSpecificHeat_calGC), kit->mashTunSpecificHeat_calGC(), tr("Change Tun Specific Heat"), equipmentUpdate); @@ -2099,9 +2132,9 @@ void MainWindow::droppedRecipeEquipment(Equipment * kitRaw) { // won't ever be seen by the user, but there's no harm in setting them. // (The previous call here to this->pimpl->m_mashEditor->setRecipe() was a roundabout way of calling setTunWeight_kg() and // setMashTunSpecificHeat_calGC() on the mash.) - new SimpleUndoableUpdate(*this->m_recipeObs, TYPE_INFO(Recipe, batchSize_l), kit->fermenterBatchSize_l(), tr("Change Batch Size"), equipmentUpdate); + new SimpleUndoableUpdate(*this->pimpl->m_recipeObs, TYPE_INFO(Recipe, batchSize_l), kit->fermenterBatchSize_l(), tr("Change Batch Size"), equipmentUpdate); - auto boil = this->m_recipeObs->nonOptBoil(); + auto boil = this->pimpl->m_recipeObs->nonOptBoil(); new SimpleUndoableUpdate(*boil, TYPE_INFO(Boil, preBoilSize_l), kit->kettleBoilSize_l(), tr("Change Boil Size"), equipmentUpdate); if (kit->boilTime_min()) { new SimpleUndoableUpdate(*boil, TYPE_INFO(Boil, boilTime_mins), *kit->boilTime_min(), tr("Change Boil Time"), equipmentUpdate); @@ -2115,7 +2148,7 @@ void MainWindow::droppedRecipeEquipment(Equipment * kitRaw) { // This isn't called when we think it is...! void MainWindow::droppedRecipeStyle(Style * styleRaw) { - if (!this->m_recipeObs) { + if (!this->pimpl->m_recipeObs) { qDebug() << Q_FUNC_INFO; return; } @@ -2123,9 +2156,9 @@ void MainWindow::droppedRecipeStyle(Style * styleRaw) { qDebug() << Q_FUNC_INFO << "Do or redo"; auto style = ObjectStoreWrapper::getSharedFromRaw(styleRaw); this->doOrRedoUpdate( - newRelationalUndoableUpdate(*this->m_recipeObs, + newRelationalUndoableUpdate(*this->pimpl->m_recipeObs, &Recipe::setStyle, - this->m_recipeObs->style(), + this->pimpl->m_recipeObs->style(), style, &MainWindow::displayRangesEtcForCurrentRecipeStyle, tr("Change Recipe Style")) @@ -2136,7 +2169,7 @@ void MainWindow::droppedRecipeStyle(Style * styleRaw) { // Well, aint this a kick in the pants. Apparently I can't template a slot void MainWindow::droppedRecipeFermentable(QList fermentables) { - if (!this->m_recipeObs) { + if (!this->pimpl->m_recipeObs) { return; } @@ -2144,10 +2177,11 @@ void MainWindow::droppedRecipeFermentable(QList fermentables) { tabWidget_ingredients->setCurrentWidget(recipeFermentableTab); } - auto fermentableAdditions = RecipeAdditionFermentable::create(*this->m_recipeObs, fermentables); + QList> fermentableAdditions = + RecipeAdditionFermentable::create(*this->pimpl->m_recipeObs, fermentables); this->doOrRedoUpdate( - newUndoableAddOrRemoveList(*this->m_recipeObs, + newUndoableAddOrRemoveList(*this->pimpl->m_recipeObs, &Recipe::addAddition, fermentableAdditions, &Recipe::removeAddition, @@ -2157,7 +2191,7 @@ void MainWindow::droppedRecipeFermentable(QList fermentables) { } void MainWindow::droppedRecipeHop(QList hops) { - if (!this->m_recipeObs) { + if (!this->pimpl->m_recipeObs) { return; } @@ -2165,10 +2199,10 @@ void MainWindow::droppedRecipeHop(QList hops) { tabWidget_ingredients->setCurrentWidget(recipeHopTab); } - auto hopAdditions = RecipeAdditionHop::create(*this->m_recipeObs, hops); + auto hopAdditions = RecipeAdditionHop::create(*this->pimpl->m_recipeObs, hops); this->doOrRedoUpdate( - newUndoableAddOrRemoveList(*this->m_recipeObs, + newUndoableAddOrRemoveList(*this->pimpl->m_recipeObs, &Recipe::addAddition, hopAdditions, &Recipe::removeAddition, @@ -2178,7 +2212,7 @@ void MainWindow::droppedRecipeHop(QList hops) { } void MainWindow::droppedRecipeMisc(QList miscs) { - if (!this->m_recipeObs) { + if (!this->pimpl->m_recipeObs) { return; } @@ -2186,10 +2220,10 @@ void MainWindow::droppedRecipeMisc(QList miscs) { tabWidget_ingredients->setCurrentWidget(recipeMiscTab); } - auto miscAdditions = RecipeAdditionMisc::create(*this->m_recipeObs, miscs); + auto miscAdditions = RecipeAdditionMisc::create(*this->pimpl->m_recipeObs, miscs); this->doOrRedoUpdate( - newUndoableAddOrRemoveList(*this->m_recipeObs, + newUndoableAddOrRemoveList(*this->pimpl->m_recipeObs, &Recipe::addAddition, miscAdditions, &Recipe::removeAddition, @@ -2199,7 +2233,7 @@ void MainWindow::droppedRecipeMisc(QList miscs) { } void MainWindow::droppedRecipeYeast(QList yeasts) { - if (!this->m_recipeObs) { + if (!this->pimpl->m_recipeObs) { return; } @@ -2207,10 +2241,10 @@ void MainWindow::droppedRecipeYeast(QList yeasts) { tabWidget_ingredients->setCurrentWidget(recipeYeastTab); } - auto yeastAdditions = RecipeAdditionYeast::create(*this->m_recipeObs, yeasts); + auto yeastAdditions = RecipeAdditionYeast::create(*this->pimpl->m_recipeObs, yeasts); this->doOrRedoUpdate( - newUndoableAddOrRemoveList(*this->m_recipeObs, + newUndoableAddOrRemoveList(*this->pimpl->m_recipeObs, &Recipe::addAddition, yeastAdditions, &Recipe::removeAddition, @@ -2220,58 +2254,59 @@ void MainWindow::droppedRecipeYeast(QList yeasts) { } void MainWindow::updateRecipeBatchSize() { - if (!this->m_recipeObs) { + if (!this->pimpl->m_recipeObs) { return; } - this->doOrRedoUpdate(*this->m_recipeObs, + this->doOrRedoUpdate(*this->pimpl->m_recipeObs, TYPE_INFO(Recipe, batchSize_l), lineEdit_batchSize->getNonOptCanonicalQty(), tr("Change Batch Size")); return; } -void MainWindow::updateRecipeBoilSize() { - if (!this->m_recipeObs) { - return; - } - - auto boil = this->m_recipeObs->nonOptBoil(); - this->doOrRedoUpdate(*boil, - TYPE_INFO(Boil, preBoilSize_l), - lineEdit_boilSize->getNonOptCanonicalQty(), - tr("Change Boil Size")); - return; -} - -void MainWindow::updateRecipeBoilTime() { - if (!this->m_recipeObs) { - return; - } - - auto kit = m_recipeObs->equipment(); - double boilTime = Measurement::qStringToSI(lineEdit_boilTime->text(), Measurement::PhysicalQuantity::Time).quantity; - - // Here, we rely on a signal/slot connection to propagate the equipment changes to m_recipeObs->boilTime_min and maybe - // m_recipeObs->boilSize_l - // NOTE: This works because kit is the recipe's equipment, not the generic equipment in the recipe drop down. - if (kit) { - this->doOrRedoUpdate(*kit, TYPE_INFO(Equipment, boilTime_min), boilTime, tr("Change Boil Time")); - } else { - auto boil = this->m_recipeObs->nonOptBoil(); - this->doOrRedoUpdate(*boil, TYPE_INFO(Boil, boilTime_mins), boilTime, tr("Change Boil Time")); - } - - return; -} +///void MainWindow::updateRecipeBoilSize() { +/// if (!this->pimpl->m_recipeObs) { +/// return; +/// } +/// +/// // See comments in model/Boil.h for why boil size is, technically, optional +/// auto boil = this->pimpl->m_recipeObs->nonOptBoil(); +/// this->doOrRedoUpdate(*boil, +/// TYPE_INFO(Boil, preBoilSize_l), +/// lineEdit_boilSize->getOptCanonicalQty(), +/// tr("Change Boil Size")); +/// return; +///} + +///void MainWindow::updateRecipeBoilTime() { +/// if (!this->pimpl->m_recipeObs) { +/// return; +/// } +/// +/// auto kit = this->pimpl->m_recipeObs->equipment(); +/// double boilTime = Measurement::qStringToSI(lineEdit_boilTime->text(), Measurement::PhysicalQuantity::Time).quantity; +/// +/// // Here, we rely on a signal/slot connection to propagate the equipment changes to this->pimpl->m_recipeObs->boilTime_min and maybe +/// // this->pimpl->m_recipeObs->boilSize_l +/// // NOTE: This works because kit is the recipe's equipment, not the generic equipment in the recipe drop down. +/// if (kit) { +/// this->doOrRedoUpdate(*kit, TYPE_INFO(Equipment, boilTime_min), boilTime, tr("Change Boil Time")); +/// } else { +/// auto boil = this->pimpl->m_recipeObs->nonOptBoil(); +/// this->doOrRedoUpdate(*boil, TYPE_INFO(Boil, boilTime_mins), boilTime, tr("Change Boil Time")); +/// } +/// +/// return; +///} void MainWindow::updateRecipeEfficiency() { qDebug() << Q_FUNC_INFO << lineEdit_efficiency->getNonOptValue(); - if (!this->m_recipeObs) { + if (!this->pimpl->m_recipeObs) { return; } - this->doOrRedoUpdate(*this->m_recipeObs, + this->doOrRedoUpdate(*this->pimpl->m_recipeObs, TYPE_INFO(Recipe, efficiency_pct), lineEdit_efficiency->getNonOptValue(), tr("Change Recipe Efficiency")); @@ -2279,11 +2314,11 @@ void MainWindow::updateRecipeEfficiency() { } template -void MainWindow::addIngredientToRecipe(NE * ne) { - if (!this->m_recipeObs) { +void MainWindow::addIngredientToRecipe(NE & ne) { + if (!this->pimpl->m_recipeObs) { return; } - auto neAddition = std::make_shared(*this->m_recipeObs, *ne); + auto neAddition = std::make_shared(*this->pimpl->m_recipeObs, ne); this->pimpl->doRecipeAddition(neAddition); return; } @@ -2291,10 +2326,10 @@ void MainWindow::addIngredientToRecipe(NE * ne) { // Instantiate the above template function for the types that are going to use it // (This is all just a trick to allow the template definition to be here in the .cpp file and not in the header.) // -template void MainWindow::addIngredientToRecipe(Fermentable * ne); -template void MainWindow::addIngredientToRecipe(Hop * ne); -template void MainWindow::addIngredientToRecipe(Misc * ne); -template void MainWindow::addIngredientToRecipe(Yeast * ne); +template void MainWindow::addIngredientToRecipe(Fermentable & ne); +template void MainWindow::addIngredientToRecipe(Hop & ne); +template void MainWindow::addIngredientToRecipe(Misc & ne); +template void MainWindow::addIngredientToRecipe(Yeast & ne); void MainWindow::removeSelectedFermentableAddition() { this->pimpl->doRemoveRecipeAddition(fermentableAdditionTable, @@ -2325,15 +2360,15 @@ void MainWindow::removeSelectedYeastAddition() { } void MainWindow::addMashStepToMash(std::shared_ptr mashStep) { - this->pimpl->addStepToStepOwner(this->m_recipeObs->mash(), mashStep); + this->pimpl->addStepToStepOwner(this->pimpl->m_recipeObs->mash(), mashStep); return; } void MainWindow::addBoilStepToBoil(std::shared_ptr boilStep) { - this->pimpl->addStepToStepOwner(this->m_recipeObs->boil(), boilStep); + this->pimpl->addStepToStepOwner(this->pimpl->m_recipeObs->boil(), boilStep); return; } void MainWindow::addFermentationStepToFermentation(std::shared_ptr fermentationStep) { - this->pimpl->addStepToStepOwner(this->m_recipeObs->fermentation(), fermentationStep); + this->pimpl->addStepToStepOwner(this->pimpl->m_recipeObs->fermentation(), fermentationStep); return; } @@ -2341,19 +2376,19 @@ void MainWindow::addFermentationStepToFermentation(std::shared_ptrm_recipeObs) { + if (!this->pimpl->m_recipeObs) { return; } QList recipes; - recipes.append(this->m_recipeObs); + recipes.append(this->pimpl->m_recipeObs); ImportExport::exportToFile(&recipes); return; } Recipe* MainWindow::currentRecipe() { - return m_recipeObs; + return this->pimpl->m_recipeObs; } void MainWindow::setUndoRedoEnable() { @@ -2680,7 +2715,7 @@ void MainWindow::reduceInventory() { } // Make sure everything is properly set and selected - if (rec != m_recipeObs) { + if (rec != this->pimpl->m_recipeObs) { setRecipe(rec); } @@ -2727,7 +2762,7 @@ void MainWindow::newBrewNote() { } // Make sure everything is properly set and selected - if (rec != m_recipeObs) { + if (rec != this->pimpl->m_recipeObs) { setRecipe(rec); } @@ -2736,7 +2771,7 @@ void MainWindow::newBrewNote() { bNote->setBrewDate(); ObjectStoreWrapper::insert(bNote); - this->setBrewNote(bNote.get()); + this->pimpl->setBrewNote(bNote.get()); bIndex = treeView_recipe->findElement(bNote.get()); if (bIndex.isValid()) { @@ -2760,11 +2795,11 @@ void MainWindow::reBrewNote() { bNote->setBrewDate(); ObjectStoreWrapper::insert(bNote); - if (rec != m_recipeObs) { + if (rec != this->pimpl->m_recipeObs) { setRecipe(rec); } - setBrewNote(bNote.get()); + this->pimpl->setBrewNote(bNote.get()); setTreeSelection(treeView_recipe->findElement(bNote.get())); } @@ -2827,10 +2862,10 @@ void MainWindow::importFiles() { return; } -bool MainWindow::verifyImport(QString tag, QString name) { - return QMessageBox::question(this, tr("Import %1?").arg(tag), tr("Import %1?").arg(name), - QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes; -} +///bool MainWindow::verifyImport(QString tag, QString name) { +/// return QMessageBox::question(this, tr("Import %1?").arg(tag), tr("Import %1?").arg(name), +/// QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes; +///} void MainWindow::addMashStep () { this->pimpl->newStep(); return; } void MainWindow::addBoilStep () { this->pimpl->newStep(); return; } @@ -2867,7 +2902,7 @@ void MainWindow::removeMash() { auto defaultMash = std::make_shared(); ObjectStoreWrapper::insert(defaultMash); - this->m_recipeObs->setMash(defaultMash); + this->pimpl->m_recipeObs->setMash(defaultMash); this->pimpl->m_mashStepTableModel->setMash(defaultMash); @@ -2880,8 +2915,8 @@ void MainWindow::closeEvent(QCloseEvent* /*event*/) { Application::saveSystemOptions(); PersistentSettings::insert(PersistentSettings::Names::geometry, saveGeometry()); PersistentSettings::insert(PersistentSettings::Names::windowState, saveState()); - if ( m_recipeObs ) - PersistentSettings::insert(PersistentSettings::Names::recipeKey, m_recipeObs->key()); + if ( this->pimpl->m_recipeObs ) + PersistentSettings::insert(PersistentSettings::Names::recipeKey, this->pimpl->m_recipeObs->key()); // UI save state this->pimpl->saveUiState(PersistentSettings::Names::splitter_vertical_State , splitter_vertical ); @@ -2910,18 +2945,18 @@ void MainWindow::copyRecipe() { return; } - auto newRec = std::make_shared(*this->m_recipeObs); // Create a deep copy + auto newRec = std::make_shared(*this->pimpl->m_recipeObs); // Create a deep copy newRec->setName(name); ObjectStoreTyped::getInstance().insert(newRec); return; } void MainWindow::saveMash() { - if (!this->m_recipeObs || !this->m_recipeObs->mash()) { + if (!this->pimpl->m_recipeObs || !this->pimpl->m_recipeObs->mash()) { return; } - auto mash = m_recipeObs->mash(); + auto mash = this->pimpl->m_recipeObs->mash(); // Ensure the mash has a name. if( mash->name() == "" ) { QMessageBox::information( this, tr("Oops!"), tr("Please give your mash a name before saving.") ); @@ -2931,7 +2966,7 @@ void MainWindow::saveMash() { // The current UI doesn't make this 100% clear, but what we're actually doing here is saving a _copy_ of the current // Recipe's mash. - // NOTE: should NOT displace m_recipeObs' current mash. + // NOTE: should NOT displace this->pimpl->m_recipeObs' current mash. auto newMash = ObjectStoreWrapper::insertCopyOf(*mash); // NOTE: need to set the display to true for the saved, named mash to work newMash->setDisplay(true); @@ -2968,46 +3003,38 @@ void MainWindow::contextMenu(const QPoint &point) { void MainWindow::setupContextMenu() { - treeView_recipe->setupContextMenu(this, this); - treeView_equip->setupContextMenu(this, this->pimpl->m_equipEditor.get()); - treeView_ferm ->setupContextMenu(this, this->pimpl->m_fermentableEditor .get()); - treeView_hops ->setupContextMenu(this, this->pimpl->m_hopEditor .get()); - treeView_misc ->setupContextMenu(this, this->pimpl->m_miscEditor .get()); - treeView_style->setupContextMenu(this, this->pimpl->m_styleEditor.get()); - treeView_yeast->setupContextMenu(this, this->pimpl->m_yeastEditor .get()); - treeView_water->setupContextMenu(this, this->pimpl->m_waterEditor .get()); + this->treeView_recipe->setupContextMenu(this, this); + this->treeView_style ->setupContextMenu(this, this->pimpl->m_styleEditor .get()); + this->treeView_equip ->setupContextMenu(this, this->pimpl->m_equipEditor .get()); + this->treeView_ferm ->setupContextMenu(this, this->pimpl->m_fermentableEditor.get()); + this->treeView_hops ->setupContextMenu(this, this->pimpl->m_hopEditor .get()); + this->treeView_misc ->setupContextMenu(this, this->pimpl->m_miscEditor .get()); + this->treeView_yeast ->setupContextMenu(this, this->pimpl->m_yeastEditor .get()); + this->treeView_water ->setupContextMenu(this, this->pimpl->m_waterEditor .get()); // TreeView for clicks, both double and right - connect( treeView_recipe, &QAbstractItemView::doubleClicked, this, &MainWindow::treeActivated); - connect( treeView_recipe, &QWidget::customContextMenuRequested, this, &MainWindow::contextMenu); - - connect( treeView_equip, &QAbstractItemView::doubleClicked, this, &MainWindow::treeActivated); - connect( treeView_equip, &QWidget::customContextMenuRequested, this, &MainWindow::contextMenu); - - connect( treeView_ferm, &QAbstractItemView::doubleClicked, this, &MainWindow::treeActivated); - connect( treeView_ferm, &QWidget::customContextMenuRequested, this, &MainWindow::contextMenu); - - connect( treeView_hops, &QAbstractItemView::doubleClicked, this, &MainWindow::treeActivated); - connect( treeView_hops, &QWidget::customContextMenuRequested, this, &MainWindow::contextMenu); - - connect( treeView_misc, &QAbstractItemView::doubleClicked, this, &MainWindow::treeActivated); - connect( treeView_misc, &QWidget::customContextMenuRequested, this, &MainWindow::contextMenu); - - connect( treeView_yeast, &QAbstractItemView::doubleClicked, this, &MainWindow::treeActivated); - connect( treeView_yeast, &QWidget::customContextMenuRequested, this, &MainWindow::contextMenu); - - connect( treeView_style, &QAbstractItemView::doubleClicked, this, &MainWindow::treeActivated); - connect( treeView_style, &QWidget::customContextMenuRequested, this, &MainWindow::contextMenu); - - connect( treeView_water, &QAbstractItemView::doubleClicked, this, &MainWindow::treeActivated); - connect( treeView_water, &QWidget::customContextMenuRequested, this, &MainWindow::contextMenu); + connect(treeView_recipe, &QAbstractItemView::doubleClicked , this, &MainWindow::treeActivated); + connect(treeView_recipe, &QWidget::customContextMenuRequested, this, &MainWindow::contextMenu ); + connect(treeView_style , &QAbstractItemView::doubleClicked , this, &MainWindow::treeActivated); + connect(treeView_style , &QWidget::customContextMenuRequested, this, &MainWindow::contextMenu ); + connect(treeView_equip , &QAbstractItemView::doubleClicked , this, &MainWindow::treeActivated); + connect(treeView_equip , &QWidget::customContextMenuRequested, this, &MainWindow::contextMenu ); + connect(treeView_ferm , &QAbstractItemView::doubleClicked , this, &MainWindow::treeActivated); + connect(treeView_ferm , &QWidget::customContextMenuRequested, this, &MainWindow::contextMenu ); + connect(treeView_hops , &QAbstractItemView::doubleClicked , this, &MainWindow::treeActivated); + connect(treeView_hops , &QWidget::customContextMenuRequested, this, &MainWindow::contextMenu ); + connect(treeView_misc , &QAbstractItemView::doubleClicked , this, &MainWindow::treeActivated); + connect(treeView_misc , &QWidget::customContextMenuRequested, this, &MainWindow::contextMenu ); + connect(treeView_yeast , &QAbstractItemView::doubleClicked , this, &MainWindow::treeActivated); + connect(treeView_yeast , &QWidget::customContextMenuRequested, this, &MainWindow::contextMenu ); + connect(treeView_water , &QAbstractItemView::doubleClicked , this, &MainWindow::treeActivated); + connect(treeView_water , &QWidget::customContextMenuRequested, this, &MainWindow::contextMenu ); return; } void MainWindow::copySelected() { - QModelIndexList selected; +/// QModelIndexList selected; TreeView* active = qobject_cast(tabWidget_Trees->currentWidget()->focusWidget()); - active->copySelected(active->selectionModel()->selectedRows()); return; } @@ -3124,9 +3151,9 @@ void MainWindow::redisplayLabel() { void MainWindow::showPitchDialog() { // First, copy the current recipe og and volume. - if (m_recipeObs) { - this->pimpl->m_pitchDialog->setWortVolume_l( m_recipeObs->finalVolume_l() ); - this->pimpl->m_pitchDialog->setWortDensity( m_recipeObs->og() ); + if (this->pimpl->m_recipeObs) { + this->pimpl->m_pitchDialog->setWortVolume_l( this->pimpl->m_recipeObs->finalVolume_l() ); + this->pimpl->m_pitchDialog->setWortDensity( this->pimpl->m_recipeObs->og() ); this->pimpl->m_pitchDialog->calculate(); } @@ -3135,20 +3162,20 @@ void MainWindow::showPitchDialog() { } void MainWindow::showEquipmentEditor() { - if ( m_recipeObs && ! m_recipeObs->equipment() ) { - QMessageBox::warning( this, tr("No equipment"), tr("You must select or define an equipment profile first.")); + if (this->pimpl->m_recipeObs && ! this->pimpl->m_recipeObs->equipment()) { + QMessageBox::warning(this, tr("No equipment"), tr("You must select or define an equipment profile first.")); } else { - this->pimpl->m_equipEditor->setEditItem(m_recipeObs->equipment()); + this->pimpl->m_equipEditor->setEditItem(this->pimpl->m_recipeObs->equipment()); this->pimpl->m_equipEditor->show(); } return; } void MainWindow::showStyleEditor() { - if ( m_recipeObs && ! m_recipeObs->style() ) { + if ( this->pimpl->m_recipeObs && ! this->pimpl->m_recipeObs->style() ) { QMessageBox::warning( this, tr("No style"), tr("You must select a style first.")); } else { - this->pimpl->m_styleEditor->setEditItem(m_recipeObs->style()); + this->pimpl->m_styleEditor->setEditItem(this->pimpl->m_recipeObs->style()); this->pimpl->m_styleEditor->show(); } return; @@ -3171,7 +3198,7 @@ void MainWindow::changeBrewDate() { target->setBrewDate(newDate); // If this note is open in a tab - BrewNoteWidget* ni = findBrewNoteWidget(target); + BrewNoteWidget* ni = this->pimpl->findBrewNoteWidget(target); if (ni) { tabWidget_recipeView->setTabText(tabWidget_recipeView->indexOf(ni), target->brewDate_short()); return; @@ -3224,11 +3251,11 @@ void MainWindow::closeBrewNote([[maybe_unused]] int brewNoteId, std::shared_ptr< // If this isn't the focused recipe, do nothing because there are no tabs // to close. - if (parent != m_recipeObs) { + if (parent != this->pimpl->m_recipeObs) { return; } - BrewNoteWidget* ni = findBrewNoteWidget(b); + BrewNoteWidget* ni = this->pimpl->findBrewNoteWidget(b); if (ni) { tabWidget_recipeView->removeTab( tabWidget_recipeView->indexOf(ni)); } @@ -3237,9 +3264,9 @@ void MainWindow::closeBrewNote([[maybe_unused]] int brewNoteId, std::shared_ptr< } void MainWindow::showWaterChemistryTool() { - if (this->m_recipeObs) { - if (m_recipeObs->mash() && m_recipeObs->mash()->mashSteps().size() > 0) { - this->pimpl->m_waterDialog->setRecipe(m_recipeObs); + if (this->pimpl->m_recipeObs) { + if (this->pimpl->m_recipeObs->mash() && this->pimpl->m_recipeObs->mash()->mashSteps().size() > 0) { + this->pimpl->m_waterDialog->setRecipe(this->pimpl->m_recipeObs); this->pimpl->m_waterDialog->show(); return; } diff --git a/src/MainWindow.h b/src/MainWindow.h index fb8e52a8..c09aa7ab 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -33,27 +33,19 @@ #include #include -#include -#include -#include #include -#include #include #include #include "ui_mainWindow.h" + #include "undoRedo/SimpleUndoableUpdate.h" #include "utils/NoCopy.h" // Forward Declarations - -class BrewNoteWidget; -class OptionDialog; class PropertyPath; class Recipe; -class RecipeAdditionHop; - /*! * \class MainWindow * @@ -62,7 +54,6 @@ class RecipeAdditionHop; class MainWindow : public QMainWindow, public Ui::mainWindow { Q_OBJECT - friend class OptionDialog; public: MainWindow(QWidget* parent=nullptr); virtual ~MainWindow(); @@ -80,7 +71,9 @@ class MainWindow : public QMainWindow, public Ui::mainWindow { this->connect(a, b, c.get(), d); } + //! \brief Accessor to obtain \c MainWindow singleton static MainWindow & instance(); + /** * \brief Call at program termination to clean-up. Caller's responsibility not to subsequently call (or use the * return value from) \c MainWindow::instance(). @@ -98,12 +91,7 @@ class MainWindow : public QMainWindow, public Ui::mainWindow { //! \brief Get the currently observed recipe. Recipe* currentRecipe(); - bool verifyImport(QString tag, QString name); - bool verifyDelete(QString tab, QString name); - - void setBrewNoteByIndex(const QModelIndex &index); - void setBrewNote(BrewNote* bNote); - +public: //! \brief Doing updates via this method makes them undoable (and redoable). This is the simplified version // which suffices for modifications to most individual non-relational attributes. template @@ -136,10 +124,7 @@ class MainWindow : public QMainWindow, public Ui::mainWindow { * * Fortunately this does not need to be a slot function (as slots cannot be templated) */ - template void addIngredientToRecipe(NE * ne); - - void addFermentableToRecipe(Fermentable * fermentable); - void addHopToRecipe(Hop * hop); + template void addIngredientToRecipe(NE & ne); public slots: @@ -161,9 +146,9 @@ public slots: //! \brief Update Recipe batch size to that given by the relevant widget. void updateRecipeBatchSize(); //! \brief Update Recipe boil size to that given by the relevant widget. - void updateRecipeBoilSize(); +/// void updateRecipeBoilSize(); //! \brief Update Recipe boil time to that given by the relevant widget. - void updateRecipeBoilTime(); +/// void updateRecipeBoilTime(); //! \brief Update Recipe efficiency to that given by the relevant widget. void updateRecipeEfficiency(); //! \brief Update Recipe's mash @@ -241,9 +226,9 @@ public slots: //! \brief Create a new recipe in the database. void newRecipe(); - //! \brief Export current recipe to BeerXML. + //! \brief Export current recipe to BeerXML or BeerJSON. void exportRecipe(); - //! \brief Display file selection dialog and import BeerXML files. + //! \brief Display file selection dialog and import BeerXML/BeerJSON files. void importFiles(); //! \brief Create a duplicate of the current recipe. void copyRecipe(); @@ -269,8 +254,7 @@ public slots: //! \brief makes sure we can do water chemistry before we show the window void showWaterChemistryTool(); - //! \brief draws a context menu, the exact nature of which depends on which - //tree is focused + //! \brief draws a context menu, the exact nature of which depends on which tree is focused void contextMenu(const QPoint &point); //! \brief creates a new brewnote void newBrewNote(); @@ -308,11 +292,13 @@ public slots: //! \brief prepopulate the ancestorDialog when the menu is selected void setAncestor(); +public: /*! * \brief Make the widgets in the window update changes. * - * Updates all the widgets with info about the currently - * selected Recipe, except for the tables. + * Updates all the widgets with info about the currently selected Recipe, except for the tables. + * + * Called by \c Recipe and \c OptionDialog::saveLoggingSettings * * \param prop Not yet used. Will indicate which Recipe property has changed. */ @@ -340,22 +326,6 @@ private slots: // pointers get passed to UndoableAddOrRemove. We should fix that at some point. template void remove(std::shared_ptr itemToRemove); - Recipe* m_recipeObs; - - QString highSS, lowSS, goodSS, boldSS; // Palette replacements - - QList contextMenus; - QDialog* brewDayDialog; - QPrinter *printer; - - int confirmDelete; - - //! \brief Fix pixel dimensions according to dots-per-inch (DPI) of screen we're on. - void setSizesInPixelsBasedOnDpi(); - - //! \brief Find an open brewnote tab, if it is open - BrewNoteWidget* findBrewNoteWidget(BrewNote* b); - //! \brief Scroll to the given \c item in the currently visible item tree. void setTreeSelection(QModelIndex item); diff --git a/src/OptionDialog.cpp b/src/OptionDialog.cpp index a49d611a..afc0713f 100644 --- a/src/OptionDialog.cpp +++ b/src/OptionDialog.cpp @@ -471,7 +471,7 @@ class OptionDialog::impl { optionDialog.colorFormulaComboBox->findData(ColorMethods::colorFormula) ); optionDialog.ibuFormulaComboBox->setCurrentIndex( - optionDialog.ibuFormulaComboBox->findData(IbuMethods::ibuFormula) + optionDialog.ibuFormulaComboBox->findData(static_cast(IbuMethods::ibuFormula)) ); // User data directory @@ -657,13 +657,13 @@ void OptionDialog::configure_formulaCombos() { QVariant(Measurement::UnitSystems::diastaticPower_WindischKolbach.uniqueName)); // Populate combo boxes on the "Formulas" tab - ibuFormulaComboBox->addItem(tr("Tinseth's approximation"), QVariant(IbuMethods::TINSETH)); - ibuFormulaComboBox->addItem(tr("Rager's approximation"), QVariant(IbuMethods::RAGER)); - ibuFormulaComboBox->addItem(tr("Noonan's approximation"), QVariant(IbuMethods::NOONAN)); + ibuFormulaComboBox->addItem(tr("Tinseth's approximation"), QVariant(static_cast(IbuMethods::IbuFormula::Tinseth))); + ibuFormulaComboBox->addItem(tr("Rager's approximation" ), QVariant(static_cast(IbuMethods::IbuFormula::Rager ))); + ibuFormulaComboBox->addItem(tr("Noonan's approximation" ), QVariant(static_cast(IbuMethods::IbuFormula::Noonan ))); colorFormulaComboBox->addItem(tr("Mosher's approximation"), QVariant(ColorMethods::MOSHER)); colorFormulaComboBox->addItem(tr("Daniel's approximation"), QVariant(ColorMethods::DANIEL)); - colorFormulaComboBox->addItem(tr("Morey's approximation"), QVariant(ColorMethods::MOREY)); + colorFormulaComboBox->addItem(tr("Morey's approximation" ), QVariant(ColorMethods::MOREY)); } void OptionDialog::configure_logging() { @@ -907,7 +907,7 @@ void OptionDialog::saveFormulae() { bool okay = false; int ndx = ibuFormulaComboBox->itemData(ibuFormulaComboBox->currentIndex()).toInt(&okay); - IbuMethods::ibuFormula = static_cast(ndx); + IbuMethods::ibuFormula = static_cast(ndx); ndx = colorFormulaComboBox->itemData(colorFormulaComboBox->currentIndex()).toInt(&okay); ColorMethods::colorFormula = static_cast(ndx); diff --git a/src/RecipeFormatter.cpp b/src/RecipeFormatter.cpp index 16cf65e9..91ebf24e 100644 --- a/src/RecipeFormatter.cpp +++ b/src/RecipeFormatter.cpp @@ -1461,7 +1461,7 @@ QString RecipeFormatter::getToolTip(Yeast* yeast) { .arg(yeast->laboratory()); body += QString("%1%2 %") .arg(tr("Attenuation")) - .arg(Measurement::displayQuantity(yeast->getTypicalAttenuation_pct(), 0)); + .arg(Measurement::displayQuantity(yeast->attenuationTypical_pct(), 0)); // Third row -- prod id and flocculation body += QString("%1%2") diff --git a/src/WaterDialog.cpp b/src/WaterDialog.cpp index 1a7cd3f9..270d2ca0 100644 --- a/src/WaterDialog.cpp +++ b/src/WaterDialog.cpp @@ -134,8 +134,8 @@ WaterDialog::WaterDialog(QWidget* parent) : m_ppm_digits[ii]->setLimits(0.0,1000.0); m_ppm_digits[ii]->setQuantity(0.0); m_ppm_digits[ii]->setMessages(tr("Too low for target profile."), - tr("In range for target profile."), - tr("Too high for target profile.")); + tr("In range for target profile."), + tr("Too high for target profile.")); } // we can be a bit more specific with pH btDigit_ph->setLowLim(5.0); @@ -163,9 +163,9 @@ WaterDialog::WaterDialog(QWidget* parent) : connect(baseProfileButton, &WaterButton::clicked, m_base_editor, &QWidget::show); connect(targetProfileButton, &WaterButton::clicked, m_target_editor, &QWidget::show); - connect(m_saltAdjustmentTableModel, &RecipeAdjustmentSaltTableModel::newTotals, this, &WaterDialog::newTotals ); - connect(pushButton_addSalt, &QAbstractButton::clicked, m_saltAdjustmentTableModel, &RecipeAdjustmentSaltTableModel::catchSalt); - connect(pushButton_removeSalt, &QAbstractButton::clicked, this, &WaterDialog::removeSalts ); + connect(m_saltAdjustmentTableModel, &RecipeAdjustmentSaltTableModel::newTotals, this , &WaterDialog::newTotals ); + connect(pushButton_addSalt , &QAbstractButton::clicked , m_saltAdjustmentTableModel, &RecipeAdjustmentSaltTableModel::catchSalt); + connect(pushButton_removeSalt , &QAbstractButton::clicked , this , &WaterDialog::removeSalts ); connect(spinBox_mashRO, QOverload::of(&QSpinBox::valueChanged), this, &WaterDialog::setMashRO ); connect(spinBox_spargeRO, QOverload::of(&QSpinBox::valueChanged), this, &WaterDialog::setSpargeRO); diff --git a/src/catalogs/CatalogBase.h b/src/catalogs/CatalogBase.h index 665b5944..f655751f 100644 --- a/src/catalogs/CatalogBase.h +++ b/src/catalogs/CatalogBase.h @@ -31,6 +31,7 @@ #include "database/ObjectStoreWrapper.h" #include "MainWindow.h" +#include "model/Ingredient.h" #include "utils/CuriouslyRecurringTemplateBase.h" // TBD: Double-click does different things depending on whether you're looking at list of things in a recipe or @@ -82,7 +83,7 @@ * the code for the definitions of all these functions is "the same" for all editors, and should be inserted in * the implementation file using the CATALOG_COMMON_CODE macro. Eg, in HopDialog, we need: * - * CATALOG_COMMON_CODE(HopDialog) + * CATALOG_COMMON_CODE(Hop) * * There is not much to the rest of the derived class (eg HopDialog). * @@ -217,22 +218,17 @@ class CatalogBase : public CuriouslyRecurringTemplateBase && HasInventory { -/// m_neTableModel->setInventoryEditable(true); -/// return; -/// } -/// void enableEditableInventory() requires IsTableModel && HasNoInventory { -/// // No-op version -/// return; -/// } - /** * \brief Subclass should call this from its \c addItem slot * * If \b index is the default, will add the selected ingredient to list. Otherwise, will add the ingredient * at the specified index. */ - void add(QModelIndex const & index = QModelIndex()) requires IsTableModel && ObservesRecipe { +// void add(QModelIndex const & index = QModelIndex()) requires IsTableModel && ObservesRecipe { + void add(QModelIndex const & index = QModelIndex()) requires IsIngredient { + // + // Substantive version - for FermentableCatalog, HopCatalog, MiscCatalog, YeastCatalog + // qDebug() << Q_FUNC_INFO << "Index: " << index; QModelIndex translated; @@ -265,11 +261,14 @@ class CatalogBase : public CuriouslyRecurringTemplateBaseaddIngredientToRecipe(m_neTableModel->getRow(translated.row())); + m_parent->addIngredientToRecipe(*m_neTableModel->getRow(translated.row())); return; } - void add([[maybe_unused]] QModelIndex const & index = QModelIndex()) requires IsTableModel && DoesNotObserveRecipe { + void add([[maybe_unused]] QModelIndex const & index = QModelIndex()) requires IsTableModel && IsNotIngredient { + // + // No-op version - for EquipmentCatalog, StyleCatalog + // qDebug() << Q_FUNC_INFO << "No-op"; // No-op version return; diff --git a/src/config.h.in b/src/config.h.in index 58b959d1..8f75c7fb 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -49,6 +49,7 @@ constexpr char const * CONFIG_CXX_COMPILER_ID = "@CONFIG_CXX_COMPILER_ID@"; constexpr char const * CONFIG_BUILD_TIMESTAMP = "@CONFIG_BUILD_TIMESTAMP@"; // Support info -constexpr char const * CONFIG_HOMEPAGE_URL = "@CONFIG_HOMEPAGE_URL@"; +constexpr char const * CONFIG_GITHUB_URL = "@CONFIG_GITHUB_URL@"; +constexpr char const * CONFIG_WEBSITE_URL = "@CONFIG_WEBSITE_URL@"; #endif diff --git a/src/config.in b/src/config.in index 1791b1f7..60e01cdd 100644 --- a/src/config.in +++ b/src/config.in @@ -49,6 +49,7 @@ constexpr char const * CONFIG_CXX_COMPILER_ID = "${CMAKE_CXX_COMPILER_ID}"; constexpr char const * CONFIG_BUILD_TIMESTAMP = "${BUILD_TIMESTAMP}"; // Support info -constexpr char const * CONFIG_HOMEPAGE_URL = "${CONFIG_HOMEPAGE_URL}"; +constexpr char const * CONFIG_GITHUB_URL = "${CONFIG_GITHUB_URL}"; +constexpr char const * CONFIG_WEBSITE_URL = "${CONFIG_WEBSITE_URL}"; #endif diff --git a/src/database/DatabaseSchemaHelper.cpp b/src/database/DatabaseSchemaHelper.cpp index bdf8c35c..a1390926 100644 --- a/src/database/DatabaseSchemaHelper.cpp +++ b/src/database/DatabaseSchemaHelper.cpp @@ -36,7 +36,7 @@ #include "database/DbTransaction.h" #include "database/ObjectStoreTyped.h" -int constexpr DatabaseSchemaHelper::latestVersion = 11; +int constexpr DatabaseSchemaHelper::latestVersion = 13; // Default namespace hides functions from everything outside this file. namespace { @@ -556,7 +556,7 @@ namespace { "description" " " << db.getDbNativeTypeName() << ", " "notes" " " << db.getDbNativeTypeName() << ", " "pre_boil_size_l" " " << db.getDbNativeTypeName() << ", " - "boil_Time_mins" " " << db.getDbNativeTypeName() << ", " + "boil_time_mins" " " << db.getDbNativeTypeName() << ", " "temp_recipe_id" " " << db.getDbNativeTypeName() << ");"; @@ -632,6 +632,58 @@ namespace { {QString("UPDATE misc SET inventory_id = CAST(inventory_id AS int) WHERE inventory_id IS NOT null")}, {QString("UPDATE yeast SET inventory_id = CAST(inventory_id AS int) WHERE inventory_id IS NOT null")}, // + // For historical reasons, some people have a lot of indexes in their database, others do not. Where they + // relate to columns we are getting rid of we need to drop them if present. Fortunately, the syntax for doing + // this is the same for SQLite and PostgreSQL. + // + // We actually go a bit further and drop some indexes on columns we aren't getting rid of. This is because the + // indexes serve little purpose. We load all the data from the DB into memory at start-up and then access rows + // by primary key to make amendments etc. + // + // NOTE: we cannot drop indexes beginning "sqlite_autoindex_" as we would get an error "index associated with + // UNIQUE or PRIMARY KEY constraint cannot be dropped". + // + {QString("DROP INDEX IF EXISTS bt_hop_hop_id ")}, + {QString("DROP INDEX IF EXISTS hop_children_parent_id ")}, + {QString("DROP INDEX IF EXISTS hop_in_recipe_recipe_id ")}, + {QString("DROP INDEX IF EXISTS hop_in_recipe_hop_id ")}, + {QString("DROP INDEX IF EXISTS instruction_in_recipe_recipe_id ")}, + {QString("DROP INDEX IF EXISTS instruction_in_recipe_instruction_id")}, + {QString("DROP INDEX IF EXISTS equipment_children_parent_id ")}, + {QString("DROP INDEX IF EXISTS misc_inventory_id ")}, + {QString("DROP INDEX IF EXISTS misc_children_parent_id ")}, + {QString("DROP INDEX IF EXISTS misc_in_recipe_recipe_id ")}, + {QString("DROP INDEX IF EXISTS misc_in_recipe_misc_id ")}, + {QString("DROP INDEX IF EXISTS brewnote_recipe_id ")}, + {QString("DROP INDEX IF EXISTS bt_equipment_equipment_id ")}, + {QString("DROP INDEX IF EXISTS bt_fermentable_fermentable_id ")}, + {QString("DROP INDEX IF EXISTS bt_misc_misc_id ")}, + {QString("DROP INDEX IF EXISTS bt_style_style_id ")}, + {QString("DROP INDEX IF EXISTS bt_water_water_id ")}, + {QString("DROP INDEX IF EXISTS bt_yeast_yeast_id ")}, + {QString("DROP INDEX IF EXISTS fermentable_inventory_id ")}, + {QString("DROP INDEX IF EXISTS fermentable_children_parent_id ")}, + {QString("DROP INDEX IF EXISTS fermentable_in_recipe_recipe_id ")}, + {QString("DROP INDEX IF EXISTS fermentable_in_recipe_fermentable_id")}, + {QString("DROP INDEX IF EXISTS hop_inventory_id ")}, + {QString("DROP INDEX IF EXISTS mashstep_mash_id ")}, + {QString("DROP INDEX IF EXISTS recipe_equipment_id ")}, + {QString("DROP INDEX IF EXISTS recipe_mash_id ")}, + {QString("DROP INDEX IF EXISTS recipe_style_id ")}, + {QString("DROP INDEX IF EXISTS recipe_ancestor_id ")}, + {QString("DROP INDEX IF EXISTS recipe_children_parent_id ")}, + {QString("DROP INDEX IF EXISTS salt_misc_id ")}, + {QString("DROP INDEX IF EXISTS salt_in_recipe_salt_id ")}, + {QString("DROP INDEX IF EXISTS salt_in_recipe_recipe_id ")}, + {QString("DROP INDEX IF EXISTS style_children_parent_id ")}, + {QString("DROP INDEX IF EXISTS water_children_parent_id ")}, + {QString("DROP INDEX IF EXISTS water_in_recipe_recipe_id ")}, + {QString("DROP INDEX IF EXISTS water_in_recipe_water_id ")}, + {QString("DROP INDEX IF EXISTS yeast_inventory_id ")}, + {QString("DROP INDEX IF EXISTS yeast_children_parent_id ")}, + {QString("DROP INDEX IF EXISTS yeast_in_recipe_recipe_id ")}, + {QString("DROP INDEX IF EXISTS yeast_in_recipe_yeast_id ")}, + // // Salt::Type is currently stored as a raw number. We convert it to a string to bring it into line with other // enums. Current values are: // 0 == NONE @@ -732,7 +784,15 @@ namespace { // // Yeast: Extended and additional fields for BeerJSON // - // We only need to update the old Yeast type, form and flocculation mappings. The new ones should "just work". + // We only need to update the old Yeast type, form and flocculation mappings. The new ones ("very low", + // "medium low", "medium high") should "just work". + // + // For "parent" yeast records, attenuation is replaced by attenuation_min_pct and attenuation_max_pct. (For + // "child" ones it moves to attenuation_pct on yeast_in_recipe, which is done below.) Although it's unlikely + // to be strictly correct, we set attenuation_min_pct and attenuation_max_pct both to hold the same value as + // the old attenuation column, on the grounds that this is better than nothing, except in a case where the old + // attenuation column holds 0. + // {QString(" UPDATE yeast SET ytype = 'ale' WHERE ytype = 'Ale' ")}, {QString(" UPDATE yeast SET ytype = 'lager' WHERE ytype = 'Lager' ")}, {QString(" UPDATE yeast SET ytype = 'other' WHERE ytype = 'Wheat' ")}, // NB: Wheat becomes Other @@ -756,6 +816,8 @@ namespace { {QString("ALTER TABLE yeast ADD COLUMN killer_producing_k28_toxin %1").arg(db.getDbNativeTypeName())}, {QString("ALTER TABLE yeast ADD COLUMN killer_producing_klus_toxin %1").arg(db.getDbNativeTypeName())}, {QString("ALTER TABLE yeast ADD COLUMN killer_neutral %1").arg(db.getDbNativeTypeName())}, + {QString(" UPDATE yeast SET attenuation_min_pct = attenuation WHERE attenuation != 0")}, + {QString(" UPDATE yeast SET attenuation_max_pct = attenuation WHERE attenuation != 0")}, // // Style: Extended and additional fields for BeerJSON. Plus fix inconsistent column name // @@ -887,7 +949,7 @@ namespace { "description , " "notes , " "pre_boil_size_l, " - "boil_Time_mins , " + "boil_time_mins , " "temp_recipe_id " ") SELECT " "'Boil for ' || name, " @@ -1662,7 +1724,8 @@ namespace { // {QString("ALTER TABLE yeast DROP COLUMN add_to_secondary")}, // - // Attenuation percent moves from being a Yeast property to a RecipeAdditionYeast one + // For "child" yeast records, attenuation percent moves from being a Yeast property to a RecipeAdditionYeast + // one. // {QString("UPDATE yeast_in_recipe " "SET attenuation_pct = y.attenuation " @@ -1797,15 +1860,58 @@ namespace { {QString("ALTER TABLE fermentable_in_inventory ADD COLUMN unit %1").arg(db.getDbNativeTypeName())}, {QString("ALTER TABLE misc_in_inventory ADD COLUMN unit %1").arg(db.getDbNativeTypeName())}, {QString("ALTER TABLE yeast_in_inventory ADD COLUMN unit %1").arg(db.getDbNativeTypeName())}, + // + // For historical reasons, some users have some duff entries in the xxx_in_inventory tables. It's worth + // cleaning them up here. Note that empirical testing shows the "WHERE inventory_id IS NOT NULL" bit is needed + // here. + // + {QString("DELETE FROM hop_in_inventory WHERE id NOT IN (SELECT inventory_id FROM hop WHERE inventory_id IS NOT NULL)")}, + {QString("DELETE FROM fermentable_in_inventory WHERE id NOT IN (SELECT inventory_id FROM fermentable WHERE inventory_id IS NOT NULL)")}, + {QString("DELETE FROM misc_in_inventory WHERE id NOT IN (SELECT inventory_id FROM misc WHERE inventory_id IS NOT NULL)")}, + {QString("DELETE FROM yeast_in_inventory WHERE id NOT IN (SELECT inventory_id FROM yeast WHERE inventory_id IS NOT NULL)")}, + // // At this point, all hop and fermentable amounts will be weights, because prior versions of the DB did not // support measuring them by volume. + // + // There is a bit of a gotcha waiting for us here. In the old schema, where each hop refers to a + // hop_in_inventory row, there can and will be multiple hops sharing the same inventory row. In the new + // schema, where each hop_in_inventory row refers to a hop, different hops cannot share an inventory. (This is + // all by design, as we ultimately want to be able to manage multiple inventory entries per hop. That way you + // can separately track the Fuggles that you bought last year (and need to use up) from the ones you bought + // last week (so you won't run out when you use up last year's). + // + // Although we have, by this point, deleted the "child" entries in hop that signified "use of hop in recipe" + // (replacing them with entries in hop_in_recipe). There can still be multiple entries for "the same" hop in + // the hop table, all sharing the same hop_in_inventory row. Specifically this is because of hops marked + // deleted or not displayable. The simple answer would be to ignore deleted and non-displayable hops. But + // this creates another problem because there can be inventory entries for hops that are only deleted. And, + // whilst we might have a natural instinct to just delete the hop_in_inventory rows that have no corresponding + // displayable not-deleted hop, that feels wrong as we would be throwing data away that might one day be + // needed (eg if someone wants to undelete a hop that they deleted in error). + // + // So, we do something "clever". For the "update hop_in_inventory rows to point at hop rows" query, we feed it + // the list of hops in a sort of reverse order, specifically such that the deleted and non-displayable ones + // occur before the others. Thus, where there are multiple hop rows pointing to the same hop_in_inventory row, + // the latter will get updated multiple times and, if there is a corresponding displayable non-deleted hop, + // that will be the last one that the hop_in_inventory row is updated to point to, so it will "win" over the + // others. Despite sounding a bit complicated, it's makes the SQL simpler than a lot of other approaches! + // + // It _should_ be the case that there is at most one displayable non-deleted hop per row of hop_in_inventory. + // However, just in case this is ever not true, we go a bit further and say that, after ordering hops so that + // all the deleted and non-displayable ones are parsed first, the remaining ones are put in reverse ID order, + // which means the oldest entries (with the smallest IDs) get processed last and are most likely to "win" in + // conflicts. At the moment at least, it seems the most reasonable tie-breaker. + // + // Of course, all the above applies, mutatis mutandis, to fermentables, miscs, and yeasts as well. + // {QString("UPDATE hop_in_inventory " "SET hop_id = h.id, " "unit = 'kilograms' " "FROM (" "SELECT id, " "inventory_id " - "FROM hop" + "FROM hop " + "ORDER BY NOT deleted, display, id DESC " ") AS h " "WHERE hop_in_inventory.id = h.inventory_id")}, {QString("UPDATE fermentable_in_inventory " @@ -1814,7 +1920,8 @@ namespace { "FROM (" "SELECT id, " "inventory_id " - "FROM fermentable" + "FROM fermentable " + "ORDER BY NOT deleted, display, id DESC " ") AS f " "WHERE fermentable_in_inventory.id = f.inventory_id ")}, // For misc, we need to support both weights and volumes. @@ -1825,7 +1932,8 @@ namespace { "SELECT id, " "inventory_id, " "CASE WHEN amount_is_weight THEN 'kilograms' ELSE 'liters' END AS unit " - "FROM misc" + "FROM misc " + "ORDER BY NOT deleted, display, id DESC " ") AS m " "WHERE misc_in_inventory.id = m.inventory_id")}, // HOWEVER, for inventory purposes, yeast is actually stored as "number of packets" (aka quanta in various old @@ -1836,7 +1944,8 @@ namespace { "FROM (" "SELECT id, " "inventory_id " - "FROM yeast" + "FROM yeast " + "ORDER BY NOT deleted, display, id DESC " ") AS y " "WHERE yeast_in_inventory.id = y.inventory_id")}, // Now we transferred info across, we don't need the inventory_id column on hop or fermentable @@ -1957,13 +2066,82 @@ namespace { // of having the better column name IMHO. // {QString("ALTER TABLE settings DROP COLUMN repopulatechildrenonnextstart")}, - {QString("ALTER TABLE settings ADD COLUMN default_content_version").arg(db.getDbNativeTypeName())}, + {QString("ALTER TABLE settings ADD COLUMN default_content_version %1").arg(db.getDbNativeTypeName())}, {QString("UPDATE settings SET default_content_version = 0")}, }; return executeSqlQueries(q, migrationQueries); } + /** + * \brief This is not actually a schema change, but rather data fixes that were missed from migrate_to_11 - mostly + * things where we should have been less case-sensitive. + */ + bool migrate_to_12([[maybe_unused]] Database & db, BtSqlQuery q) { + QVector const migrationQueries{ + {QString( "UPDATE hop SET htype = 'aroma/bittering' WHERE lower(htype) = 'both'" )}, + {QString(" UPDATE fermentable SET ftype = 'dry extract' WHERE lower(ftype) = 'dry extract'")}, + {QString(" UPDATE fermentable SET ftype = 'dry extract' WHERE lower(ftype) = 'dryextract'" )}, + {QString(" UPDATE fermentable SET ftype = 'other' WHERE lower(ftype) = 'adjunct'" )}, + {QString(" UPDATE misc SET mtype = 'water agent' WHERE lower(mtype) = 'water agent'")}, + {QString(" UPDATE misc SET mtype = 'water agent' WHERE lower(mtype) = 'wateragent'" )}, + {QString(" UPDATE yeast SET ytype = 'other' WHERE lower(ytype) = 'wheat' ")}, + {QString(" UPDATE yeast SET flocculation = 'very high' WHERE lower(flocculation) = 'very high'")}, + {QString(" UPDATE yeast SET flocculation = 'very high' WHERE lower(flocculation) = 'veryhigh'" )}, + {QString(" UPDATE style SET stype = 'beer' WHERE lower(stype) = 'lager'")}, + {QString(" UPDATE style SET stype = 'beer' WHERE lower(stype) = 'ale' ")}, + {QString(" UPDATE style SET stype = 'beer' WHERE lower(stype) = 'wheat'")}, + {QString(" UPDATE style SET stype = 'other' WHERE lower(stype) = 'mixed'")}, + {QString(" UPDATE mash_step SET mstype = 'sparge' WHERE lower(mstype) = 'flysparge' ")}, + {QString(" UPDATE mash_step SET mstype = 'sparge' WHERE lower(mstype) = 'fly sparge' ")}, + {QString(" UPDATE mash_step SET mstype = 'drain mash tun' WHERE lower(mstype) = 'batchsparge'")}, + {QString(" UPDATE mash_step SET mstype = 'drain mash tun' WHERE lower(mstype) = 'batch sparge'")}, + {QString(" UPDATE recipe SET type = 'partial mash' WHERE lower(type) = 'partial mash'")}, + {QString(" UPDATE recipe SET type = 'partial mash' WHERE lower(type) = 'partialmash'")}, + {QString(" UPDATE recipe SET type = 'all grain' WHERE lower(type) = 'all grain' ")}, + {QString(" UPDATE recipe SET type = 'all grain' WHERE lower(type) = 'allgrain' ")}, + }; + return executeSqlQueries(q, migrationQueries); + } + + /** + * \brief Small schema change to support measuring diameter of boil kettle for new IBU calculations. Plus, some + * tidy-ups to Salt and Boil / BoilStep which we didn't get to before. + */ + bool migrate_to_13([[maybe_unused]] Database & db, BtSqlQuery q) { + QVector const migrationQueries{ + {QString("ALTER TABLE equipment ADD COLUMN kettleInternalDiameter_cm %1").arg(db.getDbNativeTypeName())}, + {QString("ALTER TABLE equipment ADD COLUMN kettleOpeningDiameter_cm %1").arg(db.getDbNativeTypeName())}, + // The is_acid column is unnecessary as we know whether it's an acide from the stype column + {QString("ALTER TABLE salt DROP COLUMN is_acid")}, + // The addTo column is not needed as it is now replaced by salt_in_recipe.when_to_add and the contents brought + // over in migrate_to_11 above. Same goes for amount_is_weight, which is replaced by salt_in_recipe.unit and + // salt_in_inventory.unit. + {QString("ALTER TABLE salt DROP COLUMN addTo")}, + {QString("ALTER TABLE salt DROP COLUMN amount_is_weight")}, + // + // The boil.boil_time_mins column is, in reality the length of the boil proper, so it should have gone straight + // to the relevant boil_step. We correct that here and do away with the column on boil. + // + // Per the comment in model/Boil.h on minimumBoilTemperature_c, 81.0°C is the temperature above which we assume + // a step is a boil. + // + {QString("UPDATE boil_step " + "SET step_time_mins = b.boil_time_mins " + "FROM (" + "SELECT id, " + "boil_time_mins " + "FROM boil" + ") AS b " + "WHERE boil_step.step_time_mins IS NULL " + "AND boil_step.start_temp_c >= 81.0 " + "AND boil_step.end_temp_c >= 81.0 " + "AND boil_step.boil_id = b.id ")}, + {QString("ALTER TABLE boil DROP COLUMN boil_time_mins")}, + }; + return executeSqlQueries(q, migrationQueries); + } + /*! * \brief Migrate from version \c oldVersion to \c oldVersion+1 */ @@ -2004,6 +2182,12 @@ namespace { case 10: ret &= migrate_to_11(database, sqlQuery); break; + case 11: + ret &= migrate_to_12(database, sqlQuery); + break; + case 12: + ret &= migrate_to_13(database, sqlQuery); + break; default: qCritical() << QString("Unknown version %1").arg(oldVersion); return false; diff --git a/src/database/ObjectStore.cpp b/src/database/ObjectStore.cpp index 574055a7..5cbc2871 100644 --- a/src/database/ObjectStore.cpp +++ b/src/database/ObjectStore.cpp @@ -1118,6 +1118,7 @@ class ObjectStore::impl { qCritical() << Q_FUNC_INFO << "Unable to find rule for storing property" << object.metaObject()->className() << "::" << propertyName << "in either" << this->primaryTable.tableName << "or any associated table"; + qCritical().noquote() << Q_FUNC_INFO << Logging::getStackTrace(); Q_ASSERT(false); } diff --git a/src/database/ObjectStoreTyped.cpp b/src/database/ObjectStoreTyped.cpp index f68d819b..6a44f538 100644 --- a/src/database/ObjectStoreTyped.cpp +++ b/src/database/ObjectStoreTyped.cpp @@ -106,6 +106,8 @@ namespace { {ObjectStore::FieldType::Double, "mash_tun_specific_heat_calgc" , PropertyNames::Equipment::mashTunSpecificHeat_calGC }, {ObjectStore::FieldType::Double, "mash_tun_volume_l" , PropertyNames::Equipment::mashTunVolume_l }, {ObjectStore::FieldType::Double, "mash_tun_weight_kg" , PropertyNames::Equipment::mashTunWeight_kg }, + {ObjectStore::FieldType::Double, "kettleInternalDiameter_cm" , PropertyNames::Equipment::kettleInternalDiameter_cm }, + {ObjectStore::FieldType::Double, "kettleOpeningDiameter_cm" , PropertyNames::Equipment::kettleOpeningDiameter_cm }, // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ {ObjectStore::FieldType::String, "hlt_type" , PropertyNames::Equipment::hltType }, {ObjectStore::FieldType::String, "mash_tun_type" , PropertyNames::Equipment::mashTunType }, @@ -358,7 +360,6 @@ namespace { {ObjectStore::FieldType::String, "description" , PropertyNames::Boil::description }, {ObjectStore::FieldType::String, "notes" , PropertyNames::Boil::notes }, {ObjectStore::FieldType::Double, "pre_boil_size_l", PropertyNames::Boil::preBoilSize_l }, - {ObjectStore::FieldType::Double, "boil_Time_mins" , PropertyNames::Boil::boilTime_mins }, } }; // Boils don't have children, and the link with their BoilSteps is stored in the BoilStep (as between Recipe and BrewNotes) @@ -489,7 +490,7 @@ namespace { {ObjectStore::FieldType::Bool , "deleted" , PropertyNames::NamedEntity::deleted }, {ObjectStore::FieldType::Bool , "display" , PropertyNames::NamedEntity::display }, {ObjectStore::FieldType::String, "folder" , PropertyNames::FolderBase::folder }, - {ObjectStore::FieldType::Bool , "is_acid" , PropertyNames::Salt::isAcid }, +/// {ObjectStore::FieldType::Bool , "is_acid" , PropertyNames::Salt::isAcid }, {ObjectStore::FieldType::Double, "percent_acid" , PropertyNames::Salt::percentAcid }, {ObjectStore::FieldType::Enum , "stype" , PropertyNames::Salt::type , &Salt::typeStringMapping}, } @@ -644,7 +645,7 @@ namespace { {ObjectStore::FieldType::String, "name" , PropertyNames::NamedEntity::name }, {ObjectStore::FieldType::Bool , "deleted" , PropertyNames::NamedEntity::deleted }, {ObjectStore::FieldType::Bool , "display" , PropertyNames::NamedEntity::display }, - {ObjectStore::FieldType::String, "folder" , PropertyNames::FolderBase::folder }, + {ObjectStore::FieldType::String, "folder" , PropertyNames::FolderBase::folder }, {ObjectStore::FieldType::Double, "age" , PropertyNames::Recipe::age_days }, {ObjectStore::FieldType::Double, "age_temp" , PropertyNames::Recipe::ageTemp_c }, {ObjectStore::FieldType::String, "assistant_brewer" , PropertyNames::Recipe::asstBrewer }, @@ -678,18 +679,32 @@ namespace { } }; template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES { - // .:TODO:. BrewNote table stores its recipe ID, so there isn't a brewnote junction table + // + // .:TODO:. This is the wrong way to model Instructions. We should treat them more like BrewNotes. In both case + // the objects cannot meaningfully exist without a corresponding Recipe. + // + // Having the "instruction" table (see PRIMARY_TABLE) and this instruction_in_recipe + // junction table implies the possibility for an existence of Instruction objects independently of Recipe + // ones, which is incorrect. + // // There is a lot of boiler-plate here, and we could have gone for a much more compact representation of junction // tables, but this keeps the definition format relatively closely aligned with that of primary tables. + // + // NOTE that the tables for ingredient additions (such as fermentable_in_recipe for RecipeAdditionFermentable) + // could, in principle, be treated as a junction tables, because they really are modelling a many-to-many + // relationship between, eg, Recipe and Fermentable. However, because they are also storing other properties + // (to do with the timing and amount of the addition), and because the Recipe class is already quite large, + // we choose to model the ingredient addition tables as freestanding entities. + // { "instruction_in_recipe", { - {ObjectStore::FieldType::Int, "id" }, + {ObjectStore::FieldType::Int, "id" }, {ObjectStore::FieldType::Int, "recipe_id", PropertyNames::NamedEntity::key, &PRIMARY_TABLE }, {ObjectStore::FieldType::Int, "instruction_id", PropertyNames::Recipe::instructionIds, &PRIMARY_TABLE}, - {ObjectStore::FieldType::Int, "instruction_number" }, + {ObjectStore::FieldType::Int, "instruction_number" }, } - }, + } }; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/editors/BoilEditor.cpp b/src/editors/BoilEditor.cpp index 2e62ec03..c9cd0a48 100644 --- a/src/editors/BoilEditor.cpp +++ b/src/editors/BoilEditor.cpp @@ -22,123 +22,44 @@ #include "model/Boil.h" #include "model/Recipe.h" -BoilEditor::BoilEditor(QWidget* parent) : QDialog(parent), m_boilObs{nullptr} { - setupUi(this); +BoilEditor::BoilEditor(QWidget* parent) : + QDialog(parent), + EditorWithRecipeBase() { + this->setupUi(this); + this->postSetupUiInit( + { + EDITOR_FIELD(Boil, label_name , lineEdit_name , PropertyNames::NamedEntity::name ), + EDITOR_FIELD(Boil, label_description, textEdit_description, PropertyNames::Boil::description ), + EDITOR_FIELD(Boil, label_preBoilSize, lineEdit_preBoilSize, PropertyNames::Boil::preBoilSize_l, 2), + EDITOR_FIELD(Boil, label_notes , textEdit_notes , PropertyNames::Boil::notes ) + } + ); // NB: label_description / textEdit_description don't need initialisation here as neither is a smart field // NB: label_notes / textEdit_notes don't need initialisation here as neither is a smart field - SMART_FIELD_INIT(BoilEditor, label_name , lineEdit_name , Boil, PropertyNames::NamedEntity::name ); - SMART_FIELD_INIT(BoilEditor, label_preBoilSize, lineEdit_preBoilSize, Boil, PropertyNames::Boil::preBoilSize_l, 1); - SMART_FIELD_INIT(BoilEditor, label_boilTime , lineEdit_boilTime , Boil, PropertyNames::Boil::boilTime_mins, 0); +/// SMART_FIELD_INIT(BoilEditor, label_name , lineEdit_name , Boil, PropertyNames::NamedEntity::name ); +/// SMART_FIELD_INIT(BoilEditor, label_preBoilSize, lineEdit_preBoilSize, Boil, PropertyNames::Boil::preBoilSize_l, 2); - connect(this, &QDialog::accepted, this, &BoilEditor::saveAndClose); - connect(this, &QDialog::rejected, this, &BoilEditor::closeEditor ); - return; -} - -BoilEditor::~BoilEditor() = default; - -void BoilEditor::showEditor() { - showChanges(); - setVisible(true); - return; -} - -void BoilEditor::closeEditor() { - setVisible(false); - return; -} - -void BoilEditor::saveAndClose() { - bool isNew = false; - - if (!this->m_boilObs) { - this->m_boilObs = std::make_shared(lineEdit_name->text()); - isNew = true; - } - qDebug() << Q_FUNC_INFO << "Saving" << (isNew ? "new" : "existing") << "boil (#" << this->m_boilObs->key() << ")"; - - this->m_boilObs->setName (this->lineEdit_name ->text ()); - this->m_boilObs->setDescription (this->textEdit_description->toPlainText ()); - this->m_boilObs->setPreBoilSize_l(this->lineEdit_preBoilSize->getOptCanonicalQty ()); - this->m_boilObs->setBoilTime_mins(this->lineEdit_boilTime ->getNonOptCanonicalQty()); - this->m_boilObs->setNotes (this->textEdit_notes ->toPlainText ()); - - if (isNew) { - ObjectStoreWrapper::insert(*this->m_boilObs); - this->m_rec->setBoil(this->m_boilObs); - } +/// connect(this, &QDialog::accepted, this, &BoilEditor::saveAndClose); +/// connect(this, &QDialog::rejected, this, &BoilEditor::closeEditor ); +/// this->connectSignalsAndSlots(); return; } -void BoilEditor::setBoil(std::shared_ptr boil) { - if (this->m_boilObs) { - disconnect(this->m_boilObs.get(), nullptr, this, nullptr); - } +BoilEditor::~BoilEditor() = default; - this->m_boilObs = boil; - if (this->m_boilObs) { - connect(this->m_boilObs.get(), &NamedEntity::changed, this, &BoilEditor::changed); - showChanges(); - } +void BoilEditor::writeFieldsToEditItem() { return; } -void BoilEditor::setRecipe(Recipe * recipe) { - if (!recipe) { - return; - } - - this->m_rec = recipe; - +void BoilEditor::writeLateFieldsToEditItem() { return; } -void BoilEditor::changed(QMetaProperty prop, QVariant /*val*/) { - if (!this->m_boilObs) { - return; - } - - - if (sender() == this->m_boilObs.get()) { - this->showChanges(&prop); - } - - if (sender() == this->m_rec) { - this->showChanges(); - } +void BoilEditor::readFieldsFromEditItem([[maybe_unused]] std::optional propName) { return; } -void BoilEditor::showChanges(QMetaProperty* prop) { - if (!this->m_boilObs) { - this->clear(); - return; - } - - QString propName; - bool updateAll = false; - if (prop == nullptr) { - updateAll = true; - } else { - propName = prop->name(); - } - qDebug() << Q_FUNC_INFO << "Updating" << (updateAll ? "all" : "property") << propName; - - if (updateAll || propName == PropertyNames::NamedEntity::name ) {this->lineEdit_name ->setText (this->m_boilObs->name ()); if (!updateAll) { return; } } - if (updateAll || propName == PropertyNames::Boil::description ) {this->textEdit_description->setPlainText(this->m_boilObs->description ()); if (!updateAll) { return; } } - if (updateAll || propName == PropertyNames::Boil::preBoilSize_l) {this->lineEdit_preBoilSize->setQuantity (this->m_boilObs->preBoilSize_l()); if (!updateAll) { return; } } - if (updateAll || propName == PropertyNames::Boil::boilTime_mins) {this->lineEdit_boilTime ->setQuantity (this->m_boilObs->boilTime_mins()); if (!updateAll) { return; } } - if (updateAll || propName == PropertyNames::Boil::notes ) {this->textEdit_notes ->setPlainText(this->m_boilObs->notes ()); if (!updateAll) { return; } } - return; -} - -void BoilEditor::clear() { - this->lineEdit_name ->setText (""); - this->textEdit_description->setText (""); - this->lineEdit_preBoilSize->setText (""); - this->lineEdit_boilTime ->setText (""); - this->textEdit_notes ->setPlainText(""); - return; -} +// Insert the boilerplate stuff that we cannot do in EditorWithRecipeBase +EDITOR_WITH_RECIPE_COMMON_CODE(BoilEditor) diff --git a/src/editors/BoilEditor.h b/src/editors/BoilEditor.h index 06f3f3e9..d18531b9 100644 --- a/src/editors/BoilEditor.h +++ b/src/editors/BoilEditor.h @@ -20,11 +20,11 @@ #include #include #include + #include "ui_boilEditor.h" -// Forward declarations. -class Recipe; -class Boil; +#include "editors/EditorWithRecipeBase.h" +#include "model/Boil.h" /*! * \class BoilEditor @@ -33,26 +33,10 @@ class Boil; * * See also \c NamedBoilEditor */ -class BoilEditor : public QDialog, public Ui::boilEditor { +class BoilEditor : public QDialog, public Ui::boilEditor, public EditorWithRecipeBase { Q_OBJECT -public: - BoilEditor(QWidget * parent = nullptr); - ~BoilEditor(); - -public slots: - void showEditor(); - void closeEditor(); - void saveAndClose(); - //! Set the boil we wish to view/edit. - void setBoil(std::shared_ptr boil); - void setRecipe(Recipe* r); - void changed(QMetaProperty,QVariant); -private: - void showChanges(QMetaProperty* prop = nullptr); - void clear(); - Recipe* m_rec; - std::shared_ptr m_boilObs; + EDITOR_WITH_RECIPE_COMMON_DECL(Boil) }; #endif diff --git a/src/editors/BoilStepEditor.cpp b/src/editors/BoilStepEditor.cpp index bea96fe1..418f4ab6 100644 --- a/src/editors/BoilStepEditor.cpp +++ b/src/editors/BoilStepEditor.cpp @@ -60,16 +60,16 @@ void BoilStepEditor::readFieldsFromEditItem(std::optional propName) { } void BoilStepEditor::writeFieldsToEditItem() { - this->m_editItem->setName (this->lineEdit_name->text()); - this->m_editItem->setDescription (this->textEdit_description ->toPlainText ()); - this->m_editItem->setStartTemp_c (this->lineEdit_startTemp ->getNonOptCanonicalQty()); - this->m_editItem->setStepTime_mins (this->lineEdit_stepTime ->getNonOptCanonicalQty()); - this->m_editItem->setRampTime_mins (this->lineEdit_rampTime ->getOptCanonicalQty ()); - this->m_editItem->setEndTemp_c (this->lineEdit_endTemp ->getOptCanonicalQty ()); - this->m_editItem->setStartAcidity_pH (this->lineEdit_startAcidity->getOptCanonicalQty ()); - this->m_editItem->setEndAcidity_pH (this->lineEdit_endAcidity ->getOptCanonicalQty ()); - this->m_editItem->setStartGravity_sg (this->lineEdit_startGravity->getOptCanonicalQty ()); - this->m_editItem->setEndGravity_sg (this->lineEdit_endGravity ->getOptCanonicalQty ()); + this->m_editItem->setName (this->lineEdit_name ->text ()); + this->m_editItem->setDescription (this->textEdit_description ->toPlainText ()); + this->m_editItem->setStartTemp_c (this->lineEdit_startTemp ->getOptCanonicalQty()); + this->m_editItem->setStepTime_mins (this->lineEdit_stepTime ->getOptCanonicalQty()); + this->m_editItem->setRampTime_mins (this->lineEdit_rampTime ->getOptCanonicalQty()); + this->m_editItem->setEndTemp_c (this->lineEdit_endTemp ->getOptCanonicalQty()); + this->m_editItem->setStartAcidity_pH(this->lineEdit_startAcidity->getOptCanonicalQty()); + this->m_editItem->setEndAcidity_pH (this->lineEdit_endAcidity ->getOptCanonicalQty()); + this->m_editItem->setStartGravity_sg(this->lineEdit_startGravity->getOptCanonicalQty()); + this->m_editItem->setEndGravity_sg (this->lineEdit_endGravity ->getOptCanonicalQty()); this->m_editItem->setChillingType(this->comboBox_boilStepChillingType->getOptValue()); return; @@ -81,4 +81,4 @@ void BoilStepEditor::writeLateFieldsToEditItem() { } // Insert the boiler-plate stuff that we cannot do in EditorBase -EDITOR_COMMON_SLOT_DEFINITIONS(BoilStepEditor) +EDITOR_COMMON_CODE(BoilStepEditor) diff --git a/src/editors/EditorBase.h b/src/editors/EditorBase.h index f5a4b8ec..80ca37dd 100644 --- a/src/editors/EditorBase.h +++ b/src/editors/EditorBase.h @@ -18,6 +18,8 @@ #pragma once #include +#include +#include #include #include @@ -27,6 +29,109 @@ #include "model/NamedEntity.h" #include "utils/CuriouslyRecurringTemplateBase.h" +/** + * \brief Field info for a field of a subclass of \c EditorBase. + * + * Note that we can't put this inside the \c EditorBase class declaration as we also want to use it there, and + * we'd get errors about "invalid use of incomplete type ‘class EditorBase’". + */ +struct EditorBaseField { + /** + * \brief Most fields are written together. However, some are marked 'Late' because they need to be written after + * the object is created. + */ + enum class WhenToWrite { + Normal, + Late + }; + + char const * labelName; + std::variant label; + // We need to know what type the field is, partly because QLineEdit and QTextEdit don't have a useful common base + // class, and partly because we want to access member functions of SmartLineEdit that don't exist on QLineEdit or + // QTextEdit. + std::variant editField; + BtStringConst const & property; + // Both the next two fields have defaults, but precision is the one that more often needs something other than + // default to be specified, so we put it first. + std::optional precision = std::nullopt; + EditorBaseField::WhenToWrite whenToWrite = WhenToWrite::Normal; + + //! Constructor for when we don't have a SmartLineEdit + template + EditorBaseField([[maybe_unused]] char const * const editorClass, + char const * const labelName, + [[maybe_unused]] char const * const labelFqName, + LabelType * label, + [[maybe_unused]] char const * const editFieldName, + [[maybe_unused]] char const * const editFieldFqName, + EditFieldType * editField, + BtStringConst const & property, + [[maybe_unused]] TypeInfo const & typeInfo, + std::optional precision = std::nullopt, + EditorBaseField::WhenToWrite whenToWrite = WhenToWrite::Normal) : + labelName {labelName }, + label {label }, + editField {editField }, + property {property }, + precision {precision }, + whenToWrite{whenToWrite} { + return; + } + + //! Constructor for when we have a SmartLineEdit + template + EditorBaseField(char const * const editorClass, + char const * const labelName, + char const * const labelFqName, + LabelType * label, + char const * const editFieldName, + char const * const editFieldFqName, + SmartLineEdit * editField, + BtStringConst const & property, + TypeInfo const & typeInfo, + std::optional precision = std::nullopt, + EditorBaseField::WhenToWrite whenToWrite = WhenToWrite::Normal) : + labelName {labelName }, + label {label }, + editField {editField }, + property {property }, + precision {precision }, + whenToWrite{whenToWrite} { + SmartAmounts::Init(editorClass, + labelName, + labelFqName, + *label, + editFieldName, + editFieldFqName, + *editField, + typeInfo, + precision); + return; + } + +}; + +/** + * \brief This macro is similar to SMART_FIELD_INIT, but allows us to pass the EditorBaseField constructor. + * + * We assume that, where Foo is some subclass of NamedEntity, then the editor class for Foo is always called + * FooEditor. + */ +#define EDITOR_FIELD(modelClass, label, editField, property, ...) \ + EditorBaseField{\ + #modelClass "Editor", \ + #label, \ + #modelClass "Editor->" #label, \ + label, \ + #editField, \ + #modelClass "Editor->" #editField, \ + editField, \ + property, \ + modelClass ::typeLookup.getType(property) \ + __VA_OPT__(, __VA_ARGS__) \ + } + /** * \class EditorBase * @@ -53,9 +158,9 @@ * - \c void \c clickedNew() public slot, which should call \c EditorBase::newEditItem * * The code for the definition of these slot functions (which is "the same" for all editors) can be inserted in - * the implementation file using the EDITOR_COMMON_SLOT_DEFINITIONS macro. Eg, in HopEditor, we need: + * the implementation file using the EDITOR_COMMON_CODE macro. Eg, in HopEditor, we need: * - * EDITOR_COMMON_SLOT_DEFINITIONS(HopEditor) + * (HopEditor) * * Note that we cannot do the equivalent for the header file declarations because the Qt MOC does not expand * non-Qt macros. @@ -76,12 +181,29 @@ template class EditorPhantom; template class EditorBase : public CuriouslyRecurringTemplateBase { public: + + /** + * \brief Constructor + * + * Note that we cannot initialise this->m_fields here, as the parameters themselves won't get constructed + * until Derived calls setupUi(). + */ EditorBase() : + m_fields{nullptr}, m_editItem{nullptr} { return; } virtual ~EditorBase() = default; + /** + * \brief Derived should call this after calling setupUi + */ + void postSetupUiInit(std::initializer_list fields) { + this->m_fields = std::make_unique>(fields); + this->connectSignalsAndSlots(); + return; + } + /** * \brief Call this at the end of derived class's constructor (in particular, after the call to \c setupUi). * @@ -92,7 +214,7 @@ class EditorBase : public CuriouslyRecurringTemplateBase void connectSignalsAndSlots() { // Standard editor slot connections this->derived().connect(this->derived().pushButton_new , &QAbstractButton::clicked, &this->derived(), &Derived::clickedNew ); - this->derived().connect(this->derived().pushButton_save , &QAbstractButton::clicked, &this->derived(), &Derived::save ); + this->derived().connect(this->derived().pushButton_save , &QAbstractButton::clicked, &this->derived(), &Derived::saveAndClose ); this->derived().connect(this->derived().pushButton_cancel, &QAbstractButton::clicked, &this->derived(), &Derived::clearAndClose); return; } @@ -109,7 +231,7 @@ class EditorBase : public CuriouslyRecurringTemplateBase this->m_editItem = editItem; if (this->m_editItem) { this->derived().connect(this->m_editItem.get(), &NamedEntity::changed, &this->derived(), &Derived::changed); - this->derived().readFieldsFromEditItem(std::nullopt); + this->readFromEditItem(std::nullopt); } return; } @@ -146,15 +268,62 @@ class EditorBase : public CuriouslyRecurringTemplateBase auto ne = std::make_shared(name); this->setFolder(ne, folder); -/// if (!folder.isEmpty()) { -/// ne->setFolder(folder); -/// } this->setEditItem(ne); this->derived().show(); return; } + /** + * \brief If \c fromEditItem is \c true supplied, sets the \c field.editField from the \c field.property of + * \c this->m_editItem -- ie populates/updates the UI input field from the model object. + * If \c fromEditItem is \c false, clears the edit field. + */ + void getProperty(EditorBaseField const & field, bool const fromEditItem = true) { + QVariant value; + if (fromEditItem) { + value = this->m_editItem->property(*field.property); + } else { + value = QString{""}; + } + + // Usually leave this debug log commented out unless trouble-shooting as it generates a lot of logging +// qDebug() << Q_FUNC_INFO << field.labelName << "read from" << field.property << "as" << value; + + if (std::holds_alternative(field.editField)) { + std::get(field.editField)->setPlainText(value.toString()); + } else if (std::holds_alternative(field.editField)) { + std::get(field.editField)->setText(value.toString()); + } else { + auto sle = std::get(field.editField); + if (fromEditItem) { + sle->setFromVariant(value); + } else { + sle->setText(value.toString()); + } + } + return; + } + + /** + * \brief Sets \c field.property on \c this->m_editItem to the \c field.editField value -- ie writes back the UI + * value into the model object. + */ + void setProperty(EditorBaseField const & field) { + QVariant val; + if (std::holds_alternative(field.editField)) { + val = QVariant::fromValue(std::get(field.editField)->toPlainText()); + } else if (std::holds_alternative(field.editField)) { + val = QVariant::fromValue(std::get(field.editField)->text()); + } else { + auto sle = std::get(field.editField); + val = sle->getAsVariant(); + } + + this->m_editItem->setProperty(*field.property, val); + return; + } + /** * \brief Subclass should override this if it needs to validate the form before saving happens. * @@ -167,7 +336,7 @@ class EditorBase : public CuriouslyRecurringTemplateBase /** * \brief Subclass should call this from its \c save slot */ - void doSave() { + void doSaveAndClose() { if (!this->m_editItem) { this->derived().setVisible(false); return; @@ -179,11 +348,11 @@ class EditorBase : public CuriouslyRecurringTemplateBase return; } - this->derived().writeFieldsToEditItem(); + this->writeNormalFields(); if (this->m_editItem->key() < 0) { ObjectStoreWrapper::insert(this->m_editItem); } - this->derived().writeLateFieldsToEditItem(); + this->writeLateFields(); this->derived().setVisible(false); return; @@ -198,20 +367,89 @@ class EditorBase : public CuriouslyRecurringTemplateBase return; } + /** + * \brief Read either one field (if \c propName specified) or all (if it is \c std::nullopt) into the UI from the + * model item. + */ + void readFromEditItem(std::optional propName) { + if (this->m_fields) { + for (auto const & field : *this->m_fields) { + if (!propName || *propName == field.property) { + this->getProperty(field); + if (propName) { + // Break out here if we were only updating one property + break; + } + } + } + } + // TODO: For the moment, we still do this call, but ultimately we'll eliminate it. + this->derived().readFieldsFromEditItem(propName); + return; + } + /** * \brief Subclass should call this from its \c changed slot * * Note that \c QObject::sender has \c protected access specifier, so we can't call it from here, not even * via the derived class pointer. Therefore we have derived class call it and pass us the result. */ - void doChanged(QObject * sender, QMetaProperty prop, [[maybe_unused]] QVariant val) { + virtual void doChanged(QObject * sender, QMetaProperty prop, [[maybe_unused]] QVariant val) { if (this->m_editItem && sender == this->m_editItem.get()) { - this->derived().readFieldsFromEditItem(prop.name()); + this->readFromEditItem(prop.name()); + } + return; + } + + void doClearFields() { + if (this->m_fields) { + for (auto const & field : *this->m_fields) { + this->getProperty(field, false); + } + } + return; + } + + void readAllFields() { + if (this->m_editItem) { + this->readFromEditItem(std::nullopt); + } else { + this->doClearFields(); + } + return; + } + + void writeFields(EditorBaseField::WhenToWrite const normalOrLate) { + if (this->m_fields) { + for (auto const & field : *this->m_fields) { + if (normalOrLate == field.whenToWrite) { + this->setProperty(field); + } + } } return; + + } + + void writeNormalFields() { + this->writeFields(EditorBaseField::WhenToWrite::Normal); + // TODO: For the moment, we still do this call, but ultimately we'll eliminate it. + this->derived().writeFieldsToEditItem(); + return; + } + + void writeLateFields() { + this->writeFields(EditorBaseField::WhenToWrite::Late); + // TODO: For the moment, we still do this call, but ultimately we'll eliminate it. + this->derived().writeLateFieldsToEditItem(); + return; } protected: + /** + * \brief Info about fields in this editor + */ + std::unique_ptr> m_fields; /** * \brief This is the \c NamedEntity subclass object we are creating or editing. We are also "observing" it in the @@ -241,7 +479,7 @@ class EditorBase : public CuriouslyRecurringTemplateBase \ public slots: \ /* Standard editor slots */ \ - void save(); \ + void saveAndClose(); \ void clearAndClose(); \ void changed(QMetaProperty, QVariant); \ void clickedNew(); \ @@ -249,8 +487,8 @@ class EditorBase : public CuriouslyRecurringTemplateBase /** * \brief Derived classes should include this in their implementation file */ -#define EDITOR_COMMON_SLOT_DEFINITIONS(EditorName) \ - void EditorName::save() { this->doSave(); return; } \ +#define EDITOR_COMMON_CODE(EditorName) \ + void EditorName::saveAndClose() { this->doSaveAndClose(); return; } \ void EditorName::clearAndClose() { this->doClearAndClose(); return; } \ void EditorName::changed(QMetaProperty prop, QVariant val) { this->doChanged(this->sender(), prop, val); return; } \ void EditorName::clickedNew() { this->newEditItem(); return;} diff --git a/src/editors/EditorWithRecipeBase.h b/src/editors/EditorWithRecipeBase.h new file mode 100644 index 00000000..5da6e9b6 --- /dev/null +++ b/src/editors/EditorWithRecipeBase.h @@ -0,0 +1,119 @@ +/*====================================================================================================================== + * editors/EditorWithRecipeBase.h is part of Brewken, and is copyright the following authors 2024: + * • Matt Young + * + * Brewken 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 3 of the License, or (at your option) any later + * version. + * + * Brewken 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 this program. If not, see + * . + =====================================================================================================================*/ +#ifndef EDITORS_EDITORWITHRECIPEBASE_H +#define EDITORS_EDITORWITHRECIPEBASE_H +#pragma once + +#include "editors/EditorBase.h" +#include "model/Recipe.h" + +/** + * \brief Extends \c EditorBase for editors where it makes sense for us to be able to watch a \c Recipe, because a + * \c Recipe can have at most one of the thing we are editing (\c Boil, \c Equipment, \c Fermentation, \c Mash, + * \c Style). + * + * Classes deriving from this one have the same requirements as those deriving directly from \c EditorBase, + * except that: + * EDITOR_WITH_RECIPE_COMMON_DECL should be placed in the header file instead of EDITOR_COMMON_DECL + * EDITOR_WITH_RECIPE_COMMON_CODE should be placed in the .cpp file instead of EDITOR_COMMON_CODE + */ +template +class EditorWithRecipeBase : public EditorBase { +public: + EditorWithRecipeBase() : + EditorBase{}, + m_recipeObs{nullptr} { + return; + } + virtual ~EditorWithRecipeBase() = default; + + void setRecipe(Recipe * recipe) { + this->m_recipeObs = recipe; + // TBD: We could automatically set the edit item as follows: +// if (this->m_recipeObs) { +// this->m_editItem = this->m_recipeObs->get() +// } + return; + } + + /** + * \brief Override \c EditorBase::doChanged + */ + virtual void doChanged(QObject * sender, QMetaProperty prop, QVariant val) { + // Extra handling if sender is Recipe we are observing... + if (this->m_recipeObs && sender == this->m_recipeObs) { + this->readAllFields(); + return; + } + // ...otherwise we fall back to the base class handling + this->EditorBase::doChanged(sender, prop, val); + return; + } + + /** + * \brief + */ + void doShowEditor() { + this->readAllFields(); + this->derived().setVisible(true); + return; + } + + /** + * \brief + */ + void doCloseEditor() { + this->derived().setVisible(true); + return; + } + +protected: + + /** + * \brief The \c Recipe, if any, that we are "observing". + */ + Recipe * m_recipeObs; +}; + +/** + * \brief Derived classes should include this in their header file, right after Q_OBJECT, instead of EDITOR_COMMON_DECL + * (which this macro also pulls in). + * + * Note we have to be careful about comment formats in macro definitions + */ +#define EDITOR_WITH_RECIPE_COMMON_DECL(NeName) \ + EDITOR_COMMON_DECL(NeName) \ + \ + /* This allows EditorWithRecipeBase to call protected and private members of Derived */ \ + friend class EditorWithRecipeBase; \ + \ + public slots: \ + /* Additional standard slots for editors with recipe */ \ + void showEditor(); \ + void closeEditor(); \ + +/** + * \brief Derived classes should include this in their implementation file, usually at the end, instead of + * EDITOR_COMMON_CODE (which this macro also pulls in). + */ +#define EDITOR_WITH_RECIPE_COMMON_CODE(EditorName) \ + EDITOR_COMMON_CODE(EditorName) \ + void EditorName::showEditor() { this->doShowEditor(); return; } \ + void EditorName::closeEditor() { this->doCloseEditor(); return; } \ + + + +#endif diff --git a/src/editors/EquipmentEditor.cpp b/src/editors/EquipmentEditor.cpp index df7c7cc4..2e8e21e0 100644 --- a/src/editors/EquipmentEditor.cpp +++ b/src/editors/EquipmentEditor.cpp @@ -418,4 +418,4 @@ void EquipmentEditor::updateDefaultEquipment() { } // Insert the boiler-plate stuff that we cannot do in EditorBase -EDITOR_COMMON_SLOT_DEFINITIONS(EquipmentEditor) +EDITOR_COMMON_CODE(EquipmentEditor) diff --git a/src/editors/EquipmentEditor.h b/src/editors/EquipmentEditor.h index 6307db8b..a0a682d8 100644 --- a/src/editors/EquipmentEditor.h +++ b/src/editors/EquipmentEditor.h @@ -22,11 +22,11 @@ #define EDITORS_EQUIPMENTEDITOR_H #pragma once -#include "ui_equipmentEditor.h" - #include #include +#include "ui_equipmentEditor.h" + #include "editors/EditorBase.h" #include "model/Equipment.h" @@ -56,51 +56,5 @@ public slots: double calcBatchSize(); }; -///class EquipmentEditor : public QDialog, private Ui::equipmentEditor { -/// Q_OBJECT -/// -///public: -/// //! \param singleEquipEditor true if you do not want the necessary elements for viewing all the database elements. -/// EquipmentEditor( QWidget *parent=nullptr, bool singleEquipEditor=false ); -/// virtual ~EquipmentEditor(); -/// -/// //! Edit the given equipment. -/// void setEquipment( Equipment* e ); -/// //! Create a new equipment record -/// void newEquipment(QString folder = ""); -/// -///public slots: -/// //! Save the changes to the equipment. -/// void save(); -/// //! Delete the equipment from the database. -/// void removeEquipment(); -/// //! Set the equipment to default values. -/// void clear(); -/// //! Close the dialog, throwing away changes. -/// void cancel(); -/// //! Set absorption back to default. -/// void resetAbsorption(); -/// -/// //! Edit the equipment currently selected in our combobox. -/// void equipmentSelected(); -/// //! If state==Qt::Checked, set the "calculate boil volume" checkbox. Otherwise, unset. -/// void updateCheckboxRecord(); -/// //! \brief set the default equipment, or unset the current equipment as the default -/// void updateDefaultEquipment(int state); -/// -/// void changed(QMetaProperty, QVariant); -/// -/// double calcBatchSize(); -/// -///protected: -/// void closeEvent(QCloseEvent *event); -/// -///private: -/// void showChanges(); -/// -/// Equipment* obsEquip; -/// EquipmentListModel* equipmentListModel; -/// NamedEntitySortProxyModel* equipmentSortProxyModel; -///}; #endif diff --git a/src/editors/FermentableEditor.cpp b/src/editors/FermentableEditor.cpp index 36cdabd1..28c90489 100644 --- a/src/editors/FermentableEditor.cpp +++ b/src/editors/FermentableEditor.cpp @@ -45,7 +45,6 @@ FermentableEditor::FermentableEditor(QWidget* parent) : SMART_FIELD_INIT(FermentableEditor, label_maxInBatch , lineEdit_maxInBatch , Fermentable, PropertyNames::Fermentable::maxInBatch_pct , 0); SMART_FIELD_INIT(FermentableEditor, label_moisture , lineEdit_moisture , Fermentable, PropertyNames::Fermentable::moisture_pct , 0); SMART_FIELD_INIT(FermentableEditor, label_protein , lineEdit_protein , Fermentable, PropertyNames::Fermentable::protein_pct , 0); -/// SMART_FIELD_INIT(FermentableEditor, label_yield , lineEdit_yield , Fermentable, PropertyNames::Fermentable::yield_pct , 1); SMART_FIELD_INIT(FermentableEditor, label_inventory , lineEdit_inventory , Fermentable, PropertyNames::Ingredient::totalInventory , 1); SMART_FIELD_INIT(FermentableEditor, label_origin , lineEdit_origin , Fermentable, PropertyNames::Fermentable::origin ); SMART_FIELD_INIT(FermentableEditor, label_supplier , lineEdit_supplier , Fermentable, PropertyNames::Fermentable::supplier ); @@ -77,7 +76,7 @@ FermentableEditor::FermentableEditor(QWidget* parent) : /// SMART_CHECK_BOX_INIT(FermentableEditor, checkBox_amountIsWeight , label_amountIsWeight , lineEdit_inventory , Fermentable, amountIsWeight ); -/// BT_COMBO_BOX_INIT_COPQ(FermentableEditor, comboBox_amountType, Fermentable, PropertyNames::Ingredient::totalInventory, lineEdit_inventory); + BT_COMBO_BOX_INIT_COPQ(FermentableEditor, comboBox_amountType, Fermentable, PropertyNames::Ingredient::totalInventory, lineEdit_inventory); this->connectSignalsAndSlots(); return; @@ -88,23 +87,20 @@ FermentableEditor::~FermentableEditor() = default; void FermentableEditor::writeFieldsToEditItem() { this->m_editItem->setType(this->comboBox_type ->getNonOptValue()); - this->m_editItem->setName (this->lineEdit_name ->text ()); -/// this->m_editItem->setYield_pct (this->lineEdit_yield ->getNonOptValue()); - this->m_editItem->setColor_srm (this->lineEdit_color ->getNonOptCanonicalQty ()); -/// this->m_editItem->setAddAfterBoil (this->checkBox_addAfterBoil ->checkState() == Qt::Checked); - this->m_editItem->setOrigin (this->lineEdit_origin ->text ()); - this->m_editItem->setSupplier (this->lineEdit_supplier ->text ()); - this->m_editItem->setCoarseFineDiff_pct (this->lineEdit_coarseFineDiff->getNonOptValue()); - this->m_editItem->setMoisture_pct (this->lineEdit_moisture ->getNonOptValue()); - this->m_editItem->setDiastaticPower_lintner(this->lineEdit_diastaticPower->getNonOptCanonicalQty ()); - this->m_editItem->setProtein_pct (this->lineEdit_protein ->getNonOptValue()); - this->m_editItem->setMaxInBatch_pct (this->lineEdit_maxInBatch ->getNonOptValue()); + this->m_editItem->setName (this->lineEdit_name ->text ()); + this->m_editItem->setColor_srm (this->lineEdit_color ->getNonOptCanonicalQty()); + this->m_editItem->setOrigin (this->lineEdit_origin ->text ()); + this->m_editItem->setSupplier (this->lineEdit_supplier ->text ()); + this->m_editItem->setCoarseFineDiff_pct (this->lineEdit_coarseFineDiff->getOptValue ()); + this->m_editItem->setMoisture_pct (this->lineEdit_moisture ->getOptValue ()); + this->m_editItem->setDiastaticPower_lintner(this->lineEdit_diastaticPower->getOptCanonicalQty ()); + this->m_editItem->setProtein_pct (this->lineEdit_protein ->getOptValue ()); + // See below for call to setTotalInventory, which needs to be done "late" + this->m_editItem->setMaxInBatch_pct (this->lineEdit_maxInBatch ->getOptValue ()); this->m_editItem->setRecommendMash (this->checkBox_recommendMash ->checkState() == Qt::Checked); -/// this->m_editItem->setIsMashed (this->checkBox_isMashed ->checkState() == Qt::Checked); - this->m_editItem->setIbuGalPerLb (this->lineEdit_ibuGalPerLb ->getNonOptValue()); // .:TBD:. No metric measure? + this->m_editItem->setIbuGalPerLb (this->lineEdit_ibuGalPerLb ->getOptValue()); // .:TBD:. No metric measure? this->m_editItem->setNotes (this->textEdit_notes ->toPlainText ()); // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ -/// this->m_editItem->setAmountIsWeight (this->checkBox_amountIsWeight ->isChecked ()); this->m_editItem->setGrainGroup (this->comboBox_grainGroup ->getOptValue()); this->m_editItem->setProducer (this->lineEdit_producer ->text ()); this->m_editItem->setProductId (this->lineEdit_productId ->text ()); @@ -130,8 +126,18 @@ void FermentableEditor::writeFieldsToEditItem() { } void FermentableEditor::writeLateFieldsToEditItem() { - // Do this late to make sure we've the row in the inventory table - this->m_editItem->setTotalInventory(lineEdit_inventory->getNonOptCanonicalAmt()); + // + // Do this late to make sure we've the row in the inventory table (because total inventory amount isn't really an + // attribute of the Fermentable). + // + // Note that we do not need to store the value of comboBox_amountType. It merely controls the available unit for + // lineEdit_inventory + // + // Note that, if the inventory field is blank, we'll treat that as meaning "don't change the inventory" + // + if (!this->lineEdit_inventory->isEmptyOrBlank()) { + this->m_editItem->setTotalInventory(lineEdit_inventory->getNonOptCanonicalAmt()); + } return; } @@ -139,22 +145,21 @@ void FermentableEditor::readFieldsFromEditItem(std::optional propName) if (!propName || *propName == PropertyNames::NamedEntity::name ) { this->lineEdit_name ->setTextCursor(m_editItem->name ()); // Continues to next line this->tabWidget_editor->setTabText(0, m_editItem->name()); if (propName) { return; } } if (!propName || *propName == PropertyNames::Fermentable::type ) { this->comboBox_type ->setValue (m_editItem->type ()); if (propName) { return; } } -/// if (!propName || *propName == PropertyNames::Fermentable::yield_pct ) { this->lineEdit_yield ->setQuantity (m_editItem->yield_pct ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Fermentable::color_srm ) { this->lineEdit_color ->setQuantity (m_editItem->color_srm ()); if (propName) { return; } } -/// if (!propName || *propName == PropertyNames::Fermentable::addAfterBoil ) { this->checkBox_addAfterBoil ->setCheckState(m_editItem->addAfterBoil() ? Qt::Checked : Qt::Unchecked); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Fermentable::color_srm ) { this->lineEdit_color ->setQuantity (m_editItem->color_srm ()); if (propName) { return; } } if (!propName || *propName == PropertyNames::Fermentable::origin ) { this->lineEdit_origin ->setTextCursor(m_editItem->origin ()); if (propName) { return; } } if (!propName || *propName == PropertyNames::Fermentable::supplier ) { this->lineEdit_supplier ->setTextCursor(m_editItem->supplier ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Fermentable::coarseFineDiff_pct ) { this->lineEdit_coarseFineDiff->setQuantity (m_editItem->coarseFineDiff_pct ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Fermentable::moisture_pct ) { this->lineEdit_moisture ->setQuantity (m_editItem->moisture_pct ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Fermentable::diastaticPower_lintner) { this->lineEdit_diastaticPower->setQuantity (m_editItem->diastaticPower_lintner()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Fermentable::protein_pct ) { this->lineEdit_protein ->setQuantity (m_editItem->protein_pct ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Fermentable::maxInBatch_pct ) { this->lineEdit_maxInBatch ->setQuantity (m_editItem->maxInBatch_pct ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Fermentable::coarseFineDiff_pct ) { this->lineEdit_coarseFineDiff->setQuantity (m_editItem->coarseFineDiff_pct ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Fermentable::moisture_pct ) { this->lineEdit_moisture ->setQuantity (m_editItem->moisture_pct ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Fermentable::diastaticPower_lintner) { this->lineEdit_diastaticPower->setQuantity (m_editItem->diastaticPower_lintner()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Fermentable::protein_pct ) { this->lineEdit_protein ->setQuantity (m_editItem->protein_pct ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Ingredient::totalInventory ) { this->lineEdit_inventory ->setAmount (m_editItem->totalInventory ()); + this->comboBox_amountType ->autoSetFromControlledField(); + if (propName) { return; } } + if (!propName || *propName == PropertyNames::Fermentable::maxInBatch_pct ) { this->lineEdit_maxInBatch ->setQuantity (m_editItem->maxInBatch_pct ()); if (propName) { return; } } if (!propName || *propName == PropertyNames::Fermentable::recommendMash ) { this->checkBox_recommendMash ->setCheckState(m_editItem->recommendMash() ? Qt::Checked : Qt::Unchecked); if (propName) { return; } } -/// if (!propName || *propName == PropertyNames::Fermentable::isMashed ) { this->checkBox_isMashed ->setCheckState(m_editItem->isMashed() ? Qt::Checked : Qt::Unchecked); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Fermentable::ibuGalPerLb ) { this->lineEdit_ibuGalPerLb ->setQuantity (m_editItem->ibuGalPerLb ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Fermentable::ibuGalPerLb ) { this->lineEdit_ibuGalPerLb ->setQuantity (m_editItem->ibuGalPerLb ()); if (propName) { return; } } if (!propName || *propName == PropertyNames::Fermentable::notes ) { this->textEdit_notes ->setPlainText (m_editItem->notes ()); if (propName) { return; } } // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ -/// if (!propName || *propName == PropertyNames::Fermentable::amountIsWeight ) { this->checkBox_amountIsWeight ->setChecked (m_editItem->amountIsWeight ()); if (propName) { return; } } if (!propName || *propName == PropertyNames::Fermentable::grainGroup ) { this->comboBox_grainGroup ->setValue (m_editItem->grainGroup ()); if (propName) { return; } } if (!propName || *propName == PropertyNames::Fermentable::producer ) { this->lineEdit_producer ->setTextCursor(m_editItem->producer ()); if (propName) { return; } } if (!propName || *propName == PropertyNames::Fermentable::productId ) { this->lineEdit_productId ->setTextCursor(m_editItem->productId ()); if (propName) { return; } } @@ -176,8 +181,9 @@ void FermentableEditor::readFieldsFromEditItem(std::optional propName) if (!propName || *propName == PropertyNames::Fermentable::fermentability_pct ) { this->lineEdit_fermentability_pct ->setQuantity (m_editItem->fermentability_pct ()); if (propName) { return; } } if (!propName || *propName == PropertyNames::Fermentable::betaGlucan_ppm ) { this->lineEdit_betaGlucan ->setQuantity (m_editItem->betaGlucan_ppm ()); if (propName) { return; } } + this->label_id_value->setText(QString::number(m_editItem->key())); return; } // Insert the boiler-plate stuff that we cannot do in EditorBase -EDITOR_COMMON_SLOT_DEFINITIONS(FermentableEditor) +EDITOR_COMMON_CODE(FermentableEditor) diff --git a/src/editors/FermentationEditor.cpp b/src/editors/FermentationEditor.cpp index 8b49cd6f..c96f9b77 100644 --- a/src/editors/FermentationEditor.cpp +++ b/src/editors/FermentationEditor.cpp @@ -22,117 +22,141 @@ #include "model/Fermentation.h" #include "model/Recipe.h" -FermentationEditor::FermentationEditor(QWidget* parent) : QDialog(parent), m_fermentationObs{nullptr} { - setupUi(this); - - // NB: label_description / textEdit_description don't need initialisation here as neither is a smart field - // NB: label_notes / textEdit_notes don't need initialisation here as neither is a smart field - SMART_FIELD_INIT(FermentationEditor, label_name , lineEdit_name , Fermentation, PropertyNames::NamedEntity::name ); - - connect(this, &QDialog::accepted, this, &FermentationEditor::saveAndClose); - connect(this, &QDialog::rejected, this, &FermentationEditor::closeEditor ); +FermentationEditor::FermentationEditor(QWidget* parent) : + QDialog(parent), + EditorWithRecipeBase() { + this->setupUi(this); + this->postSetupUiInit( + { + EDITOR_FIELD(Fermentation, label_name , lineEdit_name , PropertyNames::NamedEntity::name ), + EDITOR_FIELD(Fermentation, label_description, textEdit_description, PropertyNames::Fermentation::description ), + EDITOR_FIELD(Fermentation, label_notes , textEdit_notes , PropertyNames::Fermentation::notes ) + } + ); + +/// // NB: label_description / textEdit_description don't need initialisation here as neither is a smart field +/// // NB: label_notes / textEdit_notes don't need initialisation here as neither is a smart field +/// SMART_FIELD_INIT(FermentationEditor, label_name , lineEdit_name , Fermentation, PropertyNames::NamedEntity::name ); +/// +/// connect(this, &QDialog::accepted, this, &FermentationEditor::saveAndClose); +/// connect(this, &QDialog::rejected, this, &FermentationEditor::closeEditor ); return; } FermentationEditor::~FermentationEditor() = default; -void FermentationEditor::showEditor() { - showChanges(); - setVisible(true); - return; -} - -void FermentationEditor::closeEditor() { - setVisible(false); - return; -} - -void FermentationEditor::saveAndClose() { - bool isNew = false; - - if (!this->m_fermentationObs) { - this->m_fermentationObs = std::make_shared(lineEdit_name->text()); - isNew = true; - } - qDebug() << Q_FUNC_INFO << "Saving" << (isNew ? "new" : "existing") << "fermentation (#" << this->m_fermentationObs->key() << ")"; - - this->m_fermentationObs->setName (this->lineEdit_name ->text ()); - this->m_fermentationObs->setDescription (this->textEdit_description->toPlainText ()); - this->m_fermentationObs->setNotes (this->textEdit_notes ->toPlainText ()); - - if (isNew) { - ObjectStoreWrapper::insert(this->m_fermentationObs); - this->m_rec->setFermentation(this->m_fermentationObs); - } - - return; -} - -void FermentationEditor::setFermentation(std::shared_ptr fermentation) { - if (this->m_fermentationObs) { - disconnect(this->m_fermentationObs.get(), nullptr, this, nullptr); - } - - this->m_fermentationObs = fermentation; - if (this->m_fermentationObs) { - connect(this->m_fermentationObs.get(), &NamedEntity::changed, this, &FermentationEditor::changed); - showChanges(); - } - return; -} - -void FermentationEditor::setRecipe(Recipe * recipe) { - if (!recipe) { - return; - } - - this->m_rec = recipe; - +void FermentationEditor::writeFieldsToEditItem() { return; } -void FermentationEditor::changed(QMetaProperty prop, QVariant /*val*/) { - if (!this->m_fermentationObs) { - return; - } - - - if (sender() == this->m_fermentationObs.get()) { - this->showChanges(&prop); - } - - if (sender() == this->m_rec) { - this->showChanges(); - } +void FermentationEditor::writeLateFieldsToEditItem() { return; } -void FermentationEditor::showChanges(QMetaProperty* prop) { - if (!this->m_fermentationObs) { - this->clear(); - return; - } - - QString propName; - bool updateAll = false; - if (prop == nullptr) { - updateAll = true; - } else { - propName = prop->name(); - } - qDebug() << Q_FUNC_INFO << "Updating" << (updateAll ? "all" : "property") << propName; - - if (updateAll || propName == PropertyNames::NamedEntity::name ) {this->lineEdit_name ->setText (m_fermentationObs->name ()); if (!updateAll) { return; } } - if (updateAll || propName == PropertyNames::Fermentation::description) {this->textEdit_description->setPlainText(m_fermentationObs->description()); if (!updateAll) { return; } } - if (updateAll || propName == PropertyNames::Fermentation::notes ) {this->textEdit_notes ->setPlainText(m_fermentationObs->notes ()); if (!updateAll) { return; } } +void FermentationEditor::readFieldsFromEditItem([[maybe_unused]] std::optional propName) { return; } -void FermentationEditor::clear() { - this->lineEdit_name ->setText (""); - this->textEdit_description ->setText (""); - this->lineEdit_preFermentationSize->setText (""); - this->lineEdit_fermentationTime ->setText (""); - this->textEdit_notes ->setPlainText(""); - return; -} +// Insert the boilerplate stuff that we cannot do in EditorWithRecipeBase +EDITOR_WITH_RECIPE_COMMON_CODE(FermentationEditor) + +///void FermentationEditor::showEditor() { +/// showChanges(); +/// setVisible(true); +/// return; +///} +/// +///void FermentationEditor::closeEditor() { +/// setVisible(false); +/// return; +///} +/// +///void FermentationEditor::saveAndClose() { +/// bool isNew = false; +/// +/// if (!this->m_fermentationObs) { +/// this->m_fermentationObs = std::make_shared(lineEdit_name->text()); +/// isNew = true; +/// } +/// qDebug() << Q_FUNC_INFO << "Saving" << (isNew ? "new" : "existing") << "fermentation (#" << this->m_fermentationObs->key() << ")"; +/// +/// this->m_fermentationObs->setName (this->lineEdit_name ->text ()); +/// this->m_fermentationObs->setDescription (this->textEdit_description->toPlainText ()); +/// this->m_fermentationObs->setNotes (this->textEdit_notes ->toPlainText ()); +/// +/// if (isNew) { +/// ObjectStoreWrapper::insert(this->m_fermentationObs); +/// this->m_rec->setFermentation(this->m_fermentationObs); +/// } +/// +/// return; +///} +/// +///void FermentationEditor::setFermentation(std::shared_ptr fermentation) { +/// if (this->m_fermentationObs) { +/// disconnect(this->m_fermentationObs.get(), nullptr, this, nullptr); +/// } +/// +/// this->m_fermentationObs = fermentation; +/// if (this->m_fermentationObs) { +/// connect(this->m_fermentationObs.get(), &NamedEntity::changed, this, &FermentationEditor::changed); +/// showChanges(); +/// } +/// return; +///} +/// +///void FermentationEditor::setRecipe(Recipe * recipe) { +/// if (!recipe) { +/// return; +/// } +/// +/// this->m_rec = recipe; +/// +/// return; +///} +/// +///void FermentationEditor::changed(QMetaProperty prop, QVariant /*val*/) { +/// if (!this->m_fermentationObs) { +/// return; +/// } +/// +/// +/// if (sender() == this->m_fermentationObs.get()) { +/// this->showChanges(&prop); +/// } +/// +/// if (sender() == this->m_rec) { +/// this->showChanges(); +/// } +/// return; +///} +/// +///void FermentationEditor::showChanges(QMetaProperty* prop) { +/// if (!this->m_fermentationObs) { +/// this->clear(); +/// return; +/// } +/// +/// QString propName; +/// bool updateAll = false; +/// if (prop == nullptr) { +/// updateAll = true; +/// } else { +/// propName = prop->name(); +/// } +/// qDebug() << Q_FUNC_INFO << "Updating" << (updateAll ? "all" : "property") << propName; +/// +/// if (updateAll || propName == PropertyNames::NamedEntity::name ) {this->lineEdit_name ->setText (m_fermentationObs->name ()); if (!updateAll) { return; } } +/// if (updateAll || propName == PropertyNames::Fermentation::description) {this->textEdit_description->setPlainText(m_fermentationObs->description()); if (!updateAll) { return; } } +/// if (updateAll || propName == PropertyNames::Fermentation::notes ) {this->textEdit_notes ->setPlainText(m_fermentationObs->notes ()); if (!updateAll) { return; } } +/// return; +///} +/// +///void FermentationEditor::clear() { +/// this->lineEdit_name ->setText (""); +/// this->textEdit_description ->setText (""); +/// this->lineEdit_preFermentationSize->setText (""); +/// this->lineEdit_fermentationTime ->setText (""); +/// this->textEdit_notes ->setPlainText(""); +/// return; +///} diff --git a/src/editors/FermentationEditor.h b/src/editors/FermentationEditor.h index 1db49056..5338f515 100644 --- a/src/editors/FermentationEditor.h +++ b/src/editors/FermentationEditor.h @@ -20,11 +20,11 @@ #include #include #include + #include "ui_fermentationEditor.h" -// Forward declarations. -class Recipe; -class Fermentation; +#include "editors/EditorWithRecipeBase.h" +#include "model/Fermentation.h" /*! * \class FermentationEditor @@ -33,26 +33,34 @@ class Fermentation; * * See also \c NamedFermentationEditor */ -class FermentationEditor : public QDialog, public Ui::fermentationEditor { +class FermentationEditor : public QDialog, + public Ui::fermentationEditor, + public EditorWithRecipeBase { Q_OBJECT -public: - FermentationEditor(QWidget * parent = nullptr); - ~FermentationEditor(); - -public slots: - void showEditor(); - void closeEditor(); - void saveAndClose(); - //! Set the fermentation we wish to view/edit. - void setFermentation(std::shared_ptr fermentation); - void setRecipe(Recipe* r); - void changed(QMetaProperty,QVariant); -private: - void showChanges(QMetaProperty* prop = nullptr); - void clear(); - Recipe* m_rec; - std::shared_ptr m_fermentationObs; + EDITOR_WITH_RECIPE_COMMON_DECL(Fermentation) }; +///class FermentationEditor : public QDialog, public Ui::fermentationEditor { +/// Q_OBJECT +///public: +/// FermentationEditor(QWidget * parent = nullptr); +/// ~FermentationEditor(); +/// +///public slots: +/// void showEditor(); +/// void closeEditor(); +/// void saveAndClose(); +/// //! Set the fermentation we wish to view/edit. +/// void setFermentation(std::shared_ptr fermentation); +/// void setRecipe(Recipe* r); +/// +/// void changed(QMetaProperty,QVariant); +///private: +/// void showChanges(QMetaProperty* prop = nullptr); +/// void clear(); +/// Recipe* m_rec; +/// std::shared_ptr m_fermentationObs; +///}; + #endif diff --git a/src/editors/FermentationStepEditor.cpp b/src/editors/FermentationStepEditor.cpp index 422de4fd..14393f3c 100644 --- a/src/editors/FermentationStepEditor.cpp +++ b/src/editors/FermentationStepEditor.cpp @@ -24,7 +24,8 @@ FermentationStepEditor::FermentationStepEditor(QWidget* parent) : this->setupUi(this); // NB: Although FermentationStep inherits (via StepExtended) from Step, the rampTime_mins field is not used and - // should not be stored in the DB or serialised. See comment in model/Step.h. + // should not be stored in the DB or serialised. See comment in model/Step.h. There should therefore not be + // any label_rampTime or lineEdit_rampTime fields in the .ui file! // // NB: label_description / textEdit_description don't need initialisation here as neither is a smart field SMART_FIELD_INIT(FermentationStepEditor, label_name , lineEdit_name , FermentationStep, PropertyNames:: NamedEntity::name ); @@ -57,10 +58,8 @@ void FermentationStepEditor::readFieldsFromEditItem(std::optional propN if (!propName || *propName == PropertyNames:: Step::endAcidity_pH ) { this->lineEdit_endAcidity ->setQuantity (m_editItem->endAcidity_pH ()); if (propName) { return; } } if (!propName || *propName == PropertyNames::StepExtended::startGravity_sg) { this->lineEdit_startGravity->setQuantity (m_editItem->startGravity_sg()); if (propName) { return; } } if (!propName || *propName == PropertyNames::StepExtended::endGravity_sg ) { this->lineEdit_endGravity ->setQuantity (m_editItem->endGravity_sg ()); if (propName) { return; } } - - if (!propName || *propName == PropertyNames::FermentationStep::vessel ) { this->lineEdit_vessel ->setTextCursor (m_editItem->vessel ()); if (propName) { return; } } - - if (!propName || *propName == PropertyNames::FermentationStep::freeRise) { this->boolCombo_freeRise->setValue(m_editItem->freeRise()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::FermentationStep::vessel ) { this->lineEdit_vessel ->setTextCursor(m_editItem->vessel ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::FermentationStep::freeRise ) { this->boolCombo_freeRise ->setValue (m_editItem->freeRise ()); if (propName) { return; } } return; } @@ -68,18 +67,17 @@ void FermentationStepEditor::readFieldsFromEditItem(std::optional propN void FermentationStepEditor::writeFieldsToEditItem() { // NB: Although FermentationStep inherits (via StepExtended) from Step, the rampTime_mins field is not used and // should not be stored in the DB or serialised. See comment in model/Step.h. - this->m_editItem->setName (this->lineEdit_name->text()); - this->m_editItem->setDescription (this->textEdit_description ->toPlainText ()); - this->m_editItem->setStartTemp_c (this->lineEdit_startTemp ->getNonOptCanonicalQty()); - this->m_editItem->setStepTime_mins (this->lineEdit_stepTime ->getNonOptCanonicalQty()); - this->m_editItem->setEndTemp_c (this->lineEdit_endTemp ->getOptCanonicalQty ()); - this->m_editItem->setStartAcidity_pH (this->lineEdit_startAcidity->getOptCanonicalQty ()); - this->m_editItem->setEndAcidity_pH (this->lineEdit_endAcidity ->getOptCanonicalQty ()); - this->m_editItem->setStartGravity_sg (this->lineEdit_startGravity->getOptCanonicalQty ()); - this->m_editItem->setEndGravity_sg (this->lineEdit_endGravity ->getOptCanonicalQty ()); - - this->m_editItem->setVessel(this->lineEdit_vessel->text()); - this->m_editItem->setFreeRise(this->boolCombo_freeRise->getOptBoolValue()); + this->m_editItem->setName (this->lineEdit_name ->text ()); + this->m_editItem->setDescription (this->textEdit_description ->toPlainText ()); + this->m_editItem->setStartTemp_c (this->lineEdit_startTemp ->getOptCanonicalQty()); + this->m_editItem->setStepTime_mins (this->lineEdit_stepTime ->getOptCanonicalQty()); + this->m_editItem->setEndTemp_c (this->lineEdit_endTemp ->getOptCanonicalQty()); + this->m_editItem->setStartAcidity_pH(this->lineEdit_startAcidity->getOptCanonicalQty()); + this->m_editItem->setEndAcidity_pH (this->lineEdit_endAcidity ->getOptCanonicalQty()); + this->m_editItem->setStartGravity_sg(this->lineEdit_startGravity->getOptCanonicalQty()); + this->m_editItem->setEndGravity_sg (this->lineEdit_endGravity ->getOptCanonicalQty()); + this->m_editItem->setVessel (this->lineEdit_vessel ->text ()); + this->m_editItem->setFreeRise (this->boolCombo_freeRise ->getOptBoolValue ()); return; } @@ -89,5 +87,5 @@ void FermentationStepEditor::writeLateFieldsToEditItem() { return; } -// Insert the fermentationer-plate stuff that we cannot do in EditorBase -EDITOR_COMMON_SLOT_DEFINITIONS(FermentationStepEditor) +// Insert the boilerplate stuff that we cannot do in EditorBase +EDITOR_COMMON_CODE(FermentationStepEditor) diff --git a/src/editors/HopEditor.cpp b/src/editors/HopEditor.cpp index c8a34f45..3088871c 100644 --- a/src/editors/HopEditor.cpp +++ b/src/editors/HopEditor.cpp @@ -111,45 +111,58 @@ void HopEditor::writeFieldsToEditItem() { } void HopEditor::writeLateFieldsToEditItem() { - // Do this late to make sure we've the row in the inventory table - this->m_editItem->setTotalInventory(lineEdit_inventory->getNonOptCanonicalAmt()); + // + // Do this late to make sure we've the row in the inventory table (because total inventory amount isn't really an + // attribute of the Hop). + // + // Note that we do not need to store the value of comboBox_amountType. It merely controls the available unit for + // lineEdit_inventory + // + // Note that, if the inventory field is blank, we'll treat that as meaning "don't change the inventory" + // + if (!this->lineEdit_inventory->isEmptyOrBlank()) { + this->m_editItem->setTotalInventory(lineEdit_inventory->getNonOptCanonicalAmt()); + } return; } void HopEditor::readFieldsFromEditItem(std::optional propName) { - if (!propName || *propName == PropertyNames::Hop::type ) { this->comboBox_hopType ->setValue (m_editItem->type ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::form ) { this->comboBox_hopForm ->setValue (m_editItem->form ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::NamedEntity::name ) { this->lineEdit_name ->setTextCursor (m_editItem->name ()); // Continues to next line - this->tabWidget_editor->setTabText(0, m_editItem->name()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::origin ) { this->lineEdit_origin ->setTextCursor (m_editItem->origin ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::alpha_pct ) { this->lineEdit_alpha ->setQuantity (m_editItem->alpha_pct ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::beta_pct ) { this->lineEdit_beta ->setQuantity (m_editItem->beta_pct ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::hsi_pct ) { this->lineEdit_HSI ->setQuantity (m_editItem->hsi_pct ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::humulene_pct ) { this->lineEdit_humulene ->setQuantity (m_editItem->humulene_pct ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::caryophyllene_pct ) { this->lineEdit_caryophyllene ->setQuantity (m_editItem->caryophyllene_pct ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::cohumulone_pct ) { this->lineEdit_cohumulone ->setQuantity (m_editItem->cohumulone_pct ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::myrcene_pct ) { this->lineEdit_myrcene ->setQuantity (m_editItem->myrcene_pct ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::substitutes ) { this->textEdit_substitutes ->setPlainText (m_editItem->substitutes ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::notes ) { this->textEdit_notes ->setPlainText (m_editItem->notes ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Ingredient::totalInventory) { this->lineEdit_inventory ->setAmount (m_editItem->totalInventory ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::type ) { this->comboBox_hopType ->setValue (m_editItem->type ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::form ) { this->comboBox_hopForm ->setValue (m_editItem->form ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::NamedEntity::name ) { this->lineEdit_name ->setTextCursor(m_editItem->name ()); // Continues to next line + this->tabWidget_editor->setTabText(0, m_editItem->name()); + if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::origin ) { this->lineEdit_origin ->setTextCursor(m_editItem->origin ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::alpha_pct ) { this->lineEdit_alpha ->setQuantity (m_editItem->alpha_pct ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::beta_pct ) { this->lineEdit_beta ->setQuantity (m_editItem->beta_pct ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::hsi_pct ) { this->lineEdit_HSI ->setQuantity (m_editItem->hsi_pct ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::humulene_pct ) { this->lineEdit_humulene ->setQuantity (m_editItem->humulene_pct ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::caryophyllene_pct) { this->lineEdit_caryophyllene->setQuantity (m_editItem->caryophyllene_pct()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::cohumulone_pct ) { this->lineEdit_cohumulone ->setQuantity (m_editItem->cohumulone_pct ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::myrcene_pct ) { this->lineEdit_myrcene ->setQuantity (m_editItem->myrcene_pct ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::substitutes ) { this->textEdit_substitutes ->setPlainText (m_editItem->substitutes ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::notes ) { this->textEdit_notes ->setPlainText (m_editItem->notes ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Ingredient::totalInventory) { this->lineEdit_inventory->setAmount (m_editItem->totalInventory ()); + this->comboBox_amountType->autoSetFromControlledField(); + if (propName) { return; } } // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ -/// if (!propName || *propName == PropertyNames::Hop::amountIsWeight ) { this->checkBox_amountIsWeight ->setChecked (m_editItem->amountIsWeight ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::producer ) { this->lineEdit_producer ->setText (m_editItem->producer ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::productId ) { this->lineEdit_productId ->setText (m_editItem->productId ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::year ) { this->lineEdit_year ->setText (m_editItem->year ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::producer ) { this->lineEdit_producer ->setText (m_editItem->producer ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::productId ) { this->lineEdit_productId ->setText (m_editItem->productId ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::year ) { this->lineEdit_year ->setText (m_editItem->year ()); if (propName) { return; } } if (!propName || *propName == PropertyNames::Hop::totalOil_mlPer100g) { this->lineEdit_totalOil_mlPer100g->setQuantity (m_editItem->totalOil_mlPer100g()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::farnesene_pct ) { this->lineEdit_farnesene ->setQuantity (m_editItem->farnesene_pct ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::geraniol_pct ) { this->lineEdit_geraniol ->setQuantity (m_editItem->geraniol_pct ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::bPinene_pct ) { this->lineEdit_bPinene ->setQuantity (m_editItem->bPinene_pct ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::linalool_pct ) { this->lineEdit_linalool ->setQuantity (m_editItem->linalool_pct ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::limonene_pct ) { this->lineEdit_limonene ->setQuantity (m_editItem->limonene_pct ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::nerol_pct ) { this->lineEdit_nerol ->setQuantity (m_editItem->nerol_pct ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::pinene_pct ) { this->lineEdit_pinene ->setQuantity (m_editItem->pinene_pct ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::polyphenols_pct ) { this->lineEdit_polyphenols ->setQuantity (m_editItem->polyphenols_pct ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Hop::xanthohumol_pct ) { this->lineEdit_xanthohumol ->setQuantity (m_editItem->xanthohumol_pct ()); if (propName) { return; } } - + if (!propName || *propName == PropertyNames::Hop::farnesene_pct ) { this->lineEdit_farnesene ->setQuantity(m_editItem->farnesene_pct ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::geraniol_pct ) { this->lineEdit_geraniol ->setQuantity(m_editItem->geraniol_pct ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::bPinene_pct ) { this->lineEdit_bPinene ->setQuantity(m_editItem->bPinene_pct ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::linalool_pct ) { this->lineEdit_linalool ->setQuantity(m_editItem->linalool_pct ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::limonene_pct ) { this->lineEdit_limonene ->setQuantity(m_editItem->limonene_pct ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::nerol_pct ) { this->lineEdit_nerol ->setQuantity(m_editItem->nerol_pct ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::pinene_pct ) { this->lineEdit_pinene ->setQuantity(m_editItem->pinene_pct ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::polyphenols_pct) { this->lineEdit_polyphenols->setQuantity(m_editItem->polyphenols_pct()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Hop::xanthohumol_pct) { this->lineEdit_xanthohumol->setQuantity(m_editItem->xanthohumol_pct()); if (propName) { return; } } + + this->label_id_value->setText(QString::number(m_editItem->key())); return; } // Insert the boiler-plate stuff that we cannot do in EditorBase -EDITOR_COMMON_SLOT_DEFINITIONS(HopEditor) +EDITOR_COMMON_CODE(HopEditor) diff --git a/src/editors/MashStepEditor.cpp b/src/editors/MashStepEditor.cpp index 2ea95c72..2943d8f0 100644 --- a/src/editors/MashStepEditor.cpp +++ b/src/editors/MashStepEditor.cpp @@ -111,4 +111,4 @@ void MashStepEditor::grayOutStuff([[maybe_unused]] QString const & text) { } // Insert the boiler-plate stuff that we cannot do in EditorBase -EDITOR_COMMON_SLOT_DEFINITIONS(MashStepEditor) +EDITOR_COMMON_CODE(MashStepEditor) diff --git a/src/editors/MiscEditor.cpp b/src/editors/MiscEditor.cpp index 8c913b5e..1743bbea 100644 --- a/src/editors/MiscEditor.cpp +++ b/src/editors/MiscEditor.cpp @@ -37,10 +37,8 @@ MiscEditor::MiscEditor(QWidget * parent) : SMART_FIELD_INIT(MiscEditor, label_name , lineEdit_name , Misc, PropertyNames::NamedEntity::name); SMART_FIELD_INIT(MiscEditor, label_inventory, lineEdit_inventory, Misc, PropertyNames::Ingredient::totalInventory, 1); -/// SMART_FIELD_INIT(MiscEditor, label_time , lineEdit_time , Misc, PropertyNames::Misc::time_min ); BT_COMBO_BOX_INIT(MiscEditor, comboBox_type, Misc, type); -/// BT_COMBO_BOX_INIT(MiscEditor, comboBox_use , Misc, use ); BT_COMBO_BOX_INIT_COPQ(MiscEditor, comboBox_amountType, Misc, PropertyNames::Ingredient::totalInventory, lineEdit_inventory); @@ -55,13 +53,8 @@ MiscEditor::MiscEditor(QWidget * parent) : MiscEditor::~MiscEditor() = default; void MiscEditor::writeFieldsToEditItem() { - this->m_editItem->setType(this->comboBox_type->getNonOptValue()); -/// this->m_editItem->setUse (this->comboBox_use ->getOptValue ()); - this->m_editItem->setName (this->lineEdit_name ->text ()); -/// this->m_editItem->setTime_min (this->lineEdit_time ->getNonOptValue()); -/// this->m_editItem->setAmountIsWeight(this->checkBox_amountIsWeight->isChecked ()); this->m_editItem->setUseFor (this->textEdit_useFor ->toPlainText ()); this->m_editItem->setNotes (this->textEdit_notes ->toPlainText ()); // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ @@ -71,28 +64,37 @@ void MiscEditor::writeFieldsToEditItem() { } void MiscEditor::writeLateFieldsToEditItem() { - // Since inventory amount isn't really an attribute of the Misc, it's best to store it after we know the - // Misc has a DB record. - this->m_editItem->setTotalInventory(lineEdit_inventory->getNonOptCanonicalAmt()); + // + // Do this late to make sure we've the row in the inventory table (because total inventory amount isn't really an + // attribute of the Misc). + // + // Note that we do not need to store the value of comboBox_amountType. It merely controls the available unit for + // lineEdit_inventory + // + // Note that, if the inventory field is blank, we'll treat that as meaning "don't change the inventory" + // + if (!this->lineEdit_inventory->isEmptyOrBlank()) { + this->m_editItem->setTotalInventory(lineEdit_inventory->getNonOptCanonicalAmt()); + } return; } void MiscEditor::readFieldsFromEditItem(std::optional propName) { - if (!propName || *propName == PropertyNames::NamedEntity::name ) { this->lineEdit_name ->setTextCursor(m_editItem->name ()); // Continues to next line - this->tabWidget_editor->setTabText(0, m_editItem->name()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Misc::type ) { this->comboBox_type ->setValue (m_editItem->type ()); if (propName) { return; } } -/// if (!propName || *propName == PropertyNames::Misc::use ) { this->comboBox_use ->setValue (m_editItem->use ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Ingredient::totalInventory) { this->lineEdit_inventory ->setAmount (m_editItem->totalInventory ()); if (propName) { return; } } -/// if (!propName || *propName == PropertyNames::Misc::time_min ) { this->lineEdit_time ->setQuantity (m_editItem->time_min ()); if (propName) { return; } } -/// if (!propName || *propName == PropertyNames::Misc::amountIsWeight ) { this->checkBox_amountIsWeight->setChecked (m_editItem->amountIsWeight()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Misc::useFor ) { this->textEdit_useFor ->setPlainText (m_editItem->useFor ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Misc::notes ) { this->textEdit_notes ->setPlainText (m_editItem->notes ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::NamedEntity::name) { this->lineEdit_name ->setTextCursor(m_editItem->name ()); // Continues to next line + this->tabWidget_editor->setTabText(0, m_editItem->name()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Misc::type ) { this->comboBox_type ->setValue (m_editItem->type ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Ingredient::totalInventory) { this->lineEdit_inventory->setAmount(m_editItem->totalInventory()); + this->comboBox_amountType->autoSetFromControlledField(); + if (propName) { return; } } + if (!propName || *propName == PropertyNames::Misc::useFor ) { this->textEdit_useFor ->setPlainText (m_editItem->useFor ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Misc::notes ) { this->textEdit_notes ->setPlainText (m_editItem->notes ()); if (propName) { return; } } // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ - if (!propName || *propName == PropertyNames::Misc::producer ) { this->lineEdit_producer ->setTextCursor(m_editItem->producer ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Misc::productId ) { this->lineEdit_productId ->setTextCursor(m_editItem->productId ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Misc::producer ) { this->lineEdit_producer ->setTextCursor(m_editItem->producer ()); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Misc::productId ) { this->lineEdit_productId->setTextCursor(m_editItem->productId()); if (propName) { return; } } + this->label_id_value->setText(QString::number(m_editItem->key())); return; } // Insert the boiler-plate stuff that we cannot do in EditorBase -EDITOR_COMMON_SLOT_DEFINITIONS(MiscEditor) +EDITOR_COMMON_CODE(MiscEditor) diff --git a/src/editors/StyleEditor.cpp b/src/editors/StyleEditor.cpp index f881aba3..c145e023 100644 --- a/src/editors/StyleEditor.cpp +++ b/src/editors/StyleEditor.cpp @@ -136,4 +136,4 @@ void StyleEditor::readFieldsFromEditItem(std::optional propName) { } // Insert the boiler-plate stuff that we cannot do in EditorBase -EDITOR_COMMON_SLOT_DEFINITIONS(StyleEditor) +EDITOR_COMMON_CODE(StyleEditor) diff --git a/src/editors/YeastEditor.cpp b/src/editors/YeastEditor.cpp index c8581aa2..ab46e5aa 100644 --- a/src/editors/YeastEditor.cpp +++ b/src/editors/YeastEditor.cpp @@ -41,17 +41,13 @@ YeastEditor::YeastEditor(QWidget * parent) : SMART_FIELD_INIT(YeastEditor, label_inventory , lineEdit_inventory , Yeast, PropertyNames::Ingredient::totalInventory, 1); SMART_FIELD_INIT(YeastEditor, label_productId , lineEdit_productId , Yeast, PropertyNames::Yeast::productId ); SMART_FIELD_INIT(YeastEditor, label_minTemperature, lineEdit_minTemperature, Yeast, PropertyNames::Yeast::minTemperature_c, 1); -/// SMART_FIELD_INIT(YeastEditor, label_attenuation , lineEdit_attenuation , Yeast, PropertyNames::Yeast::attenuation_pct , 0); SMART_FIELD_INIT(YeastEditor, label_maxTemperature, lineEdit_maxTemperature, Yeast, PropertyNames::Yeast::maxTemperature_c, 1); -/// SMART_FIELD_INIT(YeastEditor, label_timesCultured , lineEdit_timesCultured , Yeast, PropertyNames::Yeast::timesCultured , 0); SMART_FIELD_INIT(YeastEditor, label_maxReuse , lineEdit_maxReuse , Yeast, PropertyNames::Yeast::maxReuse ); BT_COMBO_BOX_INIT(HopEditor, comboBox_yeastType , Yeast, type ); BT_COMBO_BOX_INIT(HopEditor, comboBox_yeastForm , Yeast, form ); BT_COMBO_BOX_INIT(HopEditor, comboBox_yeastFlocculation, Yeast, flocculation); -/// BT_BOOL_COMBO_BOX_INIT(YeastEditor, boolCombo_addToSecondary, Yeast, addToSecondary); - BT_COMBO_BOX_INIT_COPQ(YeastEditor, comboBox_amountType, Yeast, PropertyNames::Ingredient::totalInventory, lineEdit_inventory); // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ @@ -79,16 +75,12 @@ void YeastEditor::writeFieldsToEditItem() { this->m_editItem->setName (lineEdit_name ->text() ); this->m_editItem->setType (comboBox_yeastType ->getNonOptValue() ); this->m_editItem->setForm (comboBox_yeastForm ->getNonOptValue() ); -/// this->m_editItem->setAmountIsWeight (checkBox_amountIsWeight ->checkState() == Qt::Checked ); this->m_editItem->setLaboratory (lineEdit_laboratory ->text() ); this->m_editItem->setProductId (lineEdit_productId ->text() ); this->m_editItem->setMinTemperature_c(lineEdit_minTemperature ->getOptCanonicalQty() ); // ⮜⮜⮜ Optional in BeerXML ⮞⮞⮞ this->m_editItem->setMaxTemperature_c(lineEdit_maxTemperature ->getOptCanonicalQty() ); // ⮜⮜⮜ Optional in BeerXML ⮞⮞⮞ this->m_editItem->setFlocculation (comboBox_yeastFlocculation->getOptValue()); -/// this->m_editItem->setAttenuation_pct (lineEdit_attenuation ->getOptValue() ); // ⮜⮜⮜ Optional in BeerXML ⮞⮞⮞ -/// this->m_editItem->setTimesCultured (lineEdit_timesCultured ->getOptValue() ); // ⮜⮜⮜ Optional in BeerXML ⮞⮞⮞ this->m_editItem->setMaxReuse (lineEdit_maxReuse ->getOptValue() ); // ⮜⮜⮜ Optional in BeerXML ⮞⮞⮞ -/// this->m_editItem->setAddToSecondary (boolCombo_addToSecondary ->getOptBoolValue() ); // ⮜⮜⮜ Optional in BeerXML ⮞⮞⮞ this->m_editItem->setBestFor (textEdit_bestFor ->toPlainText() ); this->m_editItem->setNotes (textEdit_notes ->toPlainText() ); // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ @@ -107,8 +99,18 @@ void YeastEditor::writeFieldsToEditItem() { } void YeastEditor::writeLateFieldsToEditItem() { - // do this late to make sure we've the row in the inventory table - this->m_editItem->setTotalInventory(lineEdit_inventory->getNonOptCanonicalAmt()); + // + // Do this late to make sure we've the row in the inventory table (because total inventory amount isn't really an + // attribute of the Misc). + // + // Note that we do not need to store the value of comboBox_amountType. It merely controls the available unit for + // lineEdit_inventory + // + // Note that, if the inventory field is blank, we'll treat that as meaning "don't change the inventory" + // + if (!this->lineEdit_inventory->isEmptyOrBlank()) { + this->m_editItem->setTotalInventory(lineEdit_inventory->getNonOptCanonicalAmt()); + } return; } @@ -117,8 +119,9 @@ void YeastEditor::readFieldsFromEditItem(std::optional propName) { this->tabWidget_editor ->setTabText(0, m_editItem->name ()); if (propName) { return; } } if (!propName || *propName == PropertyNames::Yeast::type ) { this->comboBox_yeastType ->setValue (m_editItem->type ()); if (propName) { return; } } if (!propName || *propName == PropertyNames::Yeast::form ) { this->comboBox_yeastForm ->setValue (m_editItem->form ()); if (propName) { return; } } - if (!propName || *propName == PropertyNames::Ingredient::totalInventory) { this->lineEdit_inventory ->setAmount (m_editItem->totalInventory ()); if (propName) { return; } } -/// if (!propName || *propName == PropertyNames::Yeast::amountIsWeight ) { this->checkBox_amountIsWeight ->setCheckState((m_editItem->amountIsWeight()) ? Qt::Checked : Qt::Unchecked); if (propName) { return; } } + if (!propName || *propName == PropertyNames::Ingredient::totalInventory) { this->lineEdit_inventory ->setAmount (m_editItem->totalInventory ()); + this->comboBox_amountType ->autoSetFromControlledField(); + if (propName) { return; } } if (!propName || *propName == PropertyNames::Yeast::laboratory ) { this->lineEdit_laboratory ->setText (m_editItem->laboratory ()); // Continues to next line this->lineEdit_laboratory ->setCursorPosition(0) ; if (propName) { return; } } if (!propName || *propName == PropertyNames::Yeast::productId ) { this->lineEdit_productId ->setText (m_editItem->productId ()); // Continues to next line @@ -126,10 +129,7 @@ void YeastEditor::readFieldsFromEditItem(std::optional propName) { if (!propName || *propName == PropertyNames::Yeast::minTemperature_c ) { this->lineEdit_minTemperature ->setQuantity (m_editItem->minTemperature_c()); if (propName) { return; } } if (!propName || *propName == PropertyNames::Yeast::maxTemperature_c ) { this->lineEdit_maxTemperature ->setQuantity (m_editItem->maxTemperature_c()); if (propName) { return; } } if (!propName || *propName == PropertyNames::Yeast::flocculation ) { this->comboBox_yeastFlocculation->setValue (m_editItem->flocculation ()); if (propName) { return; } } -/// if (!propName || *propName == PropertyNames::Yeast::attenuation_pct ) { this->lineEdit_attenuation ->setQuantity (m_editItem->attenuation_pct ()); if (propName) { return; } } -/// if (!propName || *propName == PropertyNames::Yeast::timesCultured ) { this->lineEdit_timesCultured ->setQuantity (m_editItem->timesCultured ()); if (propName) { return; } } if (!propName || *propName == PropertyNames::Yeast::maxReuse ) { this->lineEdit_maxReuse ->setQuantity (m_editItem->maxReuse ()); if (propName) { return; } } -/// if (!propName || *propName == PropertyNames::Yeast::addToSecondary ) { this->boolCombo_addToSecondary ->setValue (m_editItem->addToSecondary ()); if (propName) { return; } } if (!propName || *propName == PropertyNames::Yeast::bestFor ) { this->textEdit_bestFor ->setPlainText(m_editItem->bestFor ()); if (propName) { return; } } if (!propName || *propName == PropertyNames::Yeast::notes ) { this->textEdit_notes ->setPlainText(m_editItem->notes ()); if (propName) { return; } } // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ @@ -144,8 +144,9 @@ void YeastEditor::readFieldsFromEditItem(std::optional propName) { if (!propName || *propName == PropertyNames::Yeast::killerProducingKlusToxin ) { this->boolCombo_killerProducingKlusToxin ->setValue(m_editItem->killerProducingKlusToxin ()); if (propName) { return; } } if (!propName || *propName == PropertyNames::Yeast::killerNeutral ) { this->boolCombo_killerNeutral ->setValue(m_editItem->killerNeutral ()); if (propName) { return; } } + this->label_id_value->setText(QString::number(m_editItem->key())); return; } // Insert the boiler-plate stuff that we cannot do in EditorBase -EDITOR_COMMON_SLOT_DEFINITIONS(YeastEditor) +EDITOR_COMMON_CODE(YeastEditor) diff --git a/src/measurement/Amount.cpp b/src/measurement/Amount.cpp index c8cd7ef9..4ba59327 100644 --- a/src/measurement/Amount.cpp +++ b/src/measurement/Amount.cpp @@ -1,5 +1,5 @@ /*====================================================================================================================== - * measurement/Amount.cpp is part of Brewken, and is copyright the following authors 2022-2023: + * measurement/Amount.cpp is part of Brewken, and is copyright the following authors 2022-2024: * • Matt Young * * Brewken is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License @@ -19,6 +19,7 @@ #include #include "measurement/Unit.h" +#include "utils/FuzzyCompare.h" namespace Measurement { @@ -57,38 +58,67 @@ namespace Measurement { return *this; } - bool Amount::isValid() const { - return (this->unit && this->quantity >= 0.0); + bool Amount::operator==(Amount const & other) const { + // Most of the time, everything will be in canonical units, but if two amounts for the same physical quantity are + // in different units, they should still be comparable. + auto const lhsCanonical {this->unit->toCanonical(this->quantity)}; + auto const rhsCanonical {other.unit->toCanonical(other.quantity)}; + // Unit classes are singletons so, once everything is in canonical units, it's OK to compare just the pointers + return lhsCanonical.unit == rhsCanonical.unit && Utils::FuzzyCompare(lhsCanonical.quantity, rhsCanonical.quantity); } -} - -bool operator<(Measurement::Amount const & lhs, Measurement::Amount const & rhs) { - // Amounts in the same units are trivial to compare - if (lhs.unit == rhs.unit) { - return lhs.quantity < rhs.quantity; + bool Amount::operator!=(Amount const & other) const { + // Don't reinvent the wheel '!=' should just be the opposite of '==' + return !(*this == other); } - // It's a coding error if we try to compare two things that aren't a measure of the same physical quantity (because - // it's meaningless to compare a temperature to a mass, etc - Q_ASSERT(lhs.unit->getPhysicalQuantity() == rhs.unit->getPhysicalQuantity()); - - return lhs.unit->toCanonical(lhs.quantity).quantity < rhs.unit->toCanonical(lhs.quantity).quantity; -} + std::partial_ordering Amount::operator<=>(Amount const & other) const { + // Comments above in operator== also apply here + auto const lhsCanonical {this->unit->toCanonical(this->quantity)}; + auto const rhsCanonical {other.unit->toCanonical(other.quantity)}; + if (lhsCanonical.unit != rhsCanonical.unit) { + return std::partial_ordering::unordered; + } + if (Utils::FuzzyCompare(lhsCanonical.quantity, rhsCanonical.quantity)) { + return std::partial_ordering::equivalent; + } + // Now we did the fuzzy compare, anything that's not a fuzzy match is safe to compare "as normal" + return (lhsCanonical.quantity < rhsCanonical.quantity) ? std::partial_ordering::less : + std::partial_ordering::greater; + }; -bool operator==(Measurement::Amount const & lhs, Measurement::Amount const & rhs) { - // Amounts in the same units are trivial to compare - if (lhs.unit == rhs.unit) { - return lhs.quantity == rhs.quantity; + bool Amount::isValid() const { + return (this->unit && this->quantity >= 0.0); } - // It's a coding error if we try to compare two things that aren't a measure of the same physical quantity (because - // it's meaningless to compare a temperature to a mass, etc - Q_ASSERT(lhs.unit->getPhysicalQuantity() == rhs.unit->getPhysicalQuantity()); - - return lhs.unit->toCanonical(lhs.quantity).quantity == rhs.unit->toCanonical(lhs.quantity).quantity; } +///bool operator<(Measurement::Amount const & lhs, Measurement::Amount const & rhs) { +/// // Amounts in the same units are trivial to compare +/// if (lhs.unit == rhs.unit) { +/// return lhs.quantity < rhs.quantity; +/// } +/// +/// // It's a coding error if we try to compare two things that aren't a measure of the same physical quantity (because +/// // it's meaningless to compare a temperature to a mass, etc +/// Q_ASSERT(lhs.unit->getPhysicalQuantity() == rhs.unit->getPhysicalQuantity()); +/// +/// return lhs.unit->toCanonical(lhs.quantity).quantity < rhs.unit->toCanonical(lhs.quantity).quantity; +///} +/// +///bool operator==(Measurement::Amount const & lhs, Measurement::Amount const & rhs) { +/// // Amounts in the same units are trivial to compare +/// if (lhs.unit == rhs.unit) { +/// return lhs.quantity == rhs.quantity; +/// } +/// +/// // It's a coding error if we try to compare two things that aren't a measure of the same physical quantity (because +/// // it's meaningless to compare a temperature to a mass, etc +/// Q_ASSERT(lhs.unit->getPhysicalQuantity() == rhs.unit->getPhysicalQuantity()); +/// +/// return lhs.unit->toCanonical(lhs.quantity).quantity == rhs.unit->toCanonical(lhs.quantity).quantity; +///} + template S & operator<<(S & stream, Measurement::Amount const amount) { // QDebug puts extra spaces around each thing you output but QTextStream does not (I think), so, to get the right gap diff --git a/src/measurement/Amount.h b/src/measurement/Amount.h index b8272d57..e952ab36 100644 --- a/src/measurement/Amount.h +++ b/src/measurement/Amount.h @@ -1,5 +1,5 @@ /*====================================================================================================================== - * measurement/Amount.h is part of Brewken, and is copyright the following authors 2022-2023: + * measurement/Amount.h is part of Brewken, and is copyright the following authors 2022-2024: * • Matt Young * * Brewken is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License @@ -17,6 +17,8 @@ #define MEASUREMENT_AMOUNT_H #pragma once +#include + // Note that we cannot #include "measurement/Unit.h" because that header file already includes this one namespace Measurement { @@ -64,14 +66,23 @@ namespace Measurement { //! Move assignment operator Amount & operator=(Amount && other) noexcept; + //! Equality operator does fuzzy comparison + bool operator==(Amount const & other) const; + + //! Inequality operator implemented in terms of equality operator + bool operator!=(Amount const & other) const; + + //! Three-way comparison (aka spaceship) operator + std::partial_ordering operator<=>(Amount const & other) const; + //! Checks for an uninitialised (or badly initialised) amount bool isValid() const; }; } -bool operator<(Measurement::Amount const & lhs, Measurement::Amount const & rhs); -bool operator==(Measurement::Amount const & lhs, Measurement::Amount const & rhs); +///bool operator<(Measurement::Amount const & lhs, Measurement::Amount const & rhs); +///bool operator==(Measurement::Amount const & lhs, Measurement::Amount const & rhs); /** * \brief Convenience function to allow output of \c Measurement::Amount to \c QDebug or \c QTextStream stream etc diff --git a/src/measurement/IbuMethods.cpp b/src/measurement/IbuMethods.cpp index 1de7826c..f471a115 100644 --- a/src/measurement/IbuMethods.cpp +++ b/src/measurement/IbuMethods.cpp @@ -1,5 +1,5 @@ /*====================================================================================================================== - * measurement/IbuMethods.cpp is part of Brewken, and is copyright the following authors 2009-2023: + * measurement/IbuMethods.cpp is part of Brewken, and is copyright the following authors 2009-2024: * • Daniel Pettersson * • Mattias Måhl * • Matt Young @@ -18,6 +18,8 @@ =====================================================================================================================*/ #include "measurement/IbuMethods.h" +#include // For std::numbers::pi + #include #include @@ -29,45 +31,93 @@ #include "PersistentSettings.h" namespace { - // The Tinseth, Rager and Garetz methods are explained and discussed at http://www.realbeer.com/hops/FAQ.html - - double tinseth(double AArating, - double hops_grams, - double finalVolume_liters, - double wort_grav, - double minutes) { - return ( - (AArating * hops_grams * 1000) / - finalVolume_liters) * ((1.0 - exp(-0.04 * minutes)) / 4.15) * (1.65 * pow(0.000125, (wort_grav - 1)) - ); + double circleAreaFromRadius(double const radius) { + return std::numbers::pi * radius * radius; + } + + /** + * \brief This intermediate calculation is used in Tinseth's formula and the mIBU formula + * + * \param wortGravity_sg + * \param boilTime_minutes usually measured from the point at which hops are added until flameout + */ + double calculateDecimalAlphaAcidUtilization(double const wortGravity_sg, + double const boilTime_minutes) { + // + // TODO This is Tinseth's "Utilization Table" from which we could probably get a better value for + // decimalAlphaAcidUtilization via look-up and interpolation. + // + // Decimal Alpha Acid Utilization vs. Boil Time and Wort Original Gravity + // + // Boil | Original Gravity + // Time | + // (min) | 1.030 1.040 1.050 1.060 1.070 1.080 1.090 1.100 1.110 1.120 1.130 + // ------+ ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- + // 0 | 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 + // 3 | 0.034 0.031 0.029 0.026 0.024 0.022 0.020 0.018 0.017 0.015 0.014 + // 6 | 0.065 0.059 0.054 0.049 0.045 0.041 0.038 0.035 0.032 0.029 0.026 + // 9 | 0.092 0.084 0.077 0.070 0.064 0.059 0.054 0.049 0.045 0.041 0.037 + // 12 | 0.116 0.106 0.097 0.088 0.081 0.074 0.068 0.062 0.056 0.052 0.047 + // 15 | 0.137 0.125 0.114 0.105 0.096 0.087 0.080 0.073 0.067 0.061 0.056 + // 18 | 0.156 0.142 0.130 0.119 0.109 0.099 0.091 0.083 0.076 0.069 0.063 + // 21 | 0.173 0.158 0.144 0.132 0.120 0.110 0.101 0.092 0.084 0.077 0.070 + // 24 | 0.187 0.171 0.157 0.143 0.131 0.120 0.109 0.100 0.091 0.083 0.076 + // 27 | 0.201 0.183 0.168 0.153 0.140 0.128 0.117 0.107 0.098 0.089 0.082 + // 30 | 0.212 0.194 0.177 0.162 0.148 0.135 0.124 0.113 0.103 0.094 0.086 + // 33 | 0.223 0.203 0.186 0.170 0.155 0.142 0.130 0.119 0.108 0.099 0.091 + // 36 | 0.232 0.212 0.194 0.177 0.162 0.148 0.135 0.124 0.113 0.103 0.094 + // 39 | 0.240 0.219 0.200 0.183 0.167 0.153 0.140 0.128 0.117 0.107 0.098 + // 42 | 0.247 0.226 0.206 0.189 0.172 0.158 0.144 0.132 0.120 0.110 0.101 + // 45 | 0.253 0.232 0.212 0.194 0.177 0.162 0.148 0.135 0.123 0.113 0.103 + // 48 | 0.259 0.237 0.216 0.198 0.181 0.165 0.151 0.138 0.126 0.115 0.105 + // 51 | 0.264 0.241 0.221 0.202 0.184 0.169 0.154 0.141 0.129 0.118 0.108 + // 54 | 0.269 0.246 0.224 0.205 0.188 0.171 0.157 0.143 0.131 0.120 0.109 + // 57 | 0.273 0.249 0.228 0.208 0.190 0.174 0.159 0.145 0.133 0.121 0.111 + // 60 | 0.276 0.252 0.231 0.211 0.193 0.176 0.161 0.147 0.135 0.123 0.112 + // 70 | 0.285 0.261 0.238 0.218 0.199 0.182 0.166 0.152 0.139 0.127 0.116 + // 80 | 0.291 0.266 0.243 0.222 0.203 0.186 0.170 0.155 0.142 0.130 0.119 + // 90 | 0.295 0.270 0.247 0.226 0.206 0.188 0.172 0.157 0.144 0.132 0.120 + // 120 | 0.301 0.275 0.252 0.230 0.210 0.192 0.176 0.161 0.147 0.134 0.123 + // + + // + // This is the short-cut way to get decimalAlphaAcidUtilization + // + double const boilTimeFactor = (1.0 - exp(-0.04 * boilTime_minutes)) / 4.15; + double const bignessFactor = 1.65 * pow(0.000125, (wortGravity_sg - 1.0)); + double const decimalAlphaAcidUtilization = bignessFactor * boilTimeFactor; + return decimalAlphaAcidUtilization; + } + + /*! + * \brief Calculates the IBU by Tinseth's formula, as described at http://www.realbeer.com/hops/research.html + */ + double tinseth(IbuMethods::IbuCalculationParms const & parms) { + double const mgPerLiterOfAddedAlphaAcids = (parms.AArating * parms.hops_grams * 1000) / parms.postBoilVolume_liters; + double const decimalAlphaAcidUtilization = calculateDecimalAlphaAcidUtilization(parms.wortGravity_sg, + parms.boilTime_minutes); + return decimalAlphaAcidUtilization * mgPerLiterOfAddedAlphaAcids; +/// return ((AArating * hops_grams * 1000) / postBoilVolume_liters) * ((1.0 - exp(-0.04 * boilTime_minutes)) / 4.15) * (1.65 * pow(0.000125, (wortGravity_sg - 1))); } - double rager(double AArating, - double hops_grams, - double finalVolume_liters, - double wort_grav, - double minutes) { - double utilization = (18.11 + 13.86 * tanh((minutes - 31.32) / 18.17)) / 100.0; + double rager(IbuMethods::IbuCalculationParms const & parms) { + double const utilization = (18.11 + 13.86 * tanh((parms.boilTime_minutes - 31.32) / 18.17)) / 100.0; - double gravityFactor = (wort_grav > 1.050) ? (wort_grav - 1.050)/0.2 : 0.0; + double const gravityFactor = (parms.wortGravity_sg > 1.050) ? (parms.wortGravity_sg - 1.050)/0.2 : 0.0; - return (hops_grams * utilization * AArating * 1000) / (finalVolume_liters * (1 + gravityFactor)); + return (parms.hops_grams * utilization * parms.AArating * 1000) / (parms.postBoilVolume_liters * (1 + gravityFactor)); } /*! - * \brief Calculates the IBU by Greg Noonans formula + * \brief Calculates the IBU by Greg Noonan's formula */ - double noonan(double AArating, - double hops_grams, - double finalVolume_liters, - double wort_grav, - double minutes) { - double volumeFactor = (Measurement::Units::us_gallons.toCanonical(5.0).quantity)/ finalVolume_liters; - double hopsFactor = hops_grams/ (Measurement::Units::ounces.toCanonical(1.0).quantity * 1000.0); - static Polynomial p(Polynomial() << 0.7000029428 << -0.08868853463 << 0.02720809386 << -0.002340415323 << 0.00009925450081 << -0.000002102006144 << 0.00000002132644293 << -0.00000000008229488217); - - //using 60 minutes as a general table - double utilizationFactorTable[4][2] = { + double noonan(IbuMethods::IbuCalculationParms const & parms) { + double const volumeFactor = (Measurement::Units::us_gallons.toCanonical(5.0).quantity)/ parms.postBoilVolume_liters; + double const hopsFactor = parms.hops_grams/ (Measurement::Units::ounces.toCanonical(1.0).quantity * 1000.0); + static const Polynomial p(Polynomial() << 0.7000029428 << -0.08868853463 << 0.02720809386 << -0.002340415323 << 0.00009925450081 << -0.000002102006144 << 0.00000002132644293 << -0.00000000008229488217); + + //using 60 boilTime_minutes as a general table + static double const utilizationFactorTable[4][2] = { {1.050, 1}, {1.065, 0.9286}, {1.085, 0.8571}, @@ -76,30 +126,105 @@ namespace { double utilizationFactor; - if(wort_grav <= utilizationFactorTable[0][0]) { + if (parms.wortGravity_sg <= utilizationFactorTable[0][0]) { utilizationFactor = utilizationFactorTable[0][1]; - } else if(wort_grav <= utilizationFactorTable[1][0]) { + } else if (parms.wortGravity_sg <= utilizationFactorTable[1][0]) { utilizationFactor = utilizationFactorTable[1][1]; - } else if(wort_grav <= utilizationFactorTable[2][0]) { + } else if (parms.wortGravity_sg <= utilizationFactorTable[2][0]) { utilizationFactor = utilizationFactorTable[2][1]; } else { utilizationFactor = utilizationFactorTable[3][1]; } - return(volumeFactor * ( hopsFactor * (100 * AArating) * p.eval(minutes) ) * utilizationFactor); + return(volumeFactor * ( hopsFactor * (100 * parms.AArating) * p.eval(parms.boilTime_minutes) ) * utilizationFactor); + } + + /** + * \brief Intermediate step used by mIBU formula + */ + double computePostBoilUtilization(double const boilTime_minutes, + double const wortGravity_sg, + double const postBoilVolume_liters, + double const coolTime_minutes, + double const kettleInternalDiameter_cm, + double const kettleOpeningDiameter_cm) { + + double const surfaceArea_cm2 = circleAreaFromRadius(kettleInternalDiameter_cm/2.0); + double const openingArea_cm2 = circleAreaFromRadius(kettleOpeningDiameter_cm/2.0); + double const effectiveArea_cm2 = sqrt(surfaceArea_cm2 * openingArea_cm2); + double const b = (0.0002925 * effectiveArea_cm2 / postBoilVolume_liters) + 0.00538; + + double const integrationTime = 0.001; + double decimalAArating = 0.0; + for (double time_minutes = boilTime_minutes; + time_minutes < boilTime_minutes + coolTime_minutes; + time_minutes += integrationTime) { + double const dU = -1.65 * pow(0.000125, (wortGravity_sg-1.0)) * -0.04 * exp(-0.04*time_minutes) / 4.15; + double const temp_degK = 53.70 * exp(-1.0 * b * (time_minutes - boilTime_minutes)) + 319.55; + double const degreeOfUtilization = + // The 1.0 case accounts for nonIAA components + (time_minutes < 5.0) ? 1.0 : 2.39*pow(10.0,11.0)*exp(-9773.0/temp_degK); + double const combinedValue = dU * degreeOfUtilization; + decimalAArating += (combinedValue * integrationTime); + } + return decimalAArating; + } + + /*! + * \brief Calculates the IBU by the mIBU formula, developed by Paul-John Hosom, and described at + * https://alchemyoverlord.wordpress.com/2015/05/12/a-modified-ibu-measurement-especially-for-late-hopping/ + */ + double mIbu(IbuMethods::IbuCalculationParms const & parms) { + // + // Check optional parameters available for this formula. We supply fallback values below, but they likely + // won't be great. + // + if (!parms.coolTime_minutes ) { qWarning() << Q_FUNC_INFO << "coolTime_minutes not set!"; } + if (!parms.kettleInternalDiameter_cm) { qWarning() << Q_FUNC_INFO << "kettleInternalDiameter_cm not set!"; } + if (!parms.kettleOpeningDiameter_cm ) { qWarning() << Q_FUNC_INFO << "kettleOpeningDiameter_cm not set!"; } + double const decimalAlphaAcidUtilization = calculateDecimalAlphaAcidUtilization(parms.wortGravity_sg, + parms.boilTime_minutes); + double const postBoilUtilization = computePostBoilUtilization(parms.boilTime_minutes, + parms.wortGravity_sg, + parms.postBoilVolume_liters, + parms.coolTime_minutes.value_or(0.0), + parms.kettleInternalDiameter_cm.value_or(45.0), + parms.kettleOpeningDiameter_cm.value_or(45.0)); + + double const totalUtilization = decimalAlphaAcidUtilization + postBoilUtilization; + double const IBU = (totalUtilization * parms.AArating * parms.hops_grams * 1000.0) / parms.postBoilVolume_liters; + return IBU; } } -IbuMethods::IbuType IbuMethods::ibuFormula = IbuMethods::TINSETH; +EnumStringMapping const IbuMethods::formulaStringMapping { + {IbuMethods::IbuFormula::Tinseth, "tinseth"}, + {IbuMethods::IbuFormula::Rager , "rager" }, + {IbuMethods::IbuFormula::Noonan , "noonan" }, + {IbuMethods::IbuFormula::mIbu , "mibu" }, + {IbuMethods::IbuFormula::Smph , "smph" }, +}; + +EnumStringMapping const IbuMethods::formulaDisplayNames { + // Not sure how translatable these names are, but I guess it doesn't hurt to include them + {IbuMethods::IbuFormula::Tinseth, QObject::tr("Tinseth")}, + {IbuMethods::IbuFormula::Rager , QObject::tr("Rager" )}, + {IbuMethods::IbuFormula::Noonan , QObject::tr("Noonan" )}, + {IbuMethods::IbuFormula::mIbu , QObject::tr("mIBU" )}, + {IbuMethods::IbuFormula::Smph , QObject::tr("SMPH" )}, +}; + + +IbuMethods::IbuFormula IbuMethods::ibuFormula = IbuMethods::IbuFormula::Tinseth; void IbuMethods::loadIbuFormula() { QString text = PersistentSettings::value(PersistentSettings::Names::ibu_formula, "tinseth").toString(); if (text == "tinseth") { - IbuMethods::ibuFormula = IbuMethods::TINSETH; + IbuMethods::ibuFormula = IbuMethods::IbuFormula::Tinseth; } else if (text == "rager") { - IbuMethods::ibuFormula = IbuMethods::RAGER; + IbuMethods::ibuFormula = IbuMethods::IbuFormula::Rager; } else if (text == "noonan") { - IbuMethods::ibuFormula = IbuMethods::NOONAN; + IbuMethods::ibuFormula = IbuMethods::IbuFormula::Noonan; } else { qCritical() << Q_FUNC_INFO << "Bad ibu_formula type:" << text; } @@ -107,39 +232,23 @@ void IbuMethods::loadIbuFormula() { } void IbuMethods::saveIbuFormula() { - switch(IbuMethods::ibuFormula) { - case IbuMethods::TINSETH: - PersistentSettings::insert(PersistentSettings::Names::ibu_formula, "tinseth"); - break; - case IbuMethods::RAGER: - PersistentSettings::insert(PersistentSettings::Names::ibu_formula, "rager"); - break; - case IbuMethods::NOONAN: - PersistentSettings::insert(PersistentSettings::Names::ibu_formula, "noonan"); - break; - } + PersistentSettings::insert(PersistentSettings::Names::ibu_formula, + IbuMethods::formulaStringMapping[IbuMethods::ibuFormula]); return; } QString IbuMethods::ibuFormulaName() { - switch (IbuMethods::ibuFormula) { - case IbuMethods::TINSETH: return "Tinseth"; - case IbuMethods::RAGER: return "Rager"; - case IbuMethods::NOONAN: return "Noonan"; - } - return QObject::tr("Unknown"); + return IbuMethods::formulaDisplayNames[IbuMethods::ibuFormula]; } -double IbuMethods::getIbus(double AArating, - double hops_grams, - double finalVolume_liters, - double wort_grav, - double minutes) { +double IbuMethods::getIbus(IbuMethods::IbuCalculationParms const & parms) { switch(IbuMethods::ibuFormula) { - case IbuMethods::TINSETH: return tinseth(AArating, hops_grams, finalVolume_liters, wort_grav, minutes); - case IbuMethods::RAGER: return rager(AArating, hops_grams, finalVolume_liters, wort_grav, minutes); - case IbuMethods::NOONAN: return noonan(AArating, hops_grams, finalVolume_liters, wort_grav, minutes); + case IbuMethods::IbuFormula::Tinseth: return tinseth(parms); + case IbuMethods::IbuFormula::Rager : return rager (parms); + case IbuMethods::IbuFormula::Noonan : return noonan (parms); } - qCritical() << Q_FUNC_INFO << QObject::tr("Unrecognized IBU formula type. %1").arg(IbuMethods::ibuFormula); - return tinseth(AArating, hops_grams, finalVolume_liters, wort_grav, minutes); + qCritical() << + Q_FUNC_INFO << "Unrecognized IBU formula type:" << static_cast(IbuMethods::ibuFormula) << + ". Defaulting to Tinseth."; + return tinseth(parms); } diff --git a/src/measurement/IbuMethods.h b/src/measurement/IbuMethods.h index cc4331b4..2902a107 100644 --- a/src/measurement/IbuMethods.h +++ b/src/measurement/IbuMethods.h @@ -1,5 +1,5 @@ /*====================================================================================================================== - * measurement/IbuMethods.h is part of Brewken, and is copyright the following authors 2009-2021: + * measurement/IbuMethods.h is part of Brewken, and is copyright the following authors 2009-2024: * • Daniel Pettersson * • Matt Young * • Philip Greggory Lee @@ -19,6 +19,8 @@ #define MEASUREMENT_IBUMETHODS_H #pragma once +#include "utils/EnumStringMapping.h" + class QString; /*! @@ -27,10 +29,43 @@ class QString; * \brief Make IBU calculations. */ namespace IbuMethods { - //! \brief The formula used to get IBUs. - enum IbuType {TINSETH, RAGER, NOONAN}; + /** + * \brief The formula used to get IBUs. + * + * Tinseth, Rager and Noonan are long-established formulae. mIBU and SMPH are more recent refinements. + * + * The Tinseth, Rager and Garetz methods are explained and discussed at http://www.realbeer.com/hops/FAQ.html + * + * The SMPH and mIBU methods are discussed by John-Paul Hosom at https://byo.com/article/ibu/ and, in more + * detail, at + * - https://alchemyoverlord.wordpress.com/2015/05/12/a-modified-ibu-measurement-especially-for-late-hopping/ + * - https://jphosom.github.io/alchemyoverlord/blog/31-ibus-and-the-smph-model/alchemyoverlord-blog-content31.html + */ + enum class IbuFormula { + Tinseth, + Rager , + Noonan , + mIbu , + Smph , + }; + // Note that we can't use the Q_ENUM macro here to allow storing the above enum class in a QVariant, because Q_ENUM + // is designed to be used inside a class that derives from QObject. + + /*! + * \brief Mapping between \c IbuMethods::IbuFormula and string values suitable for serialisation in + * \c PersistentSettings, etc. + * + * This can also be used to obtain the number of values of \c Type, albeit at run-time rather than + * compile-time. (One day, C++ will have reflection and we won't need to do things this way.) + */ + extern EnumStringMapping const formulaStringMapping; + + /*! + * \brief Localised names of \c IbuMethods::IbuFormula values suitable for displaying to the end user + */ + extern EnumStringMapping const formulaDisplayNames; - extern IbuType ibuFormula; + extern IbuFormula ibuFormula; /** * \brief Read in from persistent settings @@ -45,15 +80,46 @@ namespace IbuMethods { //! \brief return the bitterness formula's name QString ibuFormulaName(); + /** + * \brief Parameters for the various IBU calculation formulae + * + * \param AArating decimal alpha-acid rating of the hops added in [0,1] (0.04 means 4% AA for example) + * \param hops_grams - mass of hops in grams + * \param postBoilVolume_liters - In some explanations, the phrase “finished volume of beer” is used, implying + * “volume into fermenter” ie after both boil-off _and_ any trub & chiller loss. However, the comments at + * https://alchemyoverlord.wordpress.com/2015/05/12/a-modified-ibu-measurement-especially-for-late-hopping/ + * say that Tinseth himself confirmed "Post boil volume is correct" because "We are concerned with the mg/L + * and any portions of a liter lost post boil doesn’t affect the calculation". + * \param wortGravity_sg in specific gravity at around 60F I guess. + * \param boilTime_minutes - minutes that the hops are in the boil: usually measured from the point at which hops are + * added until flameout + * + * \param coolTime_minutes - (Only used in mIbu) Time after flameout, without forced cooling. Note per + * https://alchemyoverlord.wordpress.com/2015/05/12/a-modified-ibu-measurement-especially-for-late-hopping/ + * that if we quickly cool the wort at flameout, then coolTime_minutes should be zero and the mIbu method + * gives the same results as the original Tinseth formula. Only when we have some time between flameout and + * forced cooling do we get additional IBUs. + * \param kettleInternalDiameter_cm - (Only used in mIbu) This is the interior diameter of the kettle at the surface + * of the wort, and is used to calculate the surface area of wort exposed to air. + * \param kettleOpeningDiameter_cm - (Only used in mIbu) This is the interior diameter of the opening in the kettle + * through which steam can escape, and is used to calculate the surface area of + * that same opening. + */ + struct IbuCalculationParms { + double AArating; + double hops_grams; + double postBoilVolume_liters; + double wortGravity_sg; + double boilTime_minutes; + std::optional coolTime_minutes = std::nullopt; + std::optional kettleInternalDiameter_cm = std::nullopt; + std::optional kettleOpeningDiameter_cm = std::nullopt; + }; + /*! * \return IBUs according to selected algorithm. - * \param AArating in [0,1] (0.04 means 4% AA for example) - * \param hops_grams - mass of hops in grams - * \param finalVolume_liters - self explanatory - * \param wort_grav in specific gravity at around 60F I guess. - * \param minutes - minutes that the hops are in the boil */ - double getIbus(double AArating, double hops_grams, double finalVolume_liters, double wort_grav, double minutes); + double getIbus(IbuCalculationParms const & parms); } #endif diff --git a/src/measurement/PhysicalQuantity.cpp b/src/measurement/PhysicalQuantity.cpp index 86b71d9b..47568305 100644 --- a/src/measurement/PhysicalQuantity.cpp +++ b/src/measurement/PhysicalQuantity.cpp @@ -33,6 +33,7 @@ AddSettingName(unitSystem_color ) AddSettingName(unitSystem_count ) AddSettingName(unitSystem_density ) AddSettingName(unitSystem_diastaticPower ) +AddSettingName(unitSystem_length ) AddSettingName(unitSystem_massFractionOrConc ) AddSettingName(unitSystem_specificHeatCapacity) AddSettingName(unitSystem_specificVolume ) @@ -73,6 +74,7 @@ namespace { EnumStringMapping const Measurement::physicalQuantityStringMapping { {Measurement::PhysicalQuantity::Mass , "Mass" }, {Measurement::PhysicalQuantity::Volume , "Volume" }, + {Measurement::PhysicalQuantity::Length , "Length" }, {Measurement::PhysicalQuantity::Count , "Count" }, {Measurement::PhysicalQuantity::Temperature , "Temperature" }, {Measurement::PhysicalQuantity::Time , "Time" }, @@ -91,6 +93,7 @@ EnumStringMapping const Measurement::physicalQuantityStringMapping { EnumStringMapping const Measurement::physicalQuantityDisplayNames { {Measurement::PhysicalQuantity::Mass , QObject::tr("Weight (Mass)" )}, {Measurement::PhysicalQuantity::Volume , QObject::tr("Volume" )}, + {Measurement::PhysicalQuantity::Length , QObject::tr("Length" )}, {Measurement::PhysicalQuantity::Count , QObject::tr("Count" )}, {Measurement::PhysicalQuantity::Temperature , QObject::tr("Temperature" )}, {Measurement::PhysicalQuantity::Time , QObject::tr("Time" )}, @@ -115,6 +118,7 @@ BtStringConst const & Measurement::getSettingsName(PhysicalQuantity const physic // so it would be annoying to just change it now. case Measurement::PhysicalQuantity::Mass : return unitSystem_weight ; case Measurement::PhysicalQuantity::Volume : return unitSystem_volume ; + case Measurement::PhysicalQuantity::Length : return unitSystem_length ; case Measurement::PhysicalQuantity::Time : return unitSystem_time ; case Measurement::PhysicalQuantity::Count : return unitSystem_count ; case Measurement::PhysicalQuantity::Temperature : return unitSystem_temperature ; diff --git a/src/measurement/PhysicalQuantity.h b/src/measurement/PhysicalQuantity.h index 57a4bc38..7e31a460 100644 --- a/src/measurement/PhysicalQuantity.h +++ b/src/measurement/PhysicalQuantity.h @@ -102,6 +102,9 @@ namespace Measurement { Volume, + // Currently used for a couple of equipment dimension measures + Length, + // This is not really a physical quantity. However, in our domain, it makes life simpler for us to pretend that // it is. This is because "mass", "volume" and "number of" are the three canonical ways of measuring ingredients. // Note that this _is_ allowed to be fractional because you might want to add 1½ cinnamon sticks or 2.5 packets of @@ -111,7 +114,12 @@ namespace Measurement { Temperature, - // Note this is durations of time, NOT dates or times of day .:TBD:. Rename to TimeDuration + // Note this NOT dates or times of day but length-of-time or elapsed time, eg duration of a mash step, or how long + // after the start of the boil to add something. I was wondering whether to rename it LengthOfTime, or + // TimeDuration or ElapsedTime or something, but all those names are slightly unsatisfactory (eg several sound + // wrong for "when to add hops"). Since SI units talk about seconds as base unit of time, it doesn't seem crazy + // to stick with just Time here. If we ever (elsewhere) need time of day or something else, we'll pick TimeOfDay + // or TimeStamp etc for that. Time, Color, @@ -182,7 +190,6 @@ namespace Measurement { // Specific volume (= the reciprocal of density) -- see https://en.wikipedia.org/wiki/Specific_volume SpecificVolume, - // .:TBD:. Should we add Energy for PropertyNames::Recipe::calories (in which case, should canonical measure be // Joules)? }; diff --git a/src/measurement/Unit.cpp b/src/measurement/Unit.cpp index 38c2b80d..bc99f0b3 100644 --- a/src/measurement/Unit.cpp +++ b/src/measurement/Unit.cpp @@ -147,20 +147,51 @@ namespace { class Measurement::Unit::impl { public: /** - * Constructor + * Simple case constructor -- conversion to/from canonical units is just multiplication/division */ - impl(Unit & self, + impl(Unit const & self, UnitSystem const & unitSystem, - std::function convertToCanonical, - std::function convertFromCanonical, + double const multiplierToCanonical, double const boundaryValue, bool const isCanonical) : - self {self}, - unitSystem {unitSystem}, - convertToCanonical {convertToCanonical}, - convertFromCanonical{convertFromCanonical}, - boundaryValue {boundaryValue}, - isCanonical {isCanonical} { + m_self {self}, + m_unitSystem {unitSystem}, + m_multiplierToCanonical{multiplierToCanonical}, + m_convertToCanonical {}, + m_convertFromCanonical {}, + m_boundaryValue {boundaryValue}, + m_isCanonical {isCanonical} { + // If this is a canonical unit then, by definition, its multiplier should be 1.0. Usually we wouldn't compare + // doubles, but I'm pretty sure comparing against 1.0 is safe in this context because there will never be a + // rounding error from the `1.0` literal. + // + // Note, however, that it _can_ be valid for a non-canonical unit to have a 1.0 multiplier to and from canonical + // units (eg Lovibond is a no-op conversion to/from SRM). + Q_ASSERT((isCanonical && 1.0 == multiplierToCanonical) || !isCanonical); + + // It's a coding error for the multiplier to be zero. Again, I think this is an OK comparison to do since we're + // checking for source code error rather than "value is so close to zero it might as well be zero". + Q_ASSERT(0.0 != multiplierToCanonical); + + return; + } + + /** + * Complex-case constructor -- conversion to/from canonical units requires formulae (via lambda functions). By + * definition, this cannot be a canonical unit. + */ + impl(Unit const & self, + UnitSystem const & unitSystem, + std::function const convertToCanonical, + std::function const convertFromCanonical, + double const boundaryValue) : + m_self {self}, + m_unitSystem {unitSystem}, + m_multiplierToCanonical{std::nullopt}, + m_convertToCanonical {convertToCanonical}, + m_convertFromCanonical {convertFromCanonical}, + m_boundaryValue {boundaryValue}, + m_isCanonical {false} { return; } @@ -170,32 +201,34 @@ class Measurement::Unit::impl { ~impl() = default; // Member variables for impl - Unit & self; - UnitSystem const & unitSystem; - - std::function convertToCanonical; - std::function convertFromCanonical; - double const boundaryValue; - bool const isCanonical; + Unit const & m_self; + UnitSystem const & m_unitSystem; + + std::optional const m_multiplierToCanonical = std::nullopt; + std::function const m_convertToCanonical = {}; + std::function const m_convertFromCanonical = {}; + double const m_boundaryValue; + bool const m_isCanonical; + // + // TBD: We could store a pointer to the canonical Unit here, since we have it in the constructors below + // }; Measurement::Unit::Unit(UnitSystem const & unitSystem, QString const unitName, - std::function convertToCanonical, - std::function convertFromCanonical, - double boundaryValue, - Measurement::Unit const * canonical) : + double const multiplierToCanonical, + Measurement::Unit const * canonical, + double const boundaryValue) : name{unitName}, pimpl{std::make_unique(*this, unitSystem, - convertToCanonical, - convertFromCanonical, + multiplierToCanonical, boundaryValue, - (canonical == nullptr) )} { + (canonical == nullptr))} { // - // You might think here would be a neat place to the Unit we are constructing to unitNameLookup and, if appropriate, - // physicalQuantityToCanonicalUnit. However, there is not guarantee that unitSystem is constructed at this point, so - // unitSystem.getPhysicalQuantity() could result in a core dump. + // You might think here would be a neat place to add the Unit we are constructing to unitNameLookup and, if + // appropriate, physicalQuantityToCanonicalUnit. However, there is not guarantee that unitSystem is constructed at + // this point, so unitSystem.getPhysicalQuantity() could result in a core dump. // // What we can do safely is add ourselves to listOfAllUnits // @@ -203,13 +236,33 @@ Measurement::Unit::Unit(UnitSystem const & unitSystem, return; } +Measurement::Unit::Unit(UnitSystem const & unitSystem, + QString const unitName, + std::function convertToCanonical, + std::function convertFromCanonical, + Measurement::Unit const * canonical, + double const boundaryValue) : + name{unitName}, + pimpl{std::make_unique(*this, + unitSystem, + convertToCanonical, + convertFromCanonical, + boundaryValue)} { + // It's a coding error if we used this version of the constructor for a canonical unit + Q_ASSERT(canonical); + + // See comment in other version of constructor above + listOfAllUnits.append(this); + return; +} + Measurement::Unit::~Unit() = default; void Measurement::Unit::initialiseLookups() { for (auto const unit : listOfAllUnits) { - Measurement::PhysicalQuantity const physicalQuantity = unit->pimpl->unitSystem.getPhysicalQuantity(); + Measurement::PhysicalQuantity const physicalQuantity = unit->pimpl->m_unitSystem.getPhysicalQuantity(); unitNameLookup.insert(NameLookupKey{physicalQuantity, unit->name.toLower()}, unit); - if (unit->pimpl->isCanonical) { + if (unit->pimpl->m_isCanonical) { physicalQuantityToCanonicalUnit.insert(physicalQuantity, unit); } } @@ -279,28 +332,36 @@ Measurement::Unit const & Measurement::Unit::getCanonical() const { } bool Measurement::Unit::isCanonical() const { - return &this->getCanonical() == this; + Q_ASSERT(this->pimpl->m_isCanonical == (&this->getCanonical() == this)); + return this->pimpl->m_isCanonical; } Measurement::Amount Measurement::Unit::toCanonical(double amt) const { - return Measurement::Amount{this->pimpl->convertToCanonical(amt), this->getCanonical()}; + double const convertedQuantity{ + this->pimpl->m_multiplierToCanonical ? amt * (*this->pimpl->m_multiplierToCanonical) : + this->pimpl->m_convertToCanonical(amt) + }; + return Measurement::Amount{convertedQuantity, this->getCanonical()}; } double Measurement::Unit::fromCanonical(double amt) const { - return this->pimpl->convertFromCanonical(amt); + if (this->pimpl->m_multiplierToCanonical) { + return amt / (*this->pimpl->m_multiplierToCanonical); + } + return this->pimpl->m_convertFromCanonical(amt); } Measurement::PhysicalQuantity Measurement::Unit::getPhysicalQuantity() const { // The PhysicalQuantity for this Unit is already stored in its UnitSystem, so we don't store it separately here - return this->pimpl->unitSystem.getPhysicalQuantity(); + return this->pimpl->m_unitSystem.getPhysicalQuantity(); } Measurement::UnitSystem const & Measurement::Unit::getUnitSystem() const { - return this->pimpl->unitSystem; + return this->pimpl->m_unitSystem; } double Measurement::Unit::boundary() const { - return this->pimpl->boundaryValue; + return this->pimpl->m_boundaryValue; } Measurement::Unit const & Measurement::Unit::getCanonicalUnit(Measurement::PhysicalQuantity const physicalQuantity) { @@ -443,34 +504,44 @@ namespace Measurement::Units { // === Mass === // See comment in measurement/UnitSystem.cpp for why we have separate entities for US Customary pounds/ounces and Imperials ones, even though they are, in fact, the same - Unit const kilograms {Measurement::UnitSystems::mass_Metric, QObject::tr("kg"), [](double x){return x;}, [](double y){return y;}, 1.0}; - Unit const grams {Measurement::UnitSystems::mass_Metric, QObject::tr("g"), [](double x){return x/1000.0;}, [](double y){return y*1000.0;}, 1.0, &kilograms}; - Unit const milligrams {Measurement::UnitSystems::mass_Metric, QObject::tr("mg"), [](double x){return x/1000000.0;}, [](double y){return y*1000000.0;}, 1.0, &kilograms}; - Unit const pounds {Measurement::UnitSystems::mass_UsCustomary, QObject::tr("lb"), [](double x){return x*0.45359237;}, [](double y){return y/0.45359237;}, 1.0, &kilograms}; - Unit const ounces {Measurement::UnitSystems::mass_UsCustomary, QObject::tr("oz"), [](double x){return x*0.0283495231;}, [](double y){return y/0.0283495231;}, 1.0, &kilograms}; - Unit const imperial_pounds {Measurement::UnitSystems::mass_Imperial, QObject::tr("lb"), [](double x){return x*0.45359237;}, [](double y){return y/0.45359237;}, 1.0, &kilograms}; - Unit const imperial_ounces {Measurement::UnitSystems::mass_Imperial, QObject::tr("oz"), [](double x){return x*0.0283495231;}, [](double y){return y/0.0283495231;}, 1.0, &kilograms}; + Unit const kilograms {Measurement::UnitSystems::mass_Metric , QObject::tr("kg")}; + Unit const grams {Measurement::UnitSystems::mass_Metric , QObject::tr("g" ), 1.0/1000.0 , &kilograms}; + Unit const milligrams {Measurement::UnitSystems::mass_Metric , QObject::tr("mg"), 1.0/1000000.0, &kilograms}; + Unit const pounds {Measurement::UnitSystems::mass_UsCustomary, QObject::tr("lb"), 0.45359237 , &kilograms}; + Unit const ounces {Measurement::UnitSystems::mass_UsCustomary, QObject::tr("oz"), 0.0283495231 , &kilograms}; + Unit const imperial_pounds{Measurement::UnitSystems::mass_Imperial , QObject::tr("lb"), 0.45359237 , &kilograms}; + Unit const imperial_ounces{Measurement::UnitSystems::mass_Imperial , QObject::tr("oz"), 0.0283495231 , &kilograms}; // === Volume === // Where possible, the multipliers for going to and from litres come from www.conversion-metric.org as it seems to offer the most decimal places on its conversion tables - Unit const liters {Measurement::UnitSystems::volume_Metric , QObject::tr("L"), [](double x){return x;}, [](double y){return y;}, 1.0}; - Unit const milliliters {Measurement::UnitSystems::volume_Metric , QObject::tr("mL"), [](double x){return x/1000.0;}, [](double y){return y*1000.0;}, 1.0, &liters}; - Unit const us_barrels {Measurement::UnitSystems::volume_UsCustomary, QObject::tr("bbl"), [](double x){return x*117.34777;}, [](double y){return y/117.34777;}, 1.0, &liters}; - Unit const us_gallons {Measurement::UnitSystems::volume_UsCustomary, QObject::tr("gal"), [](double x){return x*3.7854117840007;}, [](double y){return y/3.7854117840007;}, 1.0, &liters}; - Unit const us_quarts {Measurement::UnitSystems::volume_UsCustomary, QObject::tr("qt"), [](double x){return x*0.94635294599999;}, [](double y){return y/0.94635294599999;}, 1.0, &liters}; - Unit const us_pints {Measurement::UnitSystems::volume_UsCustomary, QObject::tr("qt"), [](double x){return x*0.473176473;}, [](double y){return y/0.473176473;}, 1.0, &liters}; - Unit const us_cups {Measurement::UnitSystems::volume_UsCustomary, QObject::tr("cup"), [](double x){return x*0.23658823648491;}, [](double y){return y/0.23658823648491;}, 0.25, &liters}; - Unit const us_fluidOunces {Measurement::UnitSystems::volume_UsCustomary, QObject::tr("floz"), [](double x){return x*0.029573529564112;}, [](double y){return y/0.029573529564112;}, 1.0, &liters}; - Unit const us_tablespoons {Measurement::UnitSystems::volume_UsCustomary, QObject::tr("tbsp"), [](double x){return x*0.014786764782056;}, [](double y){return y/0.014786764782056;}, 1.0, &liters}; - Unit const us_teaspoons {Measurement::UnitSystems::volume_UsCustomary, QObject::tr("tsp"), [](double x){return x*0.0049289215940186;}, [](double y){return y/0.0049289215940186;}, 1.0, &liters}; - Unit const imperial_barrels {Measurement::UnitSystems::volume_Imperial , QObject::tr("bbl"), [](double x){return x*163.659;}, [](double y){return y/163.659;}, 1.0, &liters}; - Unit const imperial_gallons {Measurement::UnitSystems::volume_Imperial , QObject::tr("gal"), [](double x){return x*4.5460899999997;}, [](double y){return y/4.5460899999997;}, 1.0, &liters}; - Unit const imperial_quarts {Measurement::UnitSystems::volume_Imperial , QObject::tr("qt"), [](double x){return x*1.1365225;}, [](double y){return y/1.1365225;}, 1.0, &liters}; - Unit const imperial_pints {Measurement::UnitSystems::volume_Imperial , QObject::tr("qt"), [](double x){return x*0.56826125;}, [](double y){return y/0.56826125;}, 1.0, &liters}; - Unit const imperial_cups {Measurement::UnitSystems::volume_Imperial , QObject::tr("cup"), [](double x){return x*0.284130625;}, [](double y){return y/0.284130625;}, 0.25, &liters}; - Unit const imperial_fluidOunces{Measurement::UnitSystems::volume_Imperial , QObject::tr("floz"), [](double x){return x*0.028413075003383;}, [](double y){return y/0.028413075003383;}, 1.0, &liters}; - Unit const imperial_tablespoons{Measurement::UnitSystems::volume_Imperial , QObject::tr("tbsp"), [](double x){return x*0.0177581714;}, [](double y){return y/0.0177581714;}, 1.0, &liters}; - Unit const imperial_teaspoons {Measurement::UnitSystems::volume_Imperial , QObject::tr("tsp"), [](double x){return x*0.00591939047;}, [](double y){return y/0.00591939047;}, 1.0, &liters}; + Unit const liters {Measurement::UnitSystems::volume_Metric , QObject::tr("L" )}; + Unit const milliliters {Measurement::UnitSystems::volume_Metric , QObject::tr("mL" ), 1.0/1000.0 , &liters}; + Unit const us_barrels {Measurement::UnitSystems::volume_UsCustomary, QObject::tr("bbl" ), 117.34777 , &liters}; + Unit const us_gallons {Measurement::UnitSystems::volume_UsCustomary, QObject::tr("gal" ), 3.7854117840007 , &liters}; + Unit const us_quarts {Measurement::UnitSystems::volume_UsCustomary, QObject::tr("qt" ), 0.94635294599999 , &liters}; + Unit const us_pints {Measurement::UnitSystems::volume_UsCustomary, QObject::tr("qt" ), 0.473176473 , &liters}; + Unit const us_cups {Measurement::UnitSystems::volume_UsCustomary, QObject::tr("cup" ), 0.23658823648491 , &liters, 0.25}; + Unit const us_fluidOunces {Measurement::UnitSystems::volume_UsCustomary, QObject::tr("floz"), 0.029573529564112 , &liters}; + Unit const us_tablespoons {Measurement::UnitSystems::volume_UsCustomary, QObject::tr("tbsp"), 0.014786764782056 , &liters}; + Unit const us_teaspoons {Measurement::UnitSystems::volume_UsCustomary, QObject::tr("tsp" ), 0.0049289215940186, &liters}; + Unit const imperial_barrels {Measurement::UnitSystems::volume_Imperial , QObject::tr("bbl" ), 163.659 , &liters}; + Unit const imperial_gallons {Measurement::UnitSystems::volume_Imperial , QObject::tr("gal" ), 4.5460899999997 , &liters}; + Unit const imperial_quarts {Measurement::UnitSystems::volume_Imperial , QObject::tr("qt" ), 1.1365225 , &liters}; + Unit const imperial_pints {Measurement::UnitSystems::volume_Imperial , QObject::tr("qt" ), 0.56826125 , &liters}; + Unit const imperial_cups {Measurement::UnitSystems::volume_Imperial , QObject::tr("cup" ), 0.284130625 , &liters, 0.25}; + Unit const imperial_fluidOunces{Measurement::UnitSystems::volume_Imperial , QObject::tr("floz"), 0.028413075003383 , &liters}; + Unit const imperial_tablespoons{Measurement::UnitSystems::volume_Imperial , QObject::tr("tbsp"), 0.0177581714 , &liters}; + Unit const imperial_teaspoons {Measurement::UnitSystems::volume_Imperial , QObject::tr("tsp" ), 0.00591939047 , &liters}; + + // === Length === + // I suppose we could use the other official abbreviations for feet and inches -- either the real ones (see + // en.wikipedia.org/wiki/Prime_(symbol)) that no-one can type (′ and ″) or what people would actually type + // (' and ") -- but I fear it's a bit more hassle than it's worth. + Unit const centimeters{Measurement::UnitSystems::length_Metric , QObject::tr("cm")}; + Unit const millimeters{Measurement::UnitSystems::length_Metric , QObject::tr("mm"), 0.1, ¢imeters}; + Unit const meters {Measurement::UnitSystems::length_Metric , QObject::tr("m" ), 100.0, ¢imeters}; + Unit const inches {Measurement::UnitSystems::length_UsCustomary, QObject::tr("in"), 2.54, ¢imeters}; + Unit const feet {Measurement::UnitSystems::length_UsCustomary, QObject::tr("ft"), 30.48, ¢imeters}; // === Count === // The choice of abbreviation here is a bit of a compromise, in English at least, because it's a bit unnatural to say @@ -478,58 +549,58 @@ namespace Measurement::Units { // or "2.5 × cinnamon sticks". "Cinnamon sticks: 2.5" would be more natural, but I'm reluctant to have no // abbreviation, as there are, arguably, circumstances where it could lead to ambiguity or confusion. At very least // if we are showing an abbreviation for "number of" then we are showing that the units haven't been forgotten. - Unit const numberOf {Measurement::UnitSystems::count_NumberOf, QObject::tr("(№)"), [](double x){return x;}, [](double y){return y;}, 1.0}; + Unit const numberOf{Measurement::UnitSystems::count_NumberOf, QObject::tr("(№)")}; // === Temperature === - Unit const celsius {Measurement::UnitSystems::temperature_MetricIsCelsius, QObject::tr("C"), [](double x){return x;}, [](double y){return y;}, 1.0}; - Unit const fahrenheit {Measurement::UnitSystems::temperature_UsCustomaryIsFahrenheit, QObject::tr("F"), [](double x){return (x-32)*5.0/9.0;}, [](double y){return y * 9.0/5.0 + 32;}, 1.0, &celsius}; + Unit const celsius {Measurement::UnitSystems::temperature_MetricIsCelsius , QObject::tr("C")}; + Unit const fahrenheit{Measurement::UnitSystems::temperature_UsCustomaryIsFahrenheit, QObject::tr("F"), [](double x){return (x - 32.0) * 5.0/9.0;}, + [](double y){return (y * 9.0/5.0) + 32.0;}, &celsius}; // === Time === // Added weeks because BeerJSON has it // TBD I've put days and weeks in plural here, because in practice it looks jarring to have them in singular, but // maybe we should decide on abbreviations for them. - Unit const minutes {Measurement::UnitSystems::time_CoordinatedUniversalTime, QObject::tr("min"), [](double x){return x;}, [](double y){return y;}, 1.0}; - Unit const weeks {Measurement::UnitSystems::time_CoordinatedUniversalTime, QObject::tr("weeks"), [](double x){return x*(7.0*24.0*60.0);}, [](double y){return y/(7.0*24.0*60.0);}, 1.0, &minutes}; - Unit const days {Measurement::UnitSystems::time_CoordinatedUniversalTime, QObject::tr("days"), [](double x){return x*(24.0*60.0);}, [](double y){return y/(24.0*60.0);}, 1.0, &minutes}; - Unit const hours {Measurement::UnitSystems::time_CoordinatedUniversalTime, QObject::tr("hr"), [](double x){return x*60.0;}, [](double y){return y/60.0;}, 2.0, &minutes}; - Unit const seconds {Measurement::UnitSystems::time_CoordinatedUniversalTime, QObject::tr("s"), [](double x){return x/60.0;}, [](double y){return y*60.0;}, 90.0, &minutes}; + Unit const minutes{Measurement::UnitSystems::time_CoordinatedUniversalTime, QObject::tr("min" )}; + Unit const weeks {Measurement::UnitSystems::time_CoordinatedUniversalTime, QObject::tr("weeks"), (7.0*24.0*60.0), &minutes}; + Unit const days {Measurement::UnitSystems::time_CoordinatedUniversalTime, QObject::tr("days" ), (24.0*60.0) , &minutes}; + Unit const hours {Measurement::UnitSystems::time_CoordinatedUniversalTime, QObject::tr("hr" ), 60.0 , &minutes, 2.0}; + Unit const seconds{Measurement::UnitSystems::time_CoordinatedUniversalTime, QObject::tr("s" ), 1.0/60.0 , &minutes, 90.0}; // === Color === // Not sure how many people use Lovibond scale these days, but BeerJSON supports it, so we need to be able to read // it. https://en.wikipedia.org/wiki/Beer_measurement#Colour= says "The Standard Reference Method (SRM) ... [gives] // results approximately equal to the °L." - Unit const srm {Measurement::UnitSystems::color_StandardReferenceMethod, QObject::tr("srm"), [](double x){return x;}, [](double y){return y;}, 1.0}; - Unit const ebc {Measurement::UnitSystems::color_EuropeanBreweryConvention, QObject::tr("ebc"), [](double x){return x * 12.7/25.0;}, [](double y){return y * 25.0/12.7;}, 1.0, &srm}; - Unit const lovibond {Measurement::UnitSystems::color_Lovibond, QObject::tr("lovibond"), [](double x){return x;}, [](double y){return y;}, 1.0, &srm}; + Unit const srm {Measurement::UnitSystems::color_StandardReferenceMethod , QObject::tr("srm" )}; + Unit const ebc {Measurement::UnitSystems::color_EuropeanBreweryConvention, QObject::tr("ebc" ), 12.7/25.0, &srm}; + Unit const lovibond{Measurement::UnitSystems::color_Lovibond , QObject::tr("lovibond"), 1.0 , &srm}; // == Density === // Brix isn't much used in beer brewing, but BeerJSON supports it, so we have it here. // Per https://en.wikipedia.org/wiki/Beer_measurement, Plato and Brix are "essentially ... the same ([both based on // mass fraction of sucrose) [and only] differ in their conversion from weight percentage to specific gravity in the // fifth and sixth decimal places" - Unit const specificGravity {Measurement::UnitSystems::density_SpecificGravity, QObject::tr("sg"), [](double x){return x;}, [](double y){return y;}, 1.0}; - Unit const plato {Measurement::UnitSystems::density_Plato, - QObject::tr("P"), - [](double x){return x == 0.0 ? 0.0 : Algorithms::PlatoToSG_20C20C(x);}, - [](double y){return y == 0.0 ? 0.0 : Algorithms::SG_20C20C_toPlato(y);}, - 1.0, - &specificGravity}; - Unit const brix {Measurement::UnitSystems::density_Brix, - QObject::tr("brix"), - [](double x){return x == 0.0 ? 0.0 : Algorithms::BrixToSgAt20C(x);}, - [](double y){return y == 0.0 ? 0.0 : Algorithms::SgAt20CToBrix(y);}, - 1.0, - &specificGravity}; + Unit const specificGravity{Measurement::UnitSystems::density_SpecificGravity, QObject::tr("sg"),}; + Unit const plato {Measurement::UnitSystems::density_Plato, + QObject::tr("P"), + [](double x){return x == 0.0 ? 0.0 : Algorithms::PlatoToSG_20C20C(x);}, + [](double y){return y == 0.0 ? 0.0 : Algorithms::SG_20C20C_toPlato(y);}, + &specificGravity}; + Unit const brix {Measurement::UnitSystems::density_Brix, + QObject::tr("brix"), + [](double x){return x == 0.0 ? 0.0 : Algorithms::BrixToSgAt20C(x);}, + [](double y){return y == 0.0 ? 0.0 : Algorithms::SgAt20CToBrix(y);}, + &specificGravity}; // == Diastatic power == - Unit const lintner {Measurement::UnitSystems::diastaticPower_Lintner, QObject::tr("L"), [](double x){return x;}, [](double y){return y;}, 1.0}; - Unit const wk {Measurement::UnitSystems::diastaticPower_WindischKolbach, QObject::tr("WK"), [](double x){return (x + 16) / 3.5;}, [](double y){return 3.5 * y - 16;}, 1.0, &lintner}; + Unit const lintner{Measurement::UnitSystems::diastaticPower_Lintner , QObject::tr("L" )}; + Unit const wk {Measurement::UnitSystems::diastaticPower_WindischKolbach, QObject::tr("WK"), [](double x){return (x + 16.0) / 3.5;}, + [](double y){return (3.5 * y) - 16.0;}, &lintner}; // == Acidity == - Unit const pH {Measurement::UnitSystems::acidity_pH, QObject::tr("pH"), [](double x){return x;}, [](double y){return y;}, 1.0}; + Unit const pH{Measurement::UnitSystems::acidity_pH, QObject::tr("pH")}; // == Bitterness == - Unit const ibu {Measurement::UnitSystems::bitterness_InternationalBitternessUnits, QObject::tr("IBU"), [](double x){return x;}, [](double y){return y;}, 1.0}; + Unit const ibu{Measurement::UnitSystems::bitterness_InternationalBitternessUnits, QObject::tr("IBU")}; // == Carbonation == // Per http://www.uigi.com/co2_conv.html, 1 cubic metre (aka 1000 litres) of CO2 at 1 atmosphere pressure and 0°C @@ -537,34 +608,34 @@ namespace Measurement::Units { // whether we should use 0°C or 20°C or some other temperature for the conversion from volumes to grams per litre. // A brewing-specific source, https://byo.com/article/master-the-action-carbonation/, gives the conversion factor as // 1.96, so we use that. - Unit const carbonationVolumes {Measurement::UnitSystems::carbonation_Volumes, QObject::tr("vol"), [](double x){return x;}, [](double y){return y;}, 1.0}; - Unit const carbonationGramsPerLiter{Measurement::UnitSystems::carbonation_MassPerVolume, QObject::tr("mg/L"), [](double x){return x / 1.96;}, [](double y){return y * 1.96;}, 1.0, &carbonationVolumes}; + Unit const carbonationVolumes {Measurement::UnitSystems::carbonation_Volumes , QObject::tr("vol" )}; + Unit const carbonationGramsPerLiter{Measurement::UnitSystems::carbonation_MassPerVolume, QObject::tr("mg/L"), 1.0/1.96, &carbonationVolumes}; // == Mass Fraction & Mass Concentration == - Unit const partsPerMillionMass {Measurement::UnitSystems::massFractionOrConc_Brewing, QObject::tr("ppm"), [](double x){return x;}, [](double y){return y;}, 1.0}; - Unit const partsPerBillionMass {Measurement::UnitSystems::massFractionOrConc_Brewing, QObject::tr("ppb"), [](double x){return x * 1000.0;}, [](double y){return y/1000.0;}, 1.0, &partsPerMillionMass}; - Unit const milligramsPerLiter {Measurement::UnitSystems::massFractionOrConc_Brewing, QObject::tr("mg/L"), [](double x){return x;}, [](double y){return y;}, 1.0, &partsPerMillionMass}; + Unit const partsPerMillionMass {Measurement::UnitSystems::massFractionOrConc_Brewing, QObject::tr("ppm" )}; + Unit const partsPerBillionMass {Measurement::UnitSystems::massFractionOrConc_Brewing, QObject::tr("ppb" ), 1000.0, &partsPerMillionMass}; + Unit const milligramsPerLiter {Measurement::UnitSystems::massFractionOrConc_Brewing, QObject::tr("mg/L"), 1.0, &partsPerMillionMass}; // == Viscosity == // Yes, 1 centipoise = 1 millipascal-second. See comment in measurement/Unit.h for more info - Unit const centipoise {Measurement::UnitSystems::viscosity_Metric , QObject::tr("cP" ), [](double x){return x;}, [](double y){return y;}, 1.0}; - Unit const millipascalSecond {Measurement::UnitSystems::viscosity_MetricAlternate, QObject::tr("mPa-s"), [](double x){return x;}, [](double y){return y;}, 1.0, ¢ipoise}; + Unit const centipoise {Measurement::UnitSystems::viscosity_Metric , QObject::tr("cP" )}; + Unit const millipascalSecond{Measurement::UnitSystems::viscosity_MetricAlternate, QObject::tr("mPa-s"), 1.0, ¢ipoise}; // == Specific heat capacity == // See comment in measurement/Unit.h for why the non-metric units are the canonical ones - Unit const caloriesPerCelsiusPerGram{Measurement::UnitSystems::specificHeatCapacity_Calories, QObject::tr("c/g·C" ), [](double x){return x;}, [](double y){return y;}, 1.0}; - Unit const joulesPerKelvinPerKg {Measurement::UnitSystems::specificHeatCapacity_Joules , QObject::tr("J/kg·K" ), [](double x){return x / 4184.0;}, [](double y){return y * 4184.0;}, 1.0, &caloriesPerCelsiusPerGram}; - Unit const btuPerFahrenheitPerPound {Measurement::UnitSystems::specificHeatCapacity_Btus , QObject::tr("BTU/lb·F"), [](double x){return x;}, [](double y){return y;}, 1.0, &caloriesPerCelsiusPerGram}; + Unit const caloriesPerCelsiusPerGram{Measurement::UnitSystems::specificHeatCapacity_Calories, QObject::tr("c/g·C" )}; + Unit const joulesPerKelvinPerKg {Measurement::UnitSystems::specificHeatCapacity_Joules , QObject::tr("J/kg·K" ), 1.0/4184.0, &caloriesPerCelsiusPerGram}; + Unit const btuPerFahrenheitPerPound {Measurement::UnitSystems::specificHeatCapacity_Btus , QObject::tr("BTU/lb·F"), 1.0 , &caloriesPerCelsiusPerGram}; // == Specific Volume == - Unit const litresPerKilogram {Measurement::UnitSystems::specificVolume_Metric , QObject::tr("L/kg" ), [](double x){return x;}, [](double y){return y;}, 1.0}; - Unit const litresPerGram {Measurement::UnitSystems::specificVolume_Metric , QObject::tr("L/g" ), [](double x){return x * 1000;}, [](double y){return y / 1000;}, 1.0, &litresPerKilogram}; - Unit const cubicMetersPerKilogram{Measurement::UnitSystems::specificVolume_Metric , QObject::tr("m^3/kg" ), [](double x){return x * 1000;}, [](double y){return y / 1000;}, 1.0, &litresPerKilogram}; - Unit const us_fluidOuncesPerOunce{Measurement::UnitSystems::specificVolume_UsCustomary, QObject::tr("floz/oz"), [](double x){return x * 66.7632356142;}, [](double y){return y / 66.7632356142;}, 1.0, &litresPerKilogram}; - Unit const us_gallonsPerPound {Measurement::UnitSystems::specificVolume_UsCustomary, QObject::tr("gal/lb" ), [](double x){return x * 8.34540445177617;}, [](double y){return y / 8.34540445177617;}, 1.0, &litresPerKilogram}; - Unit const us_quartsPerPound {Measurement::UnitSystems::specificVolume_UsCustomary, QObject::tr("qt/lb" ), [](double x){return x * 2.08635111294;}, [](double y){return y / 2.08635111294;}, 1.0, &litresPerKilogram}; - Unit const us_gallonsPerOunce {Measurement::UnitSystems::specificVolume_UsCustomary, QObject::tr("gal/oz" ), [](double x){return x * 0.521587778236;}, [](double y){return y / 0.521587778236;}, 1.0, &litresPerKilogram}; - Unit const cubicFeetPerPound {Measurement::UnitSystems::specificVolume_UsCustomary, QObject::tr("ft^3/lb"), [](double x){return x * 62.4279605755126;}, [](double y){return y / 62.4279605755126;}, 1.0, &litresPerKilogram}; + Unit const litresPerKilogram {Measurement::UnitSystems::specificVolume_Metric , QObject::tr("L/kg" )}; + Unit const litresPerGram {Measurement::UnitSystems::specificVolume_Metric , QObject::tr("L/g" ), 1000.0 , &litresPerKilogram}; + Unit const cubicMetersPerKilogram{Measurement::UnitSystems::specificVolume_Metric , QObject::tr("m^3/kg" ), 1000.0 , &litresPerKilogram}; + Unit const us_fluidOuncesPerOunce{Measurement::UnitSystems::specificVolume_UsCustomary, QObject::tr("floz/oz"), 66.7632356142 , &litresPerKilogram}; + Unit const us_gallonsPerPound {Measurement::UnitSystems::specificVolume_UsCustomary, QObject::tr("gal/lb" ), 8.34540445177617, &litresPerKilogram}; + Unit const us_quartsPerPound {Measurement::UnitSystems::specificVolume_UsCustomary, QObject::tr("qt/lb" ), 2.08635111294 , &litresPerKilogram}; + Unit const us_gallonsPerOunce {Measurement::UnitSystems::specificVolume_UsCustomary, QObject::tr("gal/oz" ), 0.521587778236 , &litresPerKilogram}; + Unit const cubicFeetPerPound {Measurement::UnitSystems::specificVolume_UsCustomary, QObject::tr("ft^3/lb"), 62.4279605755126 , &litresPerKilogram}; ObjectAddressStringMapping const unitStringMapping { { @@ -595,6 +666,12 @@ namespace Measurement::Units { {imperial_fluidOunces , "imperial_fluid_ounces" }, {imperial_tablespoons , "imperial_tablespoons" }, {imperial_teaspoons , "imperial_teaspoons" }, + // === Length === + {centimeters , "centimeters" }, + {millimeters , "millimeters" }, + {meters , "meters" }, + {inches , "inches" }, + {feet , "feet" }, // === Count === {numberOf , "number_of" }, // === Temperature === diff --git a/src/measurement/Unit.h b/src/measurement/Unit.h index fbef5957..dcb61c9b 100644 --- a/src/measurement/Unit.h +++ b/src/measurement/Unit.h @@ -61,9 +61,9 @@ namespace Measurement { * kilograms, "tsp" for teaspoons. Note that this needs to be unique within the \c UnitSystem to * which this \c Unit belongs but is \c not necessarily globally unique, eg "qt" refers to both * Imperial quarts and US Customary quarts; "L" refers to liters and Lintner. - * \param convertToCanonical Converts a quantity of this \c Unit to a quantity of \c canonical \c Unit - * \param convertFromCanonical Converts a quantity of \c canonical \c Unit to a quantity of this \c Unit - * \param boundaryValue + * \param multiplierToCanonical Factor by which to multiply a quantity of this \c Unit to convert it to a quantity + * of \c canonical \c Unit + * \param boundaryValue Threshold below which a smaller unit (of the same type) should be used \see \c boundary * \param canonical The canonical units we use for \c PhysicalQuantity this \c Unit relates to. \c nullptr means * this \c Unit is the canonical one (and therefore \c convertToCanonical and * \c convertFromCanonical are no-ops). (Note that the canonical units may or may not be in the @@ -75,12 +75,26 @@ namespace Measurement { * user isn't necessarily going to guess the exact abbreviation we've used, so it would be better to have * either a list of valid alternatives or, possibly, a list of regular expressions. */ + Unit(UnitSystem const & unitSystem, + QString const unitName, + double const multiplierToCanonical = 1.0, + Unit const * canonical = nullptr, + double const boundaryValue = 1.0); + + /** + * \brief Construct a type of unit when converting to/from canonical units requires more than a simple + * multiplication/division (eg as when converting °F to/from °C). Parameters are as for the other + * constructor, except as follows. + * + * \param convertToCanonical Converts a quantity of this \c Unit to a quantity of \c canonical \c Unit + * \param convertFromCanonical Converts a quantity of \c canonical \c Unit to a quantity of this \c Unit + */ Unit(UnitSystem const & unitSystem, QString const unitName, std::function convertToCanonical, std::function convertFromCanonical, - double boundaryValue, - Unit const * canonical = nullptr); + Unit const * canonical, + double const boundaryValue = 1.0); ~Unit(); @@ -229,6 +243,11 @@ namespace Measurement { //! This alias makes things a bit more concise eg in \c ObjectStore using UnitStringMapping = ObjectAddressStringMapping; + // + // We mostly use American spellings of words such as "liter" and "meter", on the grounds that more people speak this + // variant of English than ones where "litre" and "metre" are correct. If anyone ever cares enough, we could one day + // provide an option for which spellings we use in the UI. + // namespace Units { // === Mass === extern Unit const kilograms; @@ -259,6 +278,13 @@ namespace Measurement { extern Unit const imperial_tablespoons; extern Unit const imperial_teaspoons; + // === Length === + extern Unit const millimeters; + extern Unit const centimeters; + extern Unit const meters; + extern Unit const inches; + extern Unit const feet; + // === Count === extern Unit const numberOf; diff --git a/src/measurement/UnitSystem.cpp b/src/measurement/UnitSystem.cpp index acabbdb4..8b5535e7 100644 --- a/src/measurement/UnitSystem.cpp +++ b/src/measurement/UnitSystem.cpp @@ -28,6 +28,19 @@ #include "utils/EnumStringMapping.h" namespace { + /** + * \brief This is just a small function to create a QMap from an ordered list. TODO We should probably get rid of + * the whole map and just replace it with a vector, but that needs a bit more refactoring. + */ + QMap makeScaleToUnit(std::initializer_list unitEntriesSmallestToLargest) { + QMap scaleToUnit; + uint8_t index = 0; + for (Measurement::Unit const * unitEntry : unitEntriesSmallestToLargest) { + scaleToUnit.insert(static_cast(index++), unitEntry); + } + return scaleToUnit; + } + int const fieldWidth = 0; char const format = 'f'; int const defaultPrecision = 3; @@ -39,7 +52,7 @@ namespace { // .:TBD:. See if we can eliminate all this and get compile-time checking benefits // - // We sometimes want to be able to access RelativeScale enum values via a string name (eg code generated from .ui + // We sometimes want to be able to access RelativeScale enum values via a string name (eg in persistent settings // files) so it's useful to be able to map between them. There is some functionality built in to Qt to do this via // QMetaEnum, but this is at the cost of inheriting from QObject, which seems overkill here. Alternatively, we could // use Magic Enum C++ -- see https://github.com/Neargye/magic_enum -- at the cost of having to convert between @@ -52,6 +65,7 @@ namespace { {Measurement::UnitSystem::RelativeScale::Large , "scaleLarge" }, {Measurement::UnitSystem::RelativeScale::ExtraLarge, "scaleExtraLarge"}, {Measurement::UnitSystem::RelativeScale::Huge , "scaleHuge" }, + }; } @@ -65,13 +79,12 @@ class Measurement::UnitSystem::impl { Measurement::PhysicalQuantity const physicalQuantity, Measurement::Unit const * const thickness, Measurement::Unit const * const defaultUnit, - std::initializer_list > scaleToUnit) : + std::initializer_list unitEntriesSmallestToLargest) : self {self}, physicalQuantity{physicalQuantity}, thickness {thickness}, defaultUnit {defaultUnit}, - scaleToUnit {scaleToUnit} { + scaleToUnit {makeScaleToUnit(unitEntriesSmallestToLargest)} { return; } @@ -154,8 +167,7 @@ Measurement::UnitSystem::UnitSystem(Measurement::PhysicalQuantity const physical Measurement::Unit const * const defaultUnit, char const * const uniqueName, SystemOfMeasurement const systemOfMeasurement, - std::initializer_list > scaleToUnit, + std::initializer_list unitEntriesSmallestToLargest, Measurement::Unit const * const thickness) : uniqueName{uniqueName}, systemOfMeasurement{systemOfMeasurement}, @@ -163,7 +175,7 @@ Measurement::UnitSystem::UnitSystem(Measurement::PhysicalQuantity const physical physicalQuantity, thickness, defaultUnit, - scaleToUnit)} { + unitEntriesSmallestToLargest)} { // We assert that no other UnitSystem has the same name as this one Q_ASSERT(!nameToUnitSystem.contains(uniqueName)); nameToUnitSystem.insert(uniqueName, this); @@ -364,59 +376,74 @@ namespace Measurement::UnitSystems { &Measurement::Units::kilograms, "mass_Metric", Measurement::SystemOfMeasurement::Metric, - {{UnitSystem::RelativeScale::ExtraSmall, &Measurement::Units::milligrams}, - {UnitSystem::RelativeScale::Small, &Measurement::Units::grams }, - {UnitSystem::RelativeScale::Medium, &Measurement::Units::kilograms }}, + {&Measurement::Units::milligrams, + &Measurement::Units::grams , + &Measurement::Units::kilograms }, &Measurement::Units::kilograms}; UnitSystem const mass_UsCustomary{PhysicalQuantity::Mass, &Measurement::Units::pounds, "mass_UsCustomary", Measurement::SystemOfMeasurement::UsCustomary, - {{UnitSystem::RelativeScale::ExtraSmall, &Measurement::Units::ounces}, - {UnitSystem::RelativeScale::Small, &Measurement::Units::pounds}}, + {&Measurement::Units::ounces, + &Measurement::Units::pounds}, &Measurement::Units::pounds}; UnitSystem const mass_Imperial{PhysicalQuantity::Mass, &Measurement::Units::imperial_pounds, "mass_Imperial", Measurement::SystemOfMeasurement::Imperial, - {{UnitSystem::RelativeScale::ExtraSmall, &Measurement::Units::imperial_ounces}, - {UnitSystem::RelativeScale::Small, &Measurement::Units::imperial_pounds}}, + {&Measurement::Units::imperial_ounces, + &Measurement::Units::imperial_pounds}, &Measurement::Units::imperial_pounds}; UnitSystem const volume_Metric{PhysicalQuantity::Volume, &Measurement::Units::liters, "volume_Metric", Measurement::SystemOfMeasurement::Metric, - {{UnitSystem::RelativeScale::ExtraSmall, &Measurement::Units::milliliters}, - {UnitSystem::RelativeScale::Small, &Measurement::Units::liters }}, + {&Measurement::Units::milliliters, + &Measurement::Units::liters }, &Measurement::Units::liters}; UnitSystem const volume_UsCustomary{PhysicalQuantity::Volume, &Measurement::Units::us_gallons, "volume_UsCustomary", Measurement::SystemOfMeasurement::UsCustomary, - {{UnitSystem::RelativeScale::ExtraSmall, &Measurement::Units::us_teaspoons }, - {UnitSystem::RelativeScale::Small , &Measurement::Units::us_tablespoons}, - {UnitSystem::RelativeScale::Medium , &Measurement::Units::us_cups }, - {UnitSystem::RelativeScale::Large , &Measurement::Units::us_quarts }, - {UnitSystem::RelativeScale::ExtraLarge, &Measurement::Units::us_gallons }, - {UnitSystem::RelativeScale::Huge , &Measurement::Units::us_barrels }}, + {&Measurement::Units::us_teaspoons , + &Measurement::Units::us_tablespoons, + &Measurement::Units::us_cups , + &Measurement::Units::us_quarts , + &Measurement::Units::us_gallons , + &Measurement::Units::us_barrels }, &Measurement::Units::us_quarts}; UnitSystem const volume_Imperial{PhysicalQuantity::Volume, &Measurement::Units::imperial_gallons, "volume_Imperial", Measurement::SystemOfMeasurement::Imperial, - {{UnitSystem::RelativeScale::ExtraSmall, &Measurement::Units::imperial_teaspoons }, - {UnitSystem::RelativeScale::Small, &Measurement::Units::imperial_tablespoons}, - {UnitSystem::RelativeScale::Medium, &Measurement::Units::imperial_cups }, - {UnitSystem::RelativeScale::Large, &Measurement::Units::imperial_quarts }, - {UnitSystem::RelativeScale::ExtraLarge, &Measurement::Units::imperial_gallons }, - {UnitSystem::RelativeScale::Huge, &Measurement::Units::imperial_barrels }}, + {&Measurement::Units::imperial_teaspoons , + &Measurement::Units::imperial_tablespoons, + &Measurement::Units::imperial_cups , + &Measurement::Units::imperial_quarts , + &Measurement::Units::imperial_gallons , + &Measurement::Units::imperial_barrels }, &Measurement::Units::imperial_quarts}; + UnitSystem const length_Metric{PhysicalQuantity::Length, + &Measurement::Units::centimeters, + "length_Metric" , + Measurement::SystemOfMeasurement::Metric, + {&Measurement::Units::millimeters, + &Measurement::Units::centimeters, + &Measurement::Units::meters }}; + + UnitSystem const length_UsCustomary{PhysicalQuantity::Length, + &Measurement::Units::inches , + "length_UsCustomary", + Measurement::SystemOfMeasurement::UsCustomary, + {&Measurement::Units::inches, + &Measurement::Units::feet}}; + UnitSystem const count_NumberOf{PhysicalQuantity::Count, &Measurement::Units::numberOf, "count_NumberOf", @@ -436,10 +463,10 @@ namespace Measurement::UnitSystems { &Measurement::Units::minutes, "time_CoordinatedUniversalTime", Measurement::SystemOfMeasurement::UniversalStandard, - {{UnitSystem::RelativeScale::ExtraSmall, &Measurement::Units::seconds}, - {UnitSystem::RelativeScale::Small, &Measurement::Units::minutes}, - {UnitSystem::RelativeScale::Medium, &Measurement::Units::hours }, - {UnitSystem::RelativeScale::Large, &Measurement::Units::days }}}; + {&Measurement::Units::seconds, + &Measurement::Units::minutes, + &Measurement::Units::hours , + &Measurement::Units::days }}; UnitSystem const color_EuropeanBreweryConvention{PhysicalQuantity::Color, &Measurement::Units::ebc, @@ -503,9 +530,9 @@ namespace Measurement::UnitSystems { &Measurement::Units::partsPerMillionMass, "massFractionOrConc_Brewing", Measurement::SystemOfMeasurement::BrewingConcentration, - {{UnitSystem::RelativeScale::Small, &Measurement::Units::partsPerBillionMass}, - {UnitSystem::RelativeScale::Medium, &Measurement::Units::partsPerMillionMass}, - {UnitSystem::RelativeScale::Large , &Measurement::Units::milligramsPerLiter}}}; + {&Measurement::Units::partsPerBillionMass, + &Measurement::Units::partsPerMillionMass, + &Measurement::Units::milligramsPerLiter }}; UnitSystem const viscosity_Metric{PhysicalQuantity::Viscosity, &Measurement::Units::centipoise, @@ -536,17 +563,17 @@ namespace Measurement::UnitSystems { &Measurement::Units::litresPerKilogram, "specificVolume_Metric", Measurement::SystemOfMeasurement::Metric, - {{UnitSystem::RelativeScale::ExtraSmall, &Measurement::Units::litresPerKilogram }, - {UnitSystem::RelativeScale::Small , &Measurement::Units::cubicMetersPerKilogram}, - {UnitSystem::RelativeScale::Medium , &Measurement::Units::litresPerGram }}}; + {&Measurement::Units::litresPerKilogram , + &Measurement::Units::cubicMetersPerKilogram, + &Measurement::Units::litresPerGram }}; UnitSystem const specificVolume_UsCustomary{PhysicalQuantity::SpecificVolume, &Measurement::Units::us_quartsPerPound, "specificVolume_UsCustomary", Measurement::SystemOfMeasurement::UsCustomary, - {{UnitSystem::RelativeScale::ExtraSmall, &Measurement::Units::us_fluidOuncesPerOunce}, - {UnitSystem::RelativeScale::Small , &Measurement::Units::cubicFeetPerPound }, - {UnitSystem::RelativeScale::Medium , &Measurement::Units::us_gallonsPerPound }, - {UnitSystem::RelativeScale::Large , &Measurement::Units::us_quartsPerPound }, - {UnitSystem::RelativeScale::ExtraLarge, &Measurement::Units::us_gallonsPerOunce }}}; + {&Measurement::Units::us_fluidOuncesPerOunce, + &Measurement::Units::cubicFeetPerPound , + &Measurement::Units::us_gallonsPerPound , + &Measurement::Units::us_quartsPerPound , + &Measurement::Units::us_gallonsPerOunce }}; } diff --git a/src/measurement/UnitSystem.h b/src/measurement/UnitSystem.h index 6d385fd9..5e569937 100644 --- a/src/measurement/UnitSystem.h +++ b/src/measurement/UnitSystem.h @@ -73,19 +73,17 @@ namespace Measurement { * We only worry about units we actually use/permit, thus we don't, for example, care about where minims, * fluid drams, gills etc fit in on the imperial / US customary volume scales, as we don't support them. * - * NOTE: The names of these enums don't have any significance beyond a relative ordering. We usually start - * with \c ExtraSmall and then use as many of the subsequent entries as necessary. - * - * TODO: I feel this could be replaced with a \c uint8_t that holds the ordering in the \c UnitSystem - * constructor. Then we wouldn't need the pointless names like "ExtraSmall" and "Huge". + * NOTE: The names of these enums don't have any significance beyond a relative ordering. We automatically + * assign things starting with with \c Smallest and then use as many of the subsequent entries as + * necessary. */ - enum class RelativeScale { + enum class RelativeScale : uint8_t { ExtraSmall = 0, Small = 1, Medium = 2, Large = 3, ExtraLarge = 4, - Huge = 5 + Huge = 5, }; /*! @@ -95,15 +93,14 @@ namespace Measurement { * \param defaultUnit * \param uniqueName * \param systemOfMeasurement - * \param scaleToUnitEntries Will be empty if there is only one unit in this unit system + * \param unitEntriesSmallestToLargest Will be empty if there is only one unit in this unit system * \param thickness Used only for volume and mass unit systems, otherwise will be null */ UnitSystem(Measurement::PhysicalQuantity const physicalQuantity, Measurement::Unit const * const defaultUnit, char const * const uniqueName, SystemOfMeasurement const systemOfMeasurement = Measurement::SystemOfMeasurement::UniversalStandard, - std::initializer_list > scaleToUnit = {}, + std::initializer_list unitEntriesSmallestToLargest = {}, Measurement::Unit const * const thickness = nullptr); ~UnitSystem(); @@ -261,6 +258,13 @@ namespace Measurement { extern UnitSystem const volume_UsCustomary; extern UnitSystem const volume_Metric; + // For length, at the scales we care about (feet and inches), the US customary and the British imperial system are + // identical. Rather than have two sorts of inches (eg inches and imperial_inches), we'll just pick one. Non- + // metric units are more entrenched in the US than anywhere else, so we'll go with US customary. If we change our + // minds, we can always add imperial_inches later. + extern UnitSystem const length_UsCustomary; + extern UnitSystem const length_Metric; + extern UnitSystem const count_NumberOf; extern UnitSystem const temperature_MetricIsCelsius; diff --git a/src/model/Boil.cpp b/src/model/Boil.cpp index 7d203ae9..9ce886ee 100644 --- a/src/model/Boil.cpp +++ b/src/model/Boil.cpp @@ -22,6 +22,7 @@ #include "model/Mash.h" #include "model/MashStep.h" #include "model/NamedParameterBundle.h" +#include "utils/AutoCompare.h" QString Boil::localisedName() { return tr("Boil"); } @@ -30,10 +31,9 @@ bool Boil::isEqualTo(NamedEntity const & other) const { Boil const & rhs = static_cast(other); // Base class will already have ensured names are equal return ( - this->m_description == rhs.m_description && - this->m_notes == rhs.m_notes && - this->m_preBoilSize_l == rhs.m_preBoilSize_l && - this->m_boilTime_mins == rhs.m_boilTime_mins + Utils::AutoCompare(this->m_description , rhs.m_description ) && + Utils::AutoCompare(this->m_notes , rhs.m_notes ) && + Utils::AutoCompare(this->m_preBoilSize_l, rhs.m_preBoilSize_l) // .:TBD:. Should we check BoilSteps too? ); } @@ -48,8 +48,8 @@ TypeLookup const Boil::typeLookup { PROPERTY_TYPE_LOOKUP_ENTRY(PropertyNames::Boil::description , Boil::m_description , NonPhysicalQuantity::String), PROPERTY_TYPE_LOOKUP_ENTRY(PropertyNames::Boil::notes , Boil::m_notes , NonPhysicalQuantity::String), PROPERTY_TYPE_LOOKUP_ENTRY(PropertyNames::Boil::preBoilSize_l, Boil::m_preBoilSize_l, Measurement::PhysicalQuantity::Volume), - PROPERTY_TYPE_LOOKUP_ENTRY(PropertyNames::Boil::boilTime_mins, Boil::m_boilTime_mins, Measurement::PhysicalQuantity::Time ), + PROPERTY_TYPE_LOOKUP_ENTRY_NO_MV(PropertyNames::Boil::boilTime_mins, Boil::boilTime_mins, Measurement::PhysicalQuantity::Time), PROPERTY_TYPE_LOOKUP_ENTRY_NO_MV(PropertyNames::Boil::boilSteps, Boil::boilSteps), }, // Parent classes lookup @@ -66,8 +66,7 @@ Boil::Boil(QString name) : StepOwnerBase{}, m_description {"" }, m_notes {"" }, - m_preBoilSize_l{std::nullopt}, - m_boilTime_mins{0.0 } { + m_preBoilSize_l{std::nullopt} { return; } @@ -77,8 +76,7 @@ Boil::Boil(NamedParameterBundle const & namedParameterBundle) : StepOwnerBase{}, SET_REGULAR_FROM_NPB (m_description , namedParameterBundle, PropertyNames::Boil::description ), SET_REGULAR_FROM_NPB (m_notes , namedParameterBundle, PropertyNames::Boil::notes ), - SET_REGULAR_FROM_NPB (m_preBoilSize_l, namedParameterBundle, PropertyNames::Boil::preBoilSize_l), - SET_REGULAR_FROM_NPB (m_boilTime_mins, namedParameterBundle, PropertyNames::Boil::boilTime_mins) { + SET_REGULAR_FROM_NPB (m_preBoilSize_l, namedParameterBundle, PropertyNames::Boil::preBoilSize_l) { return; } @@ -88,8 +86,7 @@ Boil::Boil(Boil const & other) : StepOwnerBase{other}, m_description {other.m_description }, m_notes {other.m_notes }, - m_preBoilSize_l{other.m_preBoilSize_l}, - m_boilTime_mins{other.m_boilTime_mins} { + m_preBoilSize_l{other.m_preBoilSize_l} { return; } @@ -99,16 +96,64 @@ Boil::~Boil() = default; QString Boil::description () const { return this->m_description ; } QString Boil::notes () const { return this->m_notes ; } std::optional Boil::preBoilSize_l() const { return this->m_preBoilSize_l; } -double Boil::boilTime_mins() const { return this->m_boilTime_mins; } + +double Boil::boilTime_mins() const { + double boilTimeProper_mins = 0.0; + for (auto const & step : this->steps()) { + if (step->startTemp_c() && *step->startTemp_c() > Boil::minimumBoilTemperature_c && + step-> endTemp_c() && *step-> endTemp_c() > Boil::minimumBoilTemperature_c && + step->stepTime_mins()) { + boilTimeProper_mins += *step->stepTime_mins(); + } + } + return boilTimeProper_mins; +} + +std::optional Boil::coolTime_mins() const { + auto boilSteps = this->steps(); + if (boilSteps.size() > 0 && + boilSteps.last()->startTemp_c() > Boil::minimumBoilTemperature_c && + boilSteps.last()->endTemp_c () < Boil::minimumBoilTemperature_c) { + return boilSteps.last()->stepTime_mins(); + } + return std::nullopt; +} + //============================================= "SETTER" MEMBER FUNCTIONS ============================================== void Boil::setDescription (QString const & val) { SET_AND_NOTIFY(PropertyNames::Boil::description , this->m_description , val); return; } void Boil::setNotes (QString const & val) { SET_AND_NOTIFY(PropertyNames::Boil::notes , this->m_notes , val); return; } void Boil::setPreBoilSize_l(std::optional const val) { SET_AND_NOTIFY(PropertyNames::Boil::preBoilSize_l, this->m_preBoilSize_l, val); return; } -void Boil::setBoilTime_mins(double const val) { SET_AND_NOTIFY(PropertyNames::Boil::boilTime_mins, this->m_boilTime_mins, val); return; } + +// +// This is only used by BeerXML processing, so we can be a bit fast and loose. In particular, we assume the first step +// we find that is a proper boil is also the only such step. +// +void Boil::setBoilTime_mins(double const val) { + this->ensureStandardProfile(); + for (auto step : this->steps()) { + if (step->startTemp_c() && *step->startTemp_c() > Boil::minimumBoilTemperature_c && + step-> endTemp_c() && *step-> endTemp_c() > Boil::minimumBoilTemperature_c) { + step->setStepTime_mins(val); + break; + } + } + return; +} void Boil::acceptStepChange([[maybe_unused]] QMetaProperty prop, [[maybe_unused]] QVariant val) { + BoilStep * stepSender = qobject_cast(sender()); + if (!stepSender) { + return; + } + + // If one of our steps changed, our pseudo properties may also change, so we need to emit some signals + if (stepSender->ownerId() == this->key()) { + emit changed(metaProperty(*PropertyNames::Boil::boilTime_mins), QVariant()); + emit changed(metaProperty(*PropertyNames::Boil::boilSteps ), QVariant()); + } + return; } diff --git a/src/model/Boil.h b/src/model/Boil.h index e2330e93..5313b9b0 100644 --- a/src/model/Boil.h +++ b/src/model/Boil.h @@ -32,11 +32,11 @@ //========================================== Start of property name constants ========================================== // See comment in model/NamedEntity.h #define AddPropertyName(property) namespace PropertyNames::Boil { BtStringConst const property{#property}; } -AddPropertyName(description ) -AddPropertyName(notes ) -AddPropertyName(preBoilSize_l ) -AddPropertyName(boilTime_mins ) -AddPropertyName(boilSteps ) +AddPropertyName(description ) +AddPropertyName(notes ) +AddPropertyName(preBoilSize_l) +AddPropertyName(boilTime_mins) +AddPropertyName(boilSteps ) #undef AddPropertyName //=========================================== End of property name constants =========================================== //====================================================================================================================== @@ -57,6 +57,9 @@ AddPropertyName(boilSteps ) * * Additionally, there is a short-term benefit, which is that we can share a lot of the logic between * MashStep and BoilStep, which saves us duplicating code. + * + * Our \c Boil class maps closely to a BeerJSON "boil procedure", with the exception that, in BeerJSON "a + * boil procedure with no steps is the same as a standard single step boil." */ class Boil : public NamedEntity, public FolderBase, @@ -77,6 +80,7 @@ class Boil : public NamedEntity, * info. */ static TypeLookup const typeLookup; + TYPE_LOOKUP_GETTER Boil(QString name = ""); Boil(NamedParameterBundle const & namedParameterBundle); @@ -106,12 +110,28 @@ class Boil : public NamedEntity, //=================================================== PROPERTIES ==================================================== //! \brief Folder. See model/FolderBase for implementation of the getter & setter. - Q_PROPERTY(QString folder READ folder WRITE setFolder) - Q_PROPERTY(QString description READ description WRITE setDescription ) - Q_PROPERTY(QString notes READ notes WRITE setNotes ) + Q_PROPERTY(QString folder READ folder WRITE setFolder ) + Q_PROPERTY(QString description READ description WRITE setDescription) + Q_PROPERTY(QString notes READ notes WRITE setNotes ) + + /** + * \brief This is optional because it's optional in BeerJSON. The equivalent field in BeerXML (BOIL_SIZE on RECIPE) + * is a required field. + * + * Note however, that Recipe::batchSize_l is a required field in both BeerJSON and BeerXML, so callers should + * fall back to that as a "better than nothing" value when this one is \c std::nullopt. + */ Q_PROPERTY(std::optional preBoilSize_l READ preBoilSize_l WRITE setPreBoilSize_l ) - //! \brief The total time to boil the wort. Hopefully equal to the sum of the times of all the steps - Q_PROPERTY(double boilTime_mins READ boilTime_mins WRITE setBoilTime_mins ) + + /** + * \brief In BeerXML, this is "The total time to boil the wort in minutes". I think this would mostly be understood + * as the length of the "boil proper" so, in our new model, it is the length of the step(s) for which + * \c Step::startTemp_c and \c Step::endTemp_c are above \c minimumBoilTemperature_c. In other words, it is + * a convenience way of accessing the length of the boil excluding any ramp-up or cool-down steps. + * + * TBD: It's possible we should make this optional if we desire to support "no boil" recipes in future. + */ + Q_PROPERTY(double boilTime_mins READ boilTime_mins WRITE setBoilTime_mins STORED false) /** * \brief The individual boil steps. (See \c StepOwnerBase for getter/setter implementation.) * Technically this is optional in BeerJSON, but we'll treat it as required (for consistency with @@ -126,6 +146,12 @@ class Boil : public NamedEntity, std::optional preBoilSize_l() const; double boilTime_mins() const; + /** + * \brief If there is a post-boil cooling step, and if it has a duration, this will return it, otherwise returns + * \c std::nullopt + */ + std::optional coolTime_mins() const; + //============================================ "SETTER" MEMBER FUNCTIONS ============================================ void setDescription (QString const & val); void setNotes (QString const & val); @@ -160,7 +186,6 @@ public slots: QString m_description ; QString m_notes ; std::optional m_preBoilSize_l; - double m_boilTime_mins; }; BT_DECLARE_METATYPES(Boil) diff --git a/src/model/BoilStep.h b/src/model/BoilStep.h index fc96a5d8..7c416886 100644 --- a/src/model/BoilStep.h +++ b/src/model/BoilStep.h @@ -78,6 +78,7 @@ class BoilStep : public StepExtended, public StepBase { * info. */ static TypeLookup const typeLookup; + TYPE_LOOKUP_GETTER BoilStep(QString name = ""); BoilStep(NamedParameterBundle const & namedParameterBundle); diff --git a/src/model/BrewNote.cpp b/src/model/BrewNote.cpp index 9fe49d29..7978156f 100644 --- a/src/model/BrewNote.cpp +++ b/src/model/BrewNote.cpp @@ -299,7 +299,7 @@ void BrewNote::populateNote(Recipe * parent) { auto const yeastAdditions = parent->yeastAdditions(); for (auto const & yeastAddition : yeastAdditions) { if (yeastAddition->attenuation_pct() > atten_pct ) { - atten_pct = yeastAddition->yeast()->getTypicalAttenuation_pct(); + atten_pct = yeastAddition->yeast()->attenuationTypical_pct(); } } diff --git a/src/model/BrewNote.h b/src/model/BrewNote.h index f3e0e4f7..7270c733 100644 --- a/src/model/BrewNote.h +++ b/src/model/BrewNote.h @@ -94,6 +94,7 @@ class BrewNote : public OwnedByRecipe { * info. */ static TypeLookup const typeLookup; + TYPE_LOOKUP_GETTER BrewNote(QString name = ""); BrewNote(Recipe const & recipe); diff --git a/src/model/Equipment.cpp b/src/model/Equipment.cpp index 351d885e..b732c001 100644 --- a/src/model/Equipment.cpp +++ b/src/model/Equipment.cpp @@ -23,6 +23,7 @@ #include "HeatCalculations.h" #include "model/NamedParameterBundle.h" #include "model/Recipe.h" +#include "utils/AutoCompare.h" QString Equipment::localisedName() { return tr("Equipment"); } @@ -31,19 +32,28 @@ bool Equipment::isEqualTo(NamedEntity const & other) const { Equipment const & rhs = static_cast(other); // Base class will already have ensured names are equal return ( - this->m_kettleBoilSize_l == rhs.m_kettleBoilSize_l && - this->m_fermenterBatchSize_l == rhs.m_fermenterBatchSize_l && - this->m_mashTunVolume_l == rhs.m_mashTunVolume_l && - this->m_mashTunWeight_kg == rhs.m_mashTunWeight_kg && - this->m_mashTunSpecificHeat_calGC == rhs.m_mashTunSpecificHeat_calGC && - this->m_topUpWater_l == rhs.m_topUpWater_l && - this->m_kettleTrubChillerLoss_l == rhs.m_kettleTrubChillerLoss_l && - this->m_evapRate_pctHr == rhs.m_evapRate_pctHr && - this->m_kettleEvaporationPerHour_l == rhs.m_kettleEvaporationPerHour_l && - this->m_boilTime_min == rhs.m_boilTime_min && - this->m_lauterTunDeadspaceLoss_l == rhs.m_lauterTunDeadspaceLoss_l && - this->m_topUpKettle_l == rhs.m_topUpKettle_l && - this->m_hopUtilization_pct == rhs.m_hopUtilization_pct + // + // .:TBC:. We ought to do fuzzy compare here, which probably means + // + Utils::AutoCompare(this->m_kettleBoilSize_l , rhs.m_kettleBoilSize_l ) && + Utils::AutoCompare(this->m_fermenterBatchSize_l , rhs.m_fermenterBatchSize_l ) && + Utils::AutoCompare(this->m_mashTunVolume_l , rhs.m_mashTunVolume_l ) && + Utils::AutoCompare(this->m_mashTunWeight_kg , rhs.m_mashTunWeight_kg ) && + Utils::AutoCompare(this->m_mashTunSpecificHeat_calGC , rhs.m_mashTunSpecificHeat_calGC ) && + Utils::AutoCompare(this->m_topUpWater_l , rhs.m_topUpWater_l ) && + Utils::AutoCompare(this->m_kettleTrubChillerLoss_l , rhs.m_kettleTrubChillerLoss_l ) && + Utils::AutoCompare(this->m_evapRate_pctHr , rhs.m_evapRate_pctHr ) && + Utils::AutoCompare(this->m_kettleEvaporationPerHour_l, rhs.m_kettleEvaporationPerHour_l) && + Utils::AutoCompare(this->m_boilTime_min , rhs.m_boilTime_min ) && + Utils::AutoCompare(this->m_calcBoilVolume , rhs.m_calcBoilVolume ) && + Utils::AutoCompare(this->m_lauterTunDeadspaceLoss_l , rhs.m_lauterTunDeadspaceLoss_l ) && + Utils::AutoCompare(this->m_topUpKettle_l , rhs.m_topUpKettle_l ) && + Utils::AutoCompare(this->m_hopUtilization_pct , rhs.m_hopUtilization_pct ) && + Utils::AutoCompare(this->m_kettleNotes , rhs.m_kettleNotes ) && + Utils::AutoCompare(this->m_mashTunGrainAbsorption_LKg, rhs.m_mashTunGrainAbsorption_LKg) && + Utils::AutoCompare(this->m_boilingPoint_c , rhs.m_boilingPoint_c ) && + Utils::AutoCompare(this->m_kettleInternalDiameter_cm , rhs.m_kettleInternalDiameter_cm ) && + Utils::AutoCompare(this->m_kettleOpeningDiameter_cm , rhs.m_kettleOpeningDiameter_cm ) ); } @@ -65,12 +75,14 @@ TypeLookup const Equipment::typeLookup { PROPERTY_TYPE_LOOKUP_ENTRY(PropertyNames::Equipment::kettleEvaporationPerHour_l , Equipment::m_kettleEvaporationPerHour_l , Measurement::PhysicalQuantity::Volume ), // The "per hour" bit is fixed, so we simplify PROPERTY_TYPE_LOOKUP_ENTRY(PropertyNames::Equipment::boilTime_min , Equipment::m_boilTime_min , Measurement::PhysicalQuantity::Time ), PROPERTY_TYPE_LOOKUP_ENTRY(PropertyNames::Equipment::calcBoilVolume , Equipment::m_calcBoilVolume , NonPhysicalQuantity::Bool ), - PROPERTY_TYPE_LOOKUP_ENTRY(PropertyNames::Equipment::lauterTunDeadspaceLoss_l , Equipment::m_lauterTunDeadspaceLoss_l , Measurement::PhysicalQuantity::Volume ), + PROPERTY_TYPE_LOOKUP_ENTRY(PropertyNames::Equipment::lauterTunDeadspaceLoss_l , Equipment::m_lauterTunDeadspaceLoss_l , Measurement::PhysicalQuantity::Volume ), PROPERTY_TYPE_LOOKUP_ENTRY(PropertyNames::Equipment::topUpKettle_l , Equipment::m_topUpKettle_l , Measurement::PhysicalQuantity::Volume ), PROPERTY_TYPE_LOOKUP_ENTRY(PropertyNames::Equipment::hopUtilization_pct , Equipment::m_hopUtilization_pct , NonPhysicalQuantity::Percentage ), PROPERTY_TYPE_LOOKUP_ENTRY(PropertyNames::Equipment::kettleNotes , Equipment::m_kettleNotes , NonPhysicalQuantity::String ), PROPERTY_TYPE_LOOKUP_ENTRY(PropertyNames::Equipment::mashTunGrainAbsorption_LKg , Equipment::m_mashTunGrainAbsorption_LKg , Measurement::PhysicalQuantity::SpecificVolume ), PROPERTY_TYPE_LOOKUP_ENTRY(PropertyNames::Equipment::boilingPoint_c , Equipment::m_boilingPoint_c , Measurement::PhysicalQuantity::Temperature ), + PROPERTY_TYPE_LOOKUP_ENTRY(PropertyNames::Equipment::kettleInternalDiameter_cm , Equipment::m_kettleInternalDiameter_cm , Measurement::PhysicalQuantity::Length ), + PROPERTY_TYPE_LOOKUP_ENTRY(PropertyNames::Equipment::kettleOpeningDiameter_cm , Equipment::m_kettleOpeningDiameter_cm , Measurement::PhysicalQuantity::Length ), // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ PROPERTY_TYPE_LOOKUP_ENTRY(PropertyNames::Equipment::hltType , Equipment::m_hltType , NonPhysicalQuantity::String ), PROPERTY_TYPE_LOOKUP_ENTRY(PropertyNames::Equipment::mashTunType , Equipment::m_mashTunType , NonPhysicalQuantity::String ), @@ -129,6 +141,8 @@ Equipment::Equipment(QString name) : m_kettleNotes {"" }, m_mashTunGrainAbsorption_LKg {std::nullopt}, // Previously 1.086 m_boilingPoint_c {100.0 }, + m_kettleInternalDiameter_cm {std::nullopt}, + m_kettleOpeningDiameter_cm {std::nullopt}, // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ m_hltType {"" }, m_mashTunType {"" }, @@ -167,6 +181,8 @@ Equipment::Equipment(QString name) : // - kettleEvaporationPerHour_l // - mashTunGrainAbsorption_LKg // - boilingPoint_c +// - kettleInternalDiameter_cm +// - kettleOpeningDiameter_cm // Equipment::Equipment(NamedParameterBundle const & namedParameterBundle) : NamedEntity{namedParameterBundle}, @@ -188,6 +204,8 @@ Equipment::Equipment(NamedParameterBundle const & namedParameterBundle) : SET_REGULAR_FROM_NPB (m_kettleNotes , namedParameterBundle, PropertyNames::Equipment::kettleNotes ), SET_REGULAR_FROM_NPB (m_mashTunGrainAbsorption_LKg , namedParameterBundle, PropertyNames::Equipment::mashTunGrainAbsorption_LKg , 1.086), SET_REGULAR_FROM_NPB (m_boilingPoint_c , namedParameterBundle, PropertyNames::Equipment::boilingPoint_c , 100.0), + SET_REGULAR_FROM_NPB (m_kettleInternalDiameter_cm , namedParameterBundle, PropertyNames::Equipment::kettleInternalDiameter_cm , std::nullopt), + SET_REGULAR_FROM_NPB (m_kettleOpeningDiameter_cm , namedParameterBundle, PropertyNames::Equipment::kettleOpeningDiameter_cm , std::nullopt), // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ SET_REGULAR_FROM_NPB (m_hltType , namedParameterBundle, PropertyNames::Equipment::hltType ), SET_REGULAR_FROM_NPB (m_mashTunType , namedParameterBundle, PropertyNames::Equipment::mashTunType ), @@ -222,7 +240,7 @@ Equipment::Equipment(NamedParameterBundle const & namedParameterBundle) : } Equipment::Equipment(Equipment const & other) : - NamedEntity {other }, + NamedEntity {other}, FolderBase{other}, m_kettleBoilSize_l {other.m_kettleBoilSize_l }, m_fermenterBatchSize_l {other.m_fermenterBatchSize_l }, @@ -235,12 +253,14 @@ Equipment::Equipment(Equipment const & other) : m_kettleEvaporationPerHour_l {other.m_kettleEvaporationPerHour_l }, m_boilTime_min {other.m_boilTime_min }, m_calcBoilVolume {other.m_calcBoilVolume }, - m_lauterTunDeadspaceLoss_l {other.m_lauterTunDeadspaceLoss_l }, + m_lauterTunDeadspaceLoss_l {other.m_lauterTunDeadspaceLoss_l }, m_topUpKettle_l {other.m_topUpKettle_l }, m_hopUtilization_pct {other.m_hopUtilization_pct }, m_kettleNotes {other.m_kettleNotes }, m_mashTunGrainAbsorption_LKg {other.m_mashTunGrainAbsorption_LKg }, m_boilingPoint_c {other.m_boilingPoint_c }, + m_kettleInternalDiameter_cm {other.m_kettleInternalDiameter_cm }, + m_kettleOpeningDiameter_cm {other.m_kettleOpeningDiameter_cm }, // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ m_hltType {other.m_hltType }, m_mashTunType {other.m_mashTunType }, @@ -289,12 +309,14 @@ std::optional Equipment::evapRate_pctHr () const { return m_ std::optional Equipment::kettleEvaporationPerHour_l () const { return m_kettleEvaporationPerHour_l ; } std::optional Equipment::boilTime_min () const { return m_boilTime_min ; } bool Equipment::calcBoilVolume () const { return m_calcBoilVolume ; } -double Equipment::lauterTunDeadspaceLoss_l () const { return m_lauterTunDeadspaceLoss_l ; } +double Equipment::lauterTunDeadspaceLoss_l () const { return m_lauterTunDeadspaceLoss_l ; } std::optional Equipment::topUpKettle_l () const { return m_topUpKettle_l ; } std::optional Equipment::hopUtilization_pct () const { return m_hopUtilization_pct ; } QString Equipment::kettleNotes () const { return m_kettleNotes ; } std::optional Equipment::mashTunGrainAbsorption_LKg () const { return m_mashTunGrainAbsorption_LKg ; } double Equipment::boilingPoint_c () const { return m_boilingPoint_c ; } +std::optional Equipment::kettleInternalDiameter_cm () const { return m_kettleInternalDiameter_cm ; } +std::optional Equipment::kettleOpeningDiameter_cm () const { return m_kettleOpeningDiameter_cm ; } // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ QString Equipment::hltType () const { return m_hltType ; } QString Equipment::mashTunType () const { return m_mashTunType ; } @@ -370,13 +392,14 @@ void Equipment::setKettleEvaporationPerHour_l(std::optional const val) { void Equipment::setBoilTime_min (std::optional const val) { if (SET_AND_NOTIFY(PropertyNames::Equipment::boilTime_min , this->m_boilTime_min , this->enforceMin(val, "boil time"))) { doCalculations(); } return; } void Equipment::setCalcBoilVolume (bool const val) { SET_AND_NOTIFY(PropertyNames::Equipment::calcBoilVolume , this->m_calcBoilVolume , val); if ( val ) { doCalculations(); } } -void Equipment::setLauterTunDeadspaceLoss_l (double const val) { SET_AND_NOTIFY(PropertyNames::Equipment::lauterTunDeadspaceLoss_l , this->m_lauterTunDeadspaceLoss_l , this->enforceMin(val, "deadspace")); } +void Equipment::setLauterTunDeadspaceLoss_l (double const val) { SET_AND_NOTIFY(PropertyNames::Equipment::lauterTunDeadspaceLoss_l , this->m_lauterTunDeadspaceLoss_l , this->enforceMin(val, "deadspace")); } void Equipment::setTopUpKettle_l (std::optional const val) { SET_AND_NOTIFY(PropertyNames::Equipment::topUpKettle_l , this->m_topUpKettle_l , this->enforceMin(val, "top-up kettle")); } void Equipment::setHopUtilization_pct (std::optional const val) { SET_AND_NOTIFY(PropertyNames::Equipment::hopUtilization_pct , this->m_hopUtilization_pct , this->enforceMin(val, "hop utilization")); } void Equipment::setKettleNotes (QString const & val) { SET_AND_NOTIFY(PropertyNames::Equipment::kettleNotes , this->m_kettleNotes , val); } void Equipment::setMashTunGrainAbsorption_LKg (std::optional const val) { SET_AND_NOTIFY(PropertyNames::Equipment::mashTunGrainAbsorption_LKg, this->m_mashTunGrainAbsorption_LKg, this->enforceMin(val, "absorption")); } void Equipment::setBoilingPoint_c (double const val) { SET_AND_NOTIFY(PropertyNames::Equipment::boilingPoint_c , this->m_boilingPoint_c , this->enforceMin(val, "boiling point of water")); } - +void Equipment::setKettleInternalDiameter_cm (std::optional const val) { SET_AND_NOTIFY(PropertyNames::Equipment::kettleInternalDiameter_cm , this->m_kettleInternalDiameter_cm , val); } +void Equipment::setKettleOpeningDiameter_cm (std::optional const val) { SET_AND_NOTIFY(PropertyNames::Equipment::kettleOpeningDiameter_cm , this->m_kettleOpeningDiameter_cm , val); } // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ void Equipment::setHltType (QString const & val) { SET_AND_NOTIFY(PropertyNames::Equipment::hltType , this->m_hltType , val); } void Equipment::setMashTunType (QString const & val) { SET_AND_NOTIFY(PropertyNames::Equipment::mashTunType , this->m_mashTunType , val); } @@ -415,10 +438,13 @@ void Equipment::doCalculations() { return; } - this->setKettleBoilSize_l(this->fermenterBatchSize_l() - - this->topUpWater_l().value_or(Equipment::default_topUpWater_l) + - this->kettleTrubChillerLoss_l() + - (this->boilTime_min().value_or(Equipment::default_boilTime_mins)/(double)60)*this->kettleEvaporationPerHour_l().value_or(Equipment::default_kettleEvaporationPerHour_l)); + this->setKettleBoilSize_l( + this->fermenterBatchSize_l() - + this->topUpWater_l().value_or(Equipment::default_topUpWater_l) + + this->kettleTrubChillerLoss_l() + + (this->boilTime_min().value_or(Equipment::default_boilTime_mins) / 60.0) * + this->kettleEvaporationPerHour_l().value_or(Equipment::default_kettleEvaporationPerHour_l) + ); return; } diff --git a/src/model/Equipment.h b/src/model/Equipment.h index 9f23fc25..eaefa53f 100644 --- a/src/model/Equipment.h +++ b/src/model/Equipment.h @@ -54,8 +54,10 @@ AddPropertyName(hltType ) AddPropertyName(hltVolume_l ) AddPropertyName(hltWeight_kg ) AddPropertyName(hopUtilization_pct ) +AddPropertyName(kettleInternalDiameter_cm ) AddPropertyName(kettleEvaporationPerHour_l ) AddPropertyName(kettleNotes ) +AddPropertyName(kettleOpeningDiameter_cm ) AddPropertyName(kettleOutflowPerMinute_l ) AddPropertyName(kettleSpecificHeat_calGC ) AddPropertyName(kettleType ) @@ -135,6 +137,7 @@ class Equipment : public NamedEntity, * info. */ static TypeLookup const typeLookup; + TYPE_LOOKUP_GETTER Equipment(QString name = ""); Equipment(NamedParameterBundle const & namedParameterBundle); @@ -146,7 +149,7 @@ class Equipment : public NamedEntity, * \brief Some default values we use in calculations when no value is set in this record */ //! @{ - static constexpr double default_boilTime_mins = 60.0; + static constexpr double default_boilTime_mins = 60.0; static constexpr double default_hopUtilization_pct = 100.0; static constexpr double default_kettleEvaporationPerHour_l = 4.0; static constexpr double default_mashTunGrainAbsorption_LKg = 1.086; // See also PhysicalConstants::grainAbsorption_Lkg @@ -280,6 +283,19 @@ class Equipment : public NamedEntity, */ Q_PROPERTY(double boilingPoint_c READ boilingPoint_c WRITE setBoilingPoint_c ) + + /** + * \brief The interior diameter of the kettle at the surface of the wort. Used to calculate the surface area of wort + * exposed to air, eg for mIBU calculation. NB: Not part of BeerXML or BeerJSON + */ + Q_PROPERTY(std::optional kettleInternalDiameter_cm READ kettleInternalDiameter_cm WRITE setKettleInternalDiameter_cm ) + + /** + * \brief The interior diameter of the opening in the kettle through which steam can escape. Used in mIBU + * calculation. NB: Not part of BeerXML or BeerJSON + */ + Q_PROPERTY(std::optional kettleOpeningDiameter_cm READ kettleOpeningDiameter_cm WRITE setKettleOpeningDiameter_cm ) + // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ Q_PROPERTY(QString hltType READ hltType WRITE setHltType ) @@ -337,17 +353,19 @@ class Equipment : public NamedEntity, std::optional mashTunWeight_kg () const; std::optional mashTunSpecificHeat_calGC () const; std::optional topUpWater_l () const; - double kettleTrubChillerLoss_l () const; + double kettleTrubChillerLoss_l () const; std::optional evapRate_pctHr () const; std::optional kettleEvaporationPerHour_l () const; std::optional boilTime_min () const; bool calcBoilVolume () const; - double lauterTunDeadspaceLoss_l () const; + double lauterTunDeadspaceLoss_l () const; std::optional topUpKettle_l () const; std::optional hopUtilization_pct () const; QString kettleNotes () const; std::optional mashTunGrainAbsorption_LKg () const; double boilingPoint_c () const; + std::optional kettleInternalDiameter_cm () const; + std::optional kettleOpeningDiameter_cm () const; // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ QString hltType () const; QString mashTunType () const; @@ -397,6 +415,8 @@ class Equipment : public NamedEntity, void setKettleNotes (QString const & val); void setMashTunGrainAbsorption_LKg (std::optional const val); void setBoilingPoint_c (double const val); + void setKettleInternalDiameter_cm (std::optional const val); + void setKettleOpeningDiameter_cm (std::optional const val); // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ void setHltType (QString const & val); void setMashTunType (QString const & val); @@ -457,12 +477,14 @@ class Equipment : public NamedEntity, std::optional m_kettleEvaporationPerHour_l; std::optional m_boilTime_min ; bool m_calcBoilVolume ; - double m_lauterTunDeadspaceLoss_l ; + double m_lauterTunDeadspaceLoss_l ; std::optional m_topUpKettle_l ; std::optional m_hopUtilization_pct ; QString m_kettleNotes ; std::optional m_mashTunGrainAbsorption_LKg; double m_boilingPoint_c ; + std::optional m_kettleInternalDiameter_cm ; + std::optional m_kettleOpeningDiameter_cm ; // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ QString m_hltType ; QString m_mashTunType ; diff --git a/src/model/Fermentable.cpp b/src/model/Fermentable.cpp index 28e88b6f..4f7fc65e 100644 --- a/src/model/Fermentable.cpp +++ b/src/model/Fermentable.cpp @@ -32,6 +32,7 @@ #include "model/InventoryFermentable.h" #include "model/NamedParameterBundle.h" #include "model/Recipe.h" +#include "utils/AutoCompare.h" #include "utils/OptionalHelpers.h" QString Fermentable::localisedName() { return tr("Fermentable"); } @@ -89,20 +90,20 @@ bool Fermentable::isEqualTo(NamedEntity const & other) const { bool const outlinesAreEqual{ // "Outline" fields: In BeerJSON, all these fields are in the FermentableBase type - this->m_type == rhs.m_type && - this->m_origin == rhs.m_origin && - this->m_color_srm == rhs.m_color_srm && - this->m_producer == rhs.m_producer && - this->m_productId == rhs.m_productId && - this->m_grainGroup == rhs.m_grainGroup && + Utils::AutoCompare(this->m_type , rhs.m_type ) && + Utils::AutoCompare(this->m_origin , rhs.m_origin ) && + Utils::AutoCompare(this->m_color_srm , rhs.m_color_srm ) && + Utils::AutoCompare(this->m_producer , rhs.m_producer ) && + Utils::AutoCompare(this->m_productId , rhs.m_productId ) && + Utils::AutoCompare(this->m_grainGroup, rhs.m_grainGroup) && // Yield - this->m_fineGrindYield_pct == rhs.m_fineGrindYield_pct && - this->m_coarseGrindYield_pct == rhs.m_coarseGrindYield_pct && - this->m_coarseFineDiff_pct == rhs.m_coarseFineDiff_pct && - this->m_potentialYield_sg == rhs.m_potentialYield_sg && + Utils::AutoCompare(this->m_fineGrindYield_pct , rhs.m_fineGrindYield_pct ) && + Utils::AutoCompare(this->m_coarseGrindYield_pct, rhs.m_coarseGrindYield_pct) && + Utils::AutoCompare(this->m_coarseFineDiff_pct , rhs.m_coarseFineDiff_pct ) && + Utils::AutoCompare(this->m_potentialYield_sg , rhs.m_potentialYield_sg ) && - this->m_color_srm == rhs.m_color_srm + Utils::AutoCompare(this->m_color_srm , rhs.m_color_srm) }; // If either object is an outline (see comment in model/OutlineableNamedEntity.h) then there is no point comparing @@ -115,29 +116,29 @@ bool Fermentable::isEqualTo(NamedEntity const & other) const { outlinesAreEqual && // Remaining BeerJSON fields -- excluding inventories - this->m_notes == rhs.m_notes && - this->m_moisture_pct == rhs.m_moisture_pct && - this->m_alphaAmylase_dextUnits == rhs.m_alphaAmylase_dextUnits && - this->m_diastaticPower_lintner == rhs.m_diastaticPower_lintner && - this->m_protein_pct == rhs.m_protein_pct && - this->m_kolbachIndex_pct == rhs.m_kolbachIndex_pct && - this->m_maxInBatch_pct == rhs.m_maxInBatch_pct && - this->m_recommendMash == rhs.m_recommendMash && - this->m_hardnessPrpGlassy_pct == rhs.m_hardnessPrpGlassy_pct && - this->m_kernelSizePrpPlump_pct == rhs.m_kernelSizePrpPlump_pct && - this->m_hardnessPrpHalf_pct == rhs.m_hardnessPrpHalf_pct && - this->m_hardnessPrpMealy_pct == rhs.m_hardnessPrpMealy_pct && - this->m_kernelSizePrpThin_pct == rhs.m_kernelSizePrpThin_pct && - this->m_friability_pct == rhs.m_friability_pct && - this->m_di_ph == rhs.m_di_ph && - this->m_viscosity_cP == rhs.m_viscosity_cP && - this->m_dmsP_ppm == rhs.m_dmsP_ppm && - this->m_fan_ppm == rhs.m_fan_ppm && - this->m_fermentability_pct == rhs.m_fermentability_pct && - this->m_betaGlucan_ppm == rhs.m_betaGlucan_ppm && + Utils::AutoCompare(this->m_notes , rhs.m_notes ) && + Utils::AutoCompare(this->m_moisture_pct , rhs.m_moisture_pct ) && + Utils::AutoCompare(this->m_alphaAmylase_dextUnits, rhs.m_alphaAmylase_dextUnits) && + Utils::AutoCompare(this->m_diastaticPower_lintner, rhs.m_diastaticPower_lintner) && + Utils::AutoCompare(this->m_protein_pct , rhs.m_protein_pct ) && + Utils::AutoCompare(this->m_kolbachIndex_pct , rhs.m_kolbachIndex_pct ) && + Utils::AutoCompare(this->m_maxInBatch_pct , rhs.m_maxInBatch_pct ) && + Utils::AutoCompare(this->m_recommendMash , rhs.m_recommendMash ) && + Utils::AutoCompare(this->m_hardnessPrpGlassy_pct , rhs.m_hardnessPrpGlassy_pct ) && + Utils::AutoCompare(this->m_kernelSizePrpPlump_pct, rhs.m_kernelSizePrpPlump_pct) && + Utils::AutoCompare(this->m_hardnessPrpHalf_pct , rhs.m_hardnessPrpHalf_pct ) && + Utils::AutoCompare(this->m_hardnessPrpMealy_pct , rhs.m_hardnessPrpMealy_pct ) && + Utils::AutoCompare(this->m_kernelSizePrpThin_pct , rhs.m_kernelSizePrpThin_pct ) && + Utils::AutoCompare(this->m_friability_pct , rhs.m_friability_pct ) && + Utils::AutoCompare(this->m_di_ph , rhs.m_di_ph ) && + Utils::AutoCompare(this->m_viscosity_cP , rhs.m_viscosity_cP ) && + Utils::AutoCompare(this->m_dmsP_ppm , rhs.m_dmsP_ppm ) && + Utils::AutoCompare(this->m_fan_ppm , rhs.m_fan_ppm ) && + Utils::AutoCompare(this->m_fermentability_pct , rhs.m_fermentability_pct ) && + Utils::AutoCompare(this->m_betaGlucan_ppm , rhs.m_betaGlucan_ppm ) && // Non-BeerJSON fields - this->m_supplier == rhs.m_supplier + Utils::AutoCompare(this->m_supplier , rhs.m_supplier) ); } diff --git a/src/model/Fermentable.h b/src/model/Fermentable.h index 268c732e..f049b01d 100644 --- a/src/model/Fermentable.h +++ b/src/model/Fermentable.h @@ -183,6 +183,7 @@ class Fermentable : public Ingredient, public IngredientBase { * info. */ static TypeLookup const typeLookup; + TYPE_LOOKUP_GETTER Fermentable(QString name = ""); Fermentable(NamedParameterBundle const & namedParameterBundle); diff --git a/src/model/Fermentation.h b/src/model/Fermentation.h index 79d6ab03..8cf4cbf0 100644 --- a/src/model/Fermentation.h +++ b/src/model/Fermentation.h @@ -66,6 +66,7 @@ class Fermentation : public NamedEntity, * info. */ static TypeLookup const typeLookup; + TYPE_LOOKUP_GETTER Fermentation(QString name = ""); Fermentation(NamedParameterBundle const & namedParameterBundle); diff --git a/src/model/FermentationStep.h b/src/model/FermentationStep.h index 85ff5cc4..54d32410 100644 --- a/src/model/FermentationStep.h +++ b/src/model/FermentationStep.h @@ -56,6 +56,7 @@ class FermentationStep : public StepExtended, public StepBasem_producer == rhs.m_producer && - this->m_productId == rhs.m_productId && - this->m_origin == rhs.m_origin && - this->m_year == rhs.m_year && - this->m_form == rhs.m_form && - this->m_alpha_pct == rhs.m_alpha_pct && - this->m_beta_pct == rhs.m_beta_pct + Utils::AutoCompare(this->m_producer , rhs.m_producer ) && + Utils::AutoCompare(this->m_productId, rhs.m_productId) && + Utils::AutoCompare(this->m_origin , rhs.m_origin ) && + Utils::AutoCompare(this->m_year , rhs.m_year ) && + Utils::AutoCompare(this->m_form , rhs.m_form ) && + Utils::AutoCompare(this->m_alpha_pct, rhs.m_alpha_pct) && + Utils::AutoCompare(this->m_beta_pct , rhs.m_beta_pct ) }; // If either object is an outline (see comment in model/OutlineableNamedEntity.h) then there is no point comparing @@ -97,26 +98,26 @@ bool Hop::isEqualTo(NamedEntity const & other) const { // Remaining BeerJSON fields -- excluding inventories - this->m_type == rhs.m_type && - this->m_notes == rhs.m_notes && - this->m_hsi_pct == rhs.m_hsi_pct && - this->m_substitutes == rhs.m_substitutes && + Utils::AutoCompare(this->m_type , rhs.m_type ) && + Utils::AutoCompare(this->m_notes , rhs.m_notes ) && + Utils::AutoCompare(this->m_hsi_pct , rhs.m_hsi_pct ) && + Utils::AutoCompare(this->m_substitutes , rhs.m_substitutes ) && // Oil content - this->m_totalOil_mlPer100g == rhs.m_totalOil_mlPer100g && - this->m_humulene_pct == rhs.m_humulene_pct && - this->m_caryophyllene_pct == rhs.m_caryophyllene_pct && - this->m_cohumulone_pct == rhs.m_cohumulone_pct && - this->m_myrcene_pct == rhs.m_myrcene_pct && - this->m_farnesene_pct == rhs.m_farnesene_pct && - this->m_geraniol_pct == rhs.m_geraniol_pct && - this->m_bPinene_pct == rhs.m_bPinene_pct && - this->m_linalool_pct == rhs.m_linalool_pct && - this->m_limonene_pct == rhs.m_limonene_pct && - this->m_nerol_pct == rhs.m_nerol_pct && - this->m_pinene_pct == rhs.m_pinene_pct && - this->m_polyphenols_pct == rhs.m_polyphenols_pct && - this->m_xanthohumol_pct == rhs.m_xanthohumol_pct + Utils::AutoCompare(this->m_totalOil_mlPer100g, rhs.m_totalOil_mlPer100g) && + Utils::AutoCompare(this->m_humulene_pct , rhs.m_humulene_pct ) && + Utils::AutoCompare(this->m_caryophyllene_pct , rhs.m_caryophyllene_pct ) && + Utils::AutoCompare(this->m_cohumulone_pct , rhs.m_cohumulone_pct ) && + Utils::AutoCompare(this->m_myrcene_pct , rhs.m_myrcene_pct ) && + Utils::AutoCompare(this->m_farnesene_pct , rhs.m_farnesene_pct ) && + Utils::AutoCompare(this->m_geraniol_pct , rhs.m_geraniol_pct ) && + Utils::AutoCompare(this->m_bPinene_pct , rhs.m_bPinene_pct ) && + Utils::AutoCompare(this->m_linalool_pct , rhs.m_linalool_pct ) && + Utils::AutoCompare(this->m_limonene_pct , rhs.m_limonene_pct ) && + Utils::AutoCompare(this->m_nerol_pct , rhs.m_nerol_pct ) && + Utils::AutoCompare(this->m_pinene_pct , rhs.m_pinene_pct ) && + Utils::AutoCompare(this->m_polyphenols_pct , rhs.m_polyphenols_pct ) && + Utils::AutoCompare(this->m_xanthohumol_pct , rhs.m_xanthohumol_pct ) ); } diff --git a/src/model/Hop.h b/src/model/Hop.h index 42988beb..bbe21adb 100644 --- a/src/model/Hop.h +++ b/src/model/Hop.h @@ -165,6 +165,7 @@ class Hop : public Ingredient, public IngredientBase { * info. */ static TypeLookup const typeLookup; + TYPE_LOOKUP_GETTER Hop(QString name = ""); Hop(NamedParameterBundle const & namedParameterBundle); @@ -216,18 +217,18 @@ class Hop : public Ingredient, public IngredientBase { Q_PROPERTY(std::optional myrcene_pct READ myrcene_pct WRITE setMyrcene_pct ) // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ - Q_PROPERTY(std::optional totalOil_mlPer100g READ totalOil_mlPer100g WRITE setTotalOil_mlPer100g) - Q_PROPERTY(std::optional farnesene_pct READ farnesene_pct WRITE setFarnesene_pct ) - Q_PROPERTY(std::optional geraniol_pct READ geraniol_pct WRITE setGeraniol_pct ) - Q_PROPERTY(std::optional bPinene_pct READ bPinene_pct WRITE setBPinene_pct ) - Q_PROPERTY(std::optional linalool_pct READ linalool_pct WRITE setLinalool_pct ) - Q_PROPERTY(std::optional limonene_pct READ limonene_pct WRITE setLimonene_pct ) - Q_PROPERTY(std::optional nerol_pct READ nerol_pct WRITE setNerol_pct ) - Q_PROPERTY(std::optional pinene_pct READ pinene_pct WRITE setPinene_pct ) - Q_PROPERTY(std::optional polyphenols_pct READ polyphenols_pct WRITE setPolyphenols_pct ) - Q_PROPERTY(std::optional xanthohumol_pct READ xanthohumol_pct WRITE setXanthohumol_pct ) - Q_PROPERTY(QString producer READ producer WRITE setProducer ) - Q_PROPERTY(QString productId READ productId WRITE setProductId ) + Q_PROPERTY(std::optional totalOil_mlPer100g READ totalOil_mlPer100g WRITE setTotalOil_mlPer100g) + Q_PROPERTY(std::optional farnesene_pct READ farnesene_pct WRITE setFarnesene_pct ) + Q_PROPERTY(std::optional geraniol_pct READ geraniol_pct WRITE setGeraniol_pct ) + Q_PROPERTY(std::optional bPinene_pct READ bPinene_pct WRITE setBPinene_pct ) + Q_PROPERTY(std::optional linalool_pct READ linalool_pct WRITE setLinalool_pct ) + Q_PROPERTY(std::optional limonene_pct READ limonene_pct WRITE setLimonene_pct ) + Q_PROPERTY(std::optional nerol_pct READ nerol_pct WRITE setNerol_pct ) + Q_PROPERTY(std::optional pinene_pct READ pinene_pct WRITE setPinene_pct ) + Q_PROPERTY(std::optional polyphenols_pct READ polyphenols_pct WRITE setPolyphenols_pct ) + Q_PROPERTY(std::optional xanthohumol_pct READ xanthohumol_pct WRITE setXanthohumol_pct ) + Q_PROPERTY(QString producer READ producer WRITE setProducer ) + Q_PROPERTY(QString productId READ productId WRITE setProductId ) /** * \brief It might seem odd to store year as a string rather than, say, std::optional, but this is * deliberate and for two reasons. Firstly BeerJSON treats it as a string. Secondly, we don't want it diff --git a/src/model/Ingredient.h b/src/model/Ingredient.h index c9d0ecff..e32cf1a6 100644 --- a/src/model/Ingredient.h +++ b/src/model/Ingredient.h @@ -86,6 +86,7 @@ class Ingredient : public OutlineableNamedEntity, * * See comment in utils/TypeTraits.h for definition of CONCEPT_FIX_UP (and why, for now, we need it). */ -template concept CONCEPT_FIX_UP IsIngredient = std::is_base_of_v; +template concept CONCEPT_FIX_UP IsIngredient = std::is_base_of_v; +template concept CONCEPT_FIX_UP IsNotIngredient = std::negation_v>; #endif diff --git a/src/model/IngredientBase.h b/src/model/IngredientBase.h index aafbddfa..16c59015 100644 --- a/src/model/IngredientBase.h +++ b/src/model/IngredientBase.h @@ -32,14 +32,14 @@ class IngredientBase : public CuriouslyRecurringTemplateBase(this->derived())->amount(); } /** - * \brief + * \brief Used to implement Ingredient::setTotalInventory() for Ingredient subclass (ie Derived) */ void doSetTotalInventory(Measurement::Amount const val) { auto inventory = InventoryTools::getInventory(this->derived()); diff --git a/src/model/Instruction.h b/src/model/Instruction.h index 53f16c83..1aaa6f0b 100644 --- a/src/model/Instruction.h +++ b/src/model/Instruction.h @@ -66,6 +66,7 @@ class Instruction : public NamedEntity { * info. */ static TypeLookup const typeLookup; + TYPE_LOOKUP_GETTER Instruction(QString name = ""); Instruction(NamedParameterBundle const & namedParameterBundle); diff --git a/src/model/Inventory.h b/src/model/Inventory.h index a9fb2396..aff53816 100644 --- a/src/model/Inventory.h +++ b/src/model/Inventory.h @@ -187,7 +187,7 @@ namespace InventoryTools { template std::shared_ptr getInventory(Ing const & ing) { // - // At the moment, we assume there is at most on Inventory object per ingredient object. In time we would like to + // At the moment, we assume there is at most one Inventory object per ingredient object. In time we would like to // extend this to manage, eg, different purchases/batches as separate Inventory items, but that's for another day. // auto result = firstInventory(ing); @@ -213,10 +213,11 @@ namespace InventoryTools { #define INVENTORY_DECL(IngredientName, LcIngredientName) \ public: \ /** \brief See comment in model/NamedEntity.h */ \ - static QString localisedName(); \ + static QString localisedName(); \ \ /** \brief See \c NamedEntity::typeLookup. */ \ static TypeLookup const typeLookup; \ + TYPE_LOOKUP_GETTER \ \ using IngredientClass = IngredientName; \ \ diff --git a/src/model/Mash.cpp b/src/model/Mash.cpp index 3102260a..8b1ea58a 100644 --- a/src/model/Mash.cpp +++ b/src/model/Mash.cpp @@ -28,6 +28,7 @@ #include "model/MashStep.h" #include "model/NamedParameterBundle.h" #include "model/Recipe.h" +#include "utils/AutoCompare.h" QString Mash::localisedName() { return tr("Mash"); } @@ -36,12 +37,12 @@ bool Mash::isEqualTo(NamedEntity const & other) const { Mash const & rhs = static_cast(other); // Base class will already have ensured names are equal return ( - this->m_grainTemp_c == rhs.m_grainTemp_c && - this->m_tunTemp_c == rhs.m_tunTemp_c && - this->m_spargeTemp_c == rhs.m_spargeTemp_c && - this->m_ph == rhs.m_ph && - this->m_mashTunWeight_kg == rhs.m_mashTunWeight_kg && - this->m_mashTunSpecificHeat_calGC == rhs.m_mashTunSpecificHeat_calGC + Utils::AutoCompare(this->m_grainTemp_c , rhs.m_grainTemp_c ) && + Utils::AutoCompare(this->m_tunTemp_c , rhs.m_tunTemp_c ) && + Utils::AutoCompare(this->m_spargeTemp_c , rhs.m_spargeTemp_c ) && + Utils::AutoCompare(this->m_ph , rhs.m_ph ) && + Utils::AutoCompare(this->m_mashTunWeight_kg , rhs.m_mashTunWeight_kg ) && + Utils::AutoCompare(this->m_mashTunSpecificHeat_calGC, rhs.m_mashTunSpecificHeat_calGC) // .:TBD:. Should we check MashSteps too? ); } diff --git a/src/model/Mash.h b/src/model/Mash.h index 53ac10e7..79720af1 100644 --- a/src/model/Mash.h +++ b/src/model/Mash.h @@ -85,6 +85,7 @@ class Mash : public NamedEntity, * info. */ static TypeLookup const typeLookup; + TYPE_LOOKUP_GETTER Mash(QString name = ""); Mash(NamedParameterBundle const & namedParameterBundle); diff --git a/src/model/MashStep.h b/src/model/MashStep.h index 6ad27786..7ed22b51 100644 --- a/src/model/MashStep.h +++ b/src/model/MashStep.h @@ -94,6 +94,7 @@ class MashStep : public Step, public StepBase { * info. */ static TypeLookup const typeLookup; + TYPE_LOOKUP_GETTER MashStep(QString name = ""); MashStep(NamedParameterBundle const & namedParameterBundle); diff --git a/src/model/Misc.cpp b/src/model/Misc.cpp index 43a60058..5b9f0581 100644 --- a/src/model/Misc.cpp +++ b/src/model/Misc.cpp @@ -31,6 +31,7 @@ #include "model/InventoryMisc.h" #include "model/NamedParameterBundle.h" #include "model/Recipe.h" +#include "utils/AutoCompare.h" QString Misc::localisedName() { return tr("Miscellaneous"); } @@ -60,9 +61,9 @@ bool Misc::isEqualTo(NamedEntity const & other) const { // Base class will already have ensured names are equal bool const outlinesAreEqual{ // "Outline" fields: In BeerJSON, all these fields are in the FermentableBase type - this->m_producer == rhs.m_producer && - this->m_productId == rhs.m_productId && - this->m_type == rhs.m_type + Utils::AutoCompare(this->m_producer , rhs.m_producer ) && + Utils::AutoCompare(this->m_productId, rhs.m_productId) && + Utils::AutoCompare(this->m_type , rhs.m_type ) }; // If either object is an outline (see comment in model/OutlineableNamedEntity.h) then there is no point comparing @@ -75,8 +76,8 @@ bool Misc::isEqualTo(NamedEntity const & other) const { outlinesAreEqual && // Remaining BeerJSON fields -- excluding inventories - this->m_useFor == rhs.m_useFor && - this->m_notes == rhs.m_notes + Utils::AutoCompare(this->m_useFor, rhs.m_useFor) && + Utils::AutoCompare(this->m_notes , rhs.m_notes ) ); } @@ -104,12 +105,12 @@ static_assert(std::is_base_of::value); Misc::Misc(QString name) : Ingredient{name}, - m_type {Misc::Type::Spice}, - m_useFor {"" }, - m_notes {"" }, + m_type {Misc::Type::Spice}, + m_useFor {"" }, + m_notes {"" }, // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞ - m_producer {"" }, - m_productId {"" } { + m_producer {"" }, + m_productId{"" } { return; } diff --git a/src/model/Misc.h b/src/model/Misc.h index 46d319b1..92322df0 100644 --- a/src/model/Misc.h +++ b/src/model/Misc.h @@ -111,6 +111,7 @@ class Misc : public Ingredient, public IngredientBase { * info. */ static TypeLookup const typeLookup; + TYPE_LOOKUP_GETTER Misc(QString name = ""); Misc(NamedParameterBundle const & namedParameterBundle); diff --git a/src/model/NamedEntity.cpp b/src/model/NamedEntity.cpp index d53f5f63..6e7d12ea 100644 --- a/src/model/NamedEntity.cpp +++ b/src/model/NamedEntity.cpp @@ -243,7 +243,7 @@ bool NamedEntity::operator!=(NamedEntity const & other) const { return !(*this == other); } -auto NamedEntity::operator<=>(NamedEntity const & other) const { +std::strong_ordering NamedEntity::operator<=>(NamedEntity const & other) const { // The spaceship operator is not defined for two QString objects, but it is defined for a pair of std::u16string, // which is close to the same thing (in that QString stores "a string of 16-bit QChars, where each QChar corresponds // to one UTF-16 code unit". @@ -462,18 +462,24 @@ void NamedEntity::propagatePropertyChange(BtStringConst const & propertyName, bo // Send a signal if needed if (notify) { - // It's obviously a coding error to supply a property name that is not registered with Qt as a property of this - // object - int idx = this->metaObject()->indexOfProperty(*propertyName); - Q_ASSERT(idx >= 0); - QMetaProperty metaProperty = this->metaObject()->property(idx); - QVariant value = metaProperty.read(this); - emit this->changed(metaProperty, value); + this->notifyPropertyChange(propertyName); } return; } +void NamedEntity::notifyPropertyChange(BtStringConst const & propertyName) const { + // It's obviously a coding error to supply a property name that is not registered with Qt as a property of this + // object + int idx = this->metaObject()->indexOfProperty(*propertyName); + Q_ASSERT(idx >= 0); + QMetaProperty metaProperty = this->metaObject()->property(idx); + QVariant value = metaProperty.read(this); + emit this->changed(metaProperty, value); + + return; +} + std::shared_ptr NamedEntity::owningRecipe() const { // Default is for NamedEntity not to be owned. return nullptr; diff --git a/src/model/NamedEntity.h b/src/model/NamedEntity.h index 1d4a6cf0..f52c8571 100644 --- a/src/model/NamedEntity.h +++ b/src/model/NamedEntity.h @@ -34,6 +34,7 @@ #include #include "model/FolderBase.h" +#include "model/NamedEntityCasters.h" #include "utils/BtStringConst.h" #include "utils/MetaTypes.h" #include "utils/TypeLookup.h" @@ -69,6 +70,20 @@ AddPropertyName(parentKey) //=========================================== End of property name constants =========================================== //====================================================================================================================== +/** + * \brief See \c NamedEntity::typeLookup. This macro -- to include just after \c typeLookup in the class declaration of + * \c NamedEntity and every non-abstract sublass thereof -- adds a virtual member function to return the static + * \c typeLookup member. + * + * I'm not sure whether there is a clever way to do this with the curiously recurring template pattern, but, if + * there is, I haven't figured it out yet. So, for now at least, macros are better than copy-and-paste code + * everywhere. + * + * NOTE that, strictly, this is not needed on abstract classes, though it is relatively harmless to include on + * them. + */ +#define TYPE_LOOKUP_GETTER inline virtual TypeLookup const & getTypeLookup() const { return typeLookup; } + /*! * \class NamedEntity * @@ -138,8 +153,14 @@ class NamedEntity : public QObject { * it \b before we have created the object. Eg, if we are reading a \c Fermentable from the DB, we first read * all the fields and construct a \c NamedParameterBundle, and then use that \c NamedParameterBundle to * construct the \c Fermentable. + * + * In other circumstances, if we need the \c TypeLookup for an instance of an unknown subclass of + * \c NamedEntity (eg in the \c PropertyPath), we should call the virtual member function \c getTypeLookup + * which just returns the relevant object. That's a turn-the-handle function */ static TypeLookup const typeLookup; + // See comment above for what this does + TYPE_LOOKUP_GETTER NamedEntity(QString t_name, bool t_display = false); NamedEntity(NamedEntity const & other); @@ -208,8 +229,12 @@ class NamedEntity : public QObject { /** * \brief As you might expect, this ensures we order \b NamedEntity objects by name + * + * Most subclasses do not need any more ordering than this. However \c RecipeAddition subclasses do need to + * override this, so we can have a canonical ordering of a list of, eg, pointers to \c RecipeAdditionHop, so + * that we can then easily compare two such lists for equality. */ - auto operator<=>(NamedEntity const & other) const; + std::strong_ordering operator<=>(NamedEntity const & other) const; // Everything that inherits from NamedEntity has these properties Q_PROPERTY(QString name READ name WRITE setName ) @@ -539,6 +564,12 @@ class NamedEntity : public QObject { */ void propagatePropertyChange(BtStringConst const & propertyName, bool notify = true) const; + /** + * \brief Emit a "changed" signal for the supplied \c propertyName. Usually called from \c propagatePropertyChange, + * but can be called directly when the property being updated is not stored in the DB (or not stored in the + * default way -- see eg RecipeAddition subclasses). + */ + void notifyPropertyChange(BtStringConst const & propertyName) const; /** * \brief Convenience function to check for the set being a no-op. (Sometimes the UI will call all setters, even on @@ -579,101 +610,6 @@ class NamedEntity : public QObject { return true; } -public: - - /** - * \brief Converts a QVariant containing `std::shared_ptr` or `std::shared_ptr` etc to - * `std::shared_ptr`. - */ - template - static std::shared_ptr downcastPointer(QVariant const & input) { - return std::static_pointer_cast(input.value>()); - } - - /** - * \brief Opposite of \c downcastVariant. Converts `std::shared_ptr` to a QVariant containing - * `std::shared_ptr` or `std::shared_ptr` etc. - */ - template - static QVariant upcastPointer(std::shared_ptr input) { - return QVariant::fromValue(std::static_pointer_cast(input)); - } - - /** - * \brief Converts `QList>` or `QList>` etc to - * `QList>`. - */ - template - static QList< std::shared_ptr > downcastList(QList> const & inputList) { - QList< std::shared_ptr > outputList; - outputList.reserve(inputList.size()); - for (std::shared_ptr ii : inputList) { - outputList.append(std::static_pointer_cast(ii)); - } - return outputList; - } - - /** - * \brief Converts `QList>` to `QList>` or `QList>` - * etc. - */ - template - static QList< std::shared_ptr > upcastList(QList> const & inputList) { - QList< std::shared_ptr > outputList; - outputList.reserve(inputList.size()); - for (std::shared_ptr ii : inputList) { - outputList.append(std::static_pointer_cast(ii)); - } - return outputList; - } - - /** - * \brief In various parts of the generic serialisation code (for XML and JSON), it is useful, for a given subclass - * \c T of \c NamedEntity, to have a pointer to a function that can cast a list of base pointers to derived - * ones. This is typically because we want to pass such a list in to the property system so that it can call - * a setter function. This is fortunate because it means we can avoid having the function pointer signature - * depend on T (even though it points to a templated function). - */ - template - static QVariant upcastListToVariant(QList> const & inputList) { - return QVariant::fromValue(NamedEntity::upcastList(inputList)); - } - - /** - * \brief In counterpart to \c upcastListToVariant, we need to be able to cast in the opposite direction. Again, we - * don't want the function \b signature to depend on T, and again the use of \c QVariant allows this. - * - * \param inputList A \c QVariant holding QList< std::shared_ptr> - */ - template - static QList> downcastListFromVariant(QVariant const & inputList) { - return NamedEntity::downcastList(inputList.value>>()); - } - - /** - * \brief It's useful in places to have pointers to all the upcasters and downcasters for a given type - */ - struct UpAndDownCasters{ - std::shared_ptr (*m_pointerDowncaster )(QVariant const & ); - QVariant (*m_pointerUpcaster )(std::shared_ptr ); - QVariant (*m_listUpcaster )(QList> const &); - QList> (*m_listDowncaster )(QVariant const & ); - }; - - /** - * \brief And because we can't template the constructor of a non-templated class/struct, we need a templated factory - * function. - */ - template - static UpAndDownCasters makeUpAndDownCasters() { - return { - NamedEntity::downcastPointer , - NamedEntity::upcastPointer , - NamedEntity::upcastListToVariant , - NamedEntity::downcastListFromVariant - }; - } - private: QString m_name; bool m_display; @@ -681,16 +617,6 @@ class NamedEntity : public QObject { bool m_beingModified; }; -/** - * \brief The downside of the \c UpAndDownCasters is that we now need to declare all sorts of permutations of - * Q_DECLARE_METATYPE, including a lot that we'll never actually use in practice. So it's simpler to have our - * own macro that generates all the Q_DECLARE_METATYPE macros we think we'll need for a class. - */ -#define BT_DECLARE_METATYPES(ClassName) \ -Q_DECLARE_METATYPE(std::shared_ptr ) \ -Q_DECLARE_METATYPE(QList< ClassName *>) \ -Q_DECLARE_METATYPE(QList >) - /** * \brief Convenience typedef for pointer to \c isOptional(); */ diff --git a/src/model/NamedEntityCasters.h b/src/model/NamedEntityCasters.h new file mode 100644 index 00000000..dd536714 --- /dev/null +++ b/src/model/NamedEntityCasters.h @@ -0,0 +1,152 @@ +/*====================================================================================================================== + * model/NamedEntityCasters.h is part of Brewken, and is copyright the following authors 2024: + * • Matt Young + * + * Brewken 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 3 of the License, or (at your option) any later + * version. + * + * Brewken 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 this program. If not, see + * . + =====================================================================================================================*/ +#ifndef MODEL_NAMEDENTITYCASTERS_H +#define MODEL_NAMEDENTITYCASTERS_H +#pragma once + +#include + +#include + +class NamedEntity; + +/** + * \brief With raw pointers, as long as we know \c foo is a raw pointer to some subclass of \c NamedEntity, we can + * downcast \c foo to be a raw pointer to \c NamedEntity without needing to know the exact type of object it + * actually points to. + * + * With shared pointers, we can't do this, because you need to know what you're casting from. This struct + * allows, eg, a subclass of \c NamedEntity to generate helper functions that can be accessed in a generic way + * (eg via \c TypeInfo) to do the equivalent shared pointer casting. + * + * This struct needs to be a separate from \c NamedEntity to avoid circular dependencies between \c NamedEntity + * and \c TypeInfo. Fortunately, shared_ptr is implemented in a clever way that it only needs a forward + * declaration of the class it is wrapping, so we can use shared_ptr here without having to include + * model/NamedEntity.h. + * + * NOTE: Instances of this \c NamedEntityCasters should be constructed via \c NamedEntityCasters::construct + */ +struct NamedEntityCasters { + //! Pointer to a \c downcastPointer function + std::shared_ptr (*m_pointerDowncaster)(QVariant const & ); + //! Pointer to a \c upcastPointer function + QVariant (*m_pointerUpcaster )(std::shared_ptr ); + //! Pointer to a \c upcastListToVariant function + QVariant (*m_listUpcaster )(QList> const &); + //! Pointer to a \c downcastList function + QList> (*m_listDowncaster )(QVariant const & ); + + /** + * \brief Converts a QVariant containing `std::shared_ptr` or `std::shared_ptr` etc to + * `std::shared_ptr`. + */ + template + static std::shared_ptr downcastPointer(QVariant const & input) { + return std::static_pointer_cast(input.value>()); + } + + /** + * \brief Opposite of \c downcastVariant. Converts `std::shared_ptr` to a QVariant containing + * `std::shared_ptr` or `std::shared_ptr` etc. + */ + template + static QVariant upcastPointer(std::shared_ptr input) { + return QVariant::fromValue(std::static_pointer_cast(input)); + } + + /** + * \brief Converts `QList>` or `QList>` etc to + * `QList>`. + */ + template + static QList< std::shared_ptr > downcastList(QList> const & inputList) { + QList< std::shared_ptr > outputList; + outputList.reserve(inputList.size()); + for (std::shared_ptr ii : inputList) { + outputList.append(std::static_pointer_cast(ii)); + } + return outputList; + } + + /** + * \brief Converts `QList>` to `QList>` or `QList>` + * etc. + */ + template + static QList< std::shared_ptr > upcastList(QList> const & inputList) { + QList< std::shared_ptr > outputList; + outputList.reserve(inputList.size()); + for (std::shared_ptr ii : inputList) { + outputList.append(std::static_pointer_cast(ii)); + } + return outputList; + } + + /** + * \brief In various parts of the generic serialisation code (for XML and JSON), it is useful, for a given subclass + * \c T of \c NamedEntity, to have a pointer to a function that can cast a list of base pointers to derived + * ones. This is typically because we want to pass such a list in to the property system so that it can call + * a setter function. This is fortunate because it means we can avoid having the function pointer signature + * depend on T (even though it points to a templated function). + */ + template + static QVariant upcastListToVariant(QList> const & inputList) { + return QVariant::fromValue(NamedEntityCasters::upcastList(inputList)); + } + + /** + * \brief In counterpart to \c upcastListToVariant, we need to be able to cast in the opposite direction. Again, we + * don't want the function \b signature to depend on T, and again the use of \c QVariant allows this. + * + * \param inputList A \c QVariant holding QList< std::shared_ptr> + */ + template + static QList> downcastListFromVariant(QVariant const & inputList) { + return NamedEntityCasters::downcastList(inputList.value>>()); + } + + /** + * \brief And because we can't template the constructor of a non-templated class/struct, we need a templated factory + * function. + */ + template + static NamedEntityCasters construct() { + return { + NamedEntityCasters::downcastPointer , + NamedEntityCasters::upcastPointer , + NamedEntityCasters::upcastListToVariant , + NamedEntityCasters::downcastListFromVariant + }; + } + +}; + + +/** + * \brief The downside of the \c NamedEntityCasters is that we now need to declare all sorts of permutations of + * Q_DECLARE_METATYPE, including a lot that we'll never actually use in practice. So it's simpler to have our + * own macro that generates all the Q_DECLARE_METATYPE macros we think we'll need for a class. + * + * NOTE: we also need to ensure shared pointers to most subclasses of \c NamedEntity are registered in + * utils/MetaTypes.cpp, otherwise we risk "QMetaProperty::read: Unable to handle unregistered datatype" + * errors at runtime. + */ +#define BT_DECLARE_METATYPES(ClassName) \ +Q_DECLARE_METATYPE(std::shared_ptr ) \ +Q_DECLARE_METATYPE(QList< ClassName *>) \ +Q_DECLARE_METATYPE(QList >) + +#endif diff --git a/src/model/Recipe.cpp b/src/model/Recipe.cpp index d60c0c8f..db5d0e30 100644 --- a/src/model/Recipe.cpp +++ b/src/model/Recipe.cpp @@ -67,6 +67,7 @@ #include "model/Yeast.h" #include "PersistentSettings.h" #include "PhysicalConstants.h" +#include "utils/AutoCompare.h" namespace { @@ -205,13 +206,14 @@ template<> BtStringConst const & Recipe::propertyNameFor BtStringConst const & Recipe::propertyNameFor() { return PropertyNames::Recipe::fermentationId ; } template<> BtStringConst const & Recipe::propertyNameFor() { return PropertyNames::Recipe::instructionIds ; } template<> BtStringConst const & Recipe::propertyNameFor() { return PropertyNames::Recipe::mashId ; } -template<> BtStringConst const & Recipe::propertyNameFor() { return PropertyNames::Recipe::fermentableAdditionIds; } -template<> BtStringConst const & Recipe::propertyNameFor() { return PropertyNames::Recipe::hopAdditionIds ; } -template<> BtStringConst const & Recipe::propertyNameFor() { return PropertyNames::Recipe::miscAdditionIds ; } -template<> BtStringConst const & Recipe::propertyNameFor() { return PropertyNames::Recipe::yeastAdditionIds ; } -template<> BtStringConst const & Recipe::propertyNameFor() { return PropertyNames::Recipe::saltAdjustmentIds ; } -template<> BtStringConst const & Recipe::propertyNameFor() { return PropertyNames::Recipe::waterUseIds ; } template<> BtStringConst const & Recipe::propertyNameFor