# HG changeset patch # User Michiel Broek # Date 1648647117 -7200 # Node ID fb0bb9a2a7e105393df4479834206ec9de5484a5 # Parent 409d9c7214bef369b62e9c8d588c29834dfc371a Added support to build designer plugins, but it is nog yet complete. Added RangedSlider fro the brewtarget project to make our version of it. Started EditRecipe screen. diff -r 409d9c7214be -r fb0bb9a2a7e1 CMakeLists.txt --- a/CMakeLists.txt Mon Mar 28 16:54:08 2022 +0200 +++ b/CMakeLists.txt Wed Mar 30 15:31:57 2022 +0200 @@ -4,7 +4,6 @@ PROJECT(bmsapp) CMAKE_MINIMUM_REQUIRED( VERSION 3.6 ) SET(bmsapp_EXECUTABLE "bmsapp") -MESSAGE( STATUS "Building bmsapp" ) # ===== Set application version ===== @@ -14,13 +13,22 @@ # Compile flags +option(BUILD_DESIGNER_PLUGINS "If on, you will only build and install the designer plugins." OFF) option(UPDATE_TRANSLATIONS "Enable rescanning sources to update .ts files" OFF) +IF( ${BUILD_DESIGNER_PLUGINS} ) + message(STATUS "Building designer plugins" ) +ELSE() + message(STATUS "Building bmsapp" ) +ENDIF() + # Automatically run moc on source files when necessary set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) +SET(CMAKE_INCLUDE_CURRENT_DIR ON) + SET( CMAKE_CXX_FLAGS_RELEASE "-Wall -ansi -pedantic -Wno-long-long -O2 -pipe" ) SET( CMAKE_CXX_FLAGS_DEBUG "-Wall -g3 -pipe" ) @@ -40,22 +48,32 @@ SET( TARGETPATH ${BINDIR} ) SET( DOCPATH ${DOCDIR} ) +SET( LIBPATH "$ENV{QT5DIR}" ) SET(ROOTDIR "${CMAKE_CURRENT_SOURCE_DIR}") SET(SRCDIR "${ROOTDIR}/src") SET(UIDIR "${ROOTDIR}/ui") +SET(DESIGNERDIR "${ROOTDIR}/designer") SET(DATADIR "${ROOTDIR}/data") SET(TRANSLATIONSDIR "${ROOTDIR}/translations") INCLUDE_DIRECTORIES(${SRCDIR}) INCLUDE_DIRECTORIES("${CMAKE_BINARY_DIR}/src") # In case of out-of-source build. +INCLUDE_DIRECTORIES("${CMAKE_BINARY_DIR}/designer") # ===== Find Qt5 ===== # Minimum versio 5.13 for debug messages. find_package(Qt5 5.13 REQUIRED COMPONENTS Core Widgets Network Sql LinguistTools PrintSupport WebSockets) +INCLUDE_DIRECTORIES(${Qt5Core_INCLUDE_DIRS}) +INCLUDE_DIRECTORIES(${Qt5Widgets_INCLUDE_DIRS}) +INCLUDE_DIRECTORIES(${Qt5Network_INCLUDE_DIRS}) +INCLUDE_DIRECTORIES(${Qt5Sql_INCLUDE_DIRS}) +INCLUDE_DIRECTORIES(${Qt5LinguistTools_INCLUDE_DIRS}) +INCLUDE_DIRECTORIES(${Qt5PrintSupport_INCLUDE_DIRS}) +INCLUDE_DIRECTORIES(${Qt5WebSockets_INCLUDE_DIRS}) -# PrintSupport Xml LinguistTools +# Xml # Some extra files for the "make clean" target. SET_PROPERTY( @@ -79,7 +97,57 @@ # ===== All sources ===== -set( SRCS +IF( ${BUILD_DESIGNER_PLUGINS} ) + + ADD_DEFINITIONS(${QT_DEFINITIONS}) + ADD_DEFINITIONS(-DQT_PLUGIN) + ADD_DEFINITIONS(-DQT_NO_DEBUG) + ADD_DEFINITIONS(-DQT_SHARED) + + INCLUDE_DIRECTORIES( + ${QT_INCLUDE_DIR} + ${SRCDIR} + ) + + LINK_DIRECTORIES( + ${QT_LIBRARY_DIR} + ) + + set( SRC_FILES + ${SRCDIR}/RangedSlider.cpp + ${SRCDIR}/nulldateedit.cpp + ) + + # By default only QtCore and QtGui are enabled + SET( QT_USE_QTDESIGNER TRUE ) + + set( MOC_FILES + ${SRCDIR}/RangedSlider.h + ${SRCDIR}/nulldateedit.h + ) + + set( PLUGIN_MOCS + # designer/*.h + ) + + set( PLUGIN_SRCS + # designer/*.cpp + ) + + add_library( bmsapp_plugins SHARED + ${SRC_FILES} + ${PLUGIN_SRCS} + ${MOC_FILES} + ${PLUGIN_MOCS} + ) + + TARGET_LINK_LIBRARIES( bmsapp_plugins + ${QT_LIBRARIES} + ) + +ELSE() + + set( SRCS ${SRCDIR}/main.cpp ${SRCDIR}/RecipesTree.cpp ${SRCDIR}/AboutDialog.cpp @@ -105,15 +173,16 @@ ${SRCDIR}/EditProfileStyle.cpp ${SRCDIR}/ProfileFerments.cpp ${SRCDIR}/EditProfileFerment.cpp + ${SRCDIR}/EditRecipe.cpp ${SRCDIR}/Setup.cpp ${SRCDIR}/Utils.cpp ${SRCDIR}/PrinterDialog.cpp ${SRCDIR}/MainWindow.cpp ${SRCDIR}/database/database.cpp ${SRCDIR}/nulldateedit.cpp -) + ) -set( HDRS + set( HDRS ${SRCDIR}/RecipesTree.h ${SRCDIR}/AboutDialog.h ${SRCDIR}/InventorySuppliers.h @@ -138,15 +207,16 @@ ${SRCDIR}/EditProfileStyle.h ${SRCDIR}/ProfileFerments.h ${SRCDIR}/EditProfileFerment.h + ${SRCDIR}/EditRecipe.h ${SRCDIR}/Setup.h ${SRCDIR}/Utils.h ${SRCDIR}/PrinterDialog.h ${SRCDIR}/MainWindow.h ${SRCDIR}/database/database.h ${SRCDIR}/nulldateedit.h -) + ) -set( UIS + set( UIS ${UIDIR}/AboutDialog.ui ${UIDIR}/EditSupplier.ui ${UIDIR}/EditFermentable.ui @@ -159,54 +229,70 @@ ${UIDIR}/EditProfileMash.ui ${UIDIR}/EditProfileStyle.ui ${UIDIR}/EditProfileFerment.ui + ${UIDIR}/EditRecipe.ui ${UIDIR}/MainWindow.ui -) + ) - -set( TS_FILES + set( TS_FILES ${TRANSLATIONSDIR}/bmsapp_en.ts # English ${TRANSLATIONSDIR}/bmsapp_nl.ts # Dutch -) + ) -set( SOURCE_FILES + set( SOURCE_FILES ${SRCS} ${HDRS} ${UIS} resources/icons.qrc resources/qdarkstyle/theme/style.qrc -) + ) + +ENDIF() # ===== Build the application ===== -# Run with cmake -DUPDATE_TRANSLATIONS=ON .. -# or cmake -DUPDATE_TRANSLATIONS=OFF .. +IF( ${BUILD_DESIGNER_PLUGINS} ) -if(UPDATE_TRANSLATIONS) - message("** parse sources for new translations") - qt5_create_translation(QM_FILES ${SOURCE_FILES} ${TS_FILES}) -else() - message("** update qm files") - qt5_add_translation(QM_FILES ${TS_FILES}) -endif() +ELSE() + + # Run with cmake -DUPDATE_TRANSLATIONS=ON .. + # or cmake -DUPDATE_TRANSLATIONS=OFF .. -SET( bmsapp_DESKTOP - ${ROOTDIR}/bmsapp.desktop -) - + if(UPDATE_TRANSLATIONS) + message("** parse sources for new translations") + qt5_create_translation(QM_FILES ${SOURCE_FILES} ${TS_FILES}) + else() + message("** update qm files") + qt5_add_translation(QM_FILES ${TS_FILES}) + endif() -add_executable(${bmsapp_EXECUTABLE} ${SOURCE_FILES} ${QM_FILES}) -target_link_libraries(${bmsapp_EXECUTABLE} Qt5::Core Qt5::Widgets Qt5::Network Qt5::Sql Qt5::PrintSupport Qt5::WebSockets) + SET( bmsapp_DESKTOP + ${ROOTDIR}/bmsapp.desktop + ) -# `make translations' -add_custom_target(translations DEPENDS ${QM_FILES}) + add_executable(${bmsapp_EXECUTABLE} ${SOURCE_FILES} ${QM_FILES}) + target_link_libraries(${bmsapp_EXECUTABLE} Qt5::Core Qt5::Widgets Qt5::Network Qt5::Sql Qt5::PrintSupport Qt5::WebSockets) + # `make translations' + add_custom_target(translations DEPENDS ${QM_FILES}) +ENDIF() # ===== Install the application ===== -install(TARGETS ${bmsapp_EXECUTABLE} - RUNTIME DESTINATION bin -) +IF( ${BUILD_DESIGNER_PLUGINS} ) + + INSTALL(TARGETS bmsapp_plugins + DESTINATION "${LIBPATH}/plugins/designer" + ) + +ELSE() -INSTALL( FILES ${bmsapp_DESKTOP} + install(TARGETS ${bmsapp_EXECUTABLE} + RUNTIME DESTINATION bin + ) + + INSTALL( FILES ${bmsapp_DESKTOP} DESTINATION "${DATAROOTDIR}/applications" -) + ) + +ENDIF() + diff -r 409d9c7214be -r fb0bb9a2a7e1 src/EditRecipe.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/EditRecipe.cpp Wed Mar 30 15:31:57 2022 +0200 @@ -0,0 +1,276 @@ +/** + * EditRecipe.cpp is part of bmsapp. + * + * bmsapp 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. + * + * bmsapp 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 . + */ +#include "EditRecipe.h" +#include "../ui/ui_EditRecipe.h" +#include "MainWindow.h" + + +EditRecipe::EditRecipe(int id, QWidget *parent) : QDialog(parent), ui(new Ui::EditRecipe) +{ + QSqlQuery query; + + qDebug() << "EditRecipe record:" << id; + ui->setupUi(this); + this->recno = id; + + WindowTitle(); + + ui->typeEdit->addItem(tr("Extract")); + ui->typeEdit->addItem(tr("Partial Mash")); + ui->typeEdit->addItem(tr("All Grain")); + + if (id >= 0) { + query.prepare("SELECT * FROM recipes WHERE record = :recno"); + query.bindValue(":recno", id); + query.exec(); + query.next(); + + ui->lockedEdit->setChecked(query.value(2).toInt() ? true:false); + // 3 st_name + // 4 st_letter + // 5 st_guide + // 6 st_category + // 7 st_category_number + // 8 st_type + // 9 st_og_min + // 10 st_og_max + // 11 st_fg_min + // 12 st_fg_max + // 13 st_ibu_min + // 14 st_ibu_max + // 15 st_color_min + // 16 st_color_max + // 17 st_carb_min + // 18 st_carb_max + // 19 st_abv_min + // 20 st_abv_max + ui->nameEdit->setText(query.value(21).toString()); + ui->notesEdit->setPlainText(query.value(22).toString()); + ui->typeEdit->setCurrentIndex(query.value(23).toInt()); + ui->batch_sizeEdit->setValue(query.value(24).toDouble()); + ui->boil_sizeEdit->setValue(query.value(25).toDouble()); + ui->boil_timeEdit->setValue(query.value(26).toInt()); + ui->efficiencyEdit->setValue(query.value(27).toDouble()); + // 28 est_og + // 29 est_fg + // 30 est_abv + // 31 est_color + // 32 color_method + // 33 est_ibu + // 34 ibu_method + // 35 est_carb + // 36 sparge_temp + // 37 sparge_ph + // 38 sparge_volume + // 39 sparge_source + // 40 sparge_acid_type + // 41 sparge_acid_perc + // 42 sparge_acid_amount + // 43 mash_ph + // 44 mash_name + // 45 calc_acid + // 46 w1_name + // 47 w1_amount + // 48 w1_calcium + // 49 w1_sulfate + // 50 w1_chloride + // 51 w1_sodium + // 52 w1_magnesium + // 53 w1_total_alkalinity + // 54 w1_ph + // 55 w1_cost + // 56 w2_name + // 57 w2_amount + // 58 w2_calcium + // 59 w2_sulfate + // 60 w2_chloride + // 61 w2_sodium + // 62 w2_magnesium + // 63 w2_total_alkalinity + // 64 w2_ph + // 65 w2_cost + // 66 wg_amount + // 67 wg_calcium + // 68 wg_sulfate + // 69 wg_chloride + // 70 wg_sodium + // 71 wg_magnesium + // 72 wg_total_alkalinity + // 73 wg_ph + // 74 wb_calcium + // 75 wb_sulfate + // 76 wb_chloride + // 77 wb_sodium + // 78 wb_magnesium + // 79 wb_total_alkalinity + // 80 wb_ph + // 81 wa_acid_name + // 82 wa_acid_perc + // 83 wa_base_name + // 84 json_fermentables + // 85 json_hops + // 86 json_miscs + // 87 json_yeasts + // 88 json_mashs + } else { + /* Set some defaults */ + } + + connect(ui->lockedEdit, &QCheckBox::stateChanged, this, &EditRecipe::is_changed); + + connect(ui->nameEdit, &QLineEdit::textChanged, this, &EditRecipe::is_changed); + connect(ui->notesEdit, SIGNAL(textChanged()), this, SLOT(is_changed())); + connect(ui->typeEdit, &QComboBox::currentTextChanged, this, &EditRecipe::is_changed); + connect(ui->batch_sizeEdit, &QDoubleSpinBox::textChanged, this, &EditRecipe::is_changed); + connect(ui->boil_sizeEdit, &QDoubleSpinBox::textChanged, this, &EditRecipe::is_changed); + connect(ui->boil_timeEdit, &QSpinBox::textChanged, this, &EditRecipe::is_changed); + connect(ui->efficiencyEdit, &QDoubleSpinBox::textChanged, this, &EditRecipe::is_changed); + + ui->saveButton->setEnabled(false); + ui->deleteButton->setEnabled((id >= 0) ? true:false); +} + + +EditRecipe::~EditRecipe() +{ + qDebug() << "EditRecipe done"; + delete ui; + emit entry_changed(); +} + + +/* + * Window header, mark any change with '**' + */ +void EditRecipe::WindowTitle() +{ + QString txt; + + if (this->recno < 0) { + txt = QString(tr("BMSapp - Add new recipe")); + } else { + txt = QString(tr("BMSapp - Edit recipe %1").arg(this->recno)); + } + + if (this->textIsChanged) { + txt.append((QString(" **"))); + } + setWindowTitle(txt); +} + + +void EditRecipe::on_saveButton_clicked() +{ + QSqlQuery query; + + /* If there are errors in the form, show a message and do "return;" */ +// if (ui->nameEdit->text().length() < 2) { +// QMessageBox::warning(this, tr("Edit Recipe"), tr("Name empty or too short.")); +// return; +// } + + if (this->textIsChanged) { + if (this->recno == -1) { + query.prepare("INSERT INTO recipes SET name=:name, " + "uuid = :uuid"); + } else { + query.prepare("UPDATE recipes SET name=:name " + " WHERE record = :recno"); + } + //query.bindValue(":name", ui->nameEdit->text()); + //query.bindValue(":notes", ui->notesEdit->toPlainText()); + if (this->recno == -1) { + query.bindValue(":uuid", QUuid::createUuid().toString().mid(1, 36)); + } else { + query.bindValue(":recno", this->recno); + } + query.exec(); + if (query.lastError().isValid()) { + qDebug() << "EditRecipe" << query.lastError(); + QMessageBox::warning(this, tr("Database error"), + tr("MySQL error: %1\n%2\n%3") + .arg(query.lastError().nativeErrorCode()) + .arg(query.lastError().driverText()) + .arg(query.lastError().databaseText())); + } else { + qDebug() << "EditRecipe Saved"; + } + } + + ui->saveButton->setEnabled(false); + this->textIsChanged = false; + WindowTitle(); +} + + +void EditRecipe::on_deleteButton_clicked() +{ + QSqlQuery query; + + query.prepare("DELETE FROM recipes WHERE record = :recno"); + query.bindValue(":recno", this->recno); + query.exec(); + if (query.lastError().isValid()) { + qDebug() << "EditRecipe" << query.lastError(); + QMessageBox::warning(this, tr("Database error"), + tr("MySQL error: %1\n%2\n%3") + .arg(query.lastError().nativeErrorCode()) + .arg(query.lastError().driverText()) + .arg(query.lastError().databaseText())); + } else { + qDebug() << "EditRecipe Deleted" << this->recno; + } + + this->close(); + this->setResult(1); +} + + +void EditRecipe::is_changed() +{ + ui->saveButton->setEnabled(true); + ui->deleteButton->setEnabled((this->recno >= 0) ? true:false); + this->textIsChanged = true; + WindowTitle(); +} + + +void EditRecipe::time_changed() +{ + is_changed(); +} + + +void EditRecipe::on_quitButton_clicked() +{ + if (this->textIsChanged) { + int rc = QMessageBox::warning(this, tr("Recipe changed"), tr("The ingredient has been modified. Save changes?"), + QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Save); + switch (rc) { + case QMessageBox::Save: + on_saveButton_clicked(); + break; /* Saved and then Quit */ + case QMessageBox::Discard: + break; /* Quit without Save */ + case QMessageBox::Cancel: + return; /* Return to the editor page */ + } + } + + this->close(); + this->setResult(1); +} diff -r 409d9c7214be -r fb0bb9a2a7e1 src/EditRecipe.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/EditRecipe.h Wed Mar 30 15:31:57 2022 +0200 @@ -0,0 +1,37 @@ +#ifndef _EDITRECIPE_H +#define _EDITRECIPE_H + +#include + + +namespace Ui { +class EditRecipe; +} + +class EditRecipe : public QDialog +{ + Q_OBJECT + +signals: + void entry_changed(); + +public: + explicit EditRecipe(int id, QWidget *parent = 0); + ~EditRecipe(); + +private slots: + void on_saveButton_clicked(); + void on_quitButton_clicked(); + void on_deleteButton_clicked(); + void is_changed(); + void time_changed(); + +private: + Ui::EditRecipe *ui; + int recno, lasttime = 0; + bool textIsChanged = false; + + void WindowTitle(); +}; + +#endif diff -r 409d9c7214be -r fb0bb9a2a7e1 src/RangedSlider.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/RangedSlider.cpp Wed Mar 30 15:31:57 2022 +0200 @@ -0,0 +1,443 @@ +/* + * RangedSlider.cpp is part bmsapp. + * + * Original written for Brewtarget, and is Copyright the following + * authors 2009-2020 + * - Matt Young + * - Mik Firestone + * - Philip G. Lee + * + * bmsapp 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. + * + * bmsapp 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 . + */ + +#include "RangedSlider.h" +//#include "brewtarget.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +RangedSlider::RangedSlider(QWidget* parent) + : QWidget(parent), + _min(0.0), + _max(1.0), + _prefMin(0.25), + _prefMax(0.75), + _val(0.5), + _valText("0.500"), + _prec(3), + _tickInterval(0), + _secondaryTicks(1), + _tooltipText(""), + _bgBrush(QColor(255,255,255)), + _prefRangeBrush(QColor(0,0,0)), + _prefRangePen(Qt::NoPen), + _markerBrush(QColor(255,255,255)), + _markerTextIsValue(false), + valueTextFont("Arial", + 14, // QFonts are specified in point size, so the hard-coded number is fine here. + QFont::Black), // Note that QFont::Black is a weight (more bold than ExtraBold), not a colour. + indicatorTextFont("Arial", + 10, + QFont::Normal) // Previously we just did the indicator text in 'default' font +{ + // Ensure this->heightInPixels is properly initialised + this->recalculateHeightInPixels(); + + // In principle we want to set our min/max sizes etc here. However, if, say, a maximumSize property has been set + // for this object in a Designer UI File (eg ui/mainWindow.ui) then that setting will override this one, because it + // will be applied later (in fact pretty much straight after this constructor returns). So we also make this call + // inside setValue(), which will be invoked _after_ the setter calls that were auto-generated from the Designer UI + // File. + this->setSizes(); + + // Generate mouse move events whenever mouse movers over widget. + this->setMouseTracking(true); + + this->repaint(); +} + +void RangedSlider::setPreferredRange( double min, double max ) +{ + _prefMin = min; + _prefMax = max; + + // Only show tooltips if the range has nonzero size. + setMouseTracking(min < max); + + _tooltipText = QString("%1 - %2").arg(min, 0, 'f', _prec).arg(max, 0, 'f', _prec); + + update(); +} + +void RangedSlider::setPreferredRange(QPair minmax) +{ + setPreferredRange( minmax.first, minmax.second ); +} + +void RangedSlider::setRange( double min, double max ) +{ + _min = min; + _max = max; + update(); +} + +void RangedSlider::setRange(QPair minmax) +{ + setRange( minmax.first, minmax.second ); +} + +void RangedSlider::setValue(double value) +{ + _val = value; + _valText = QString("%1").arg(_val, 0, 'f', _prec); + update(); + + // See comment in constructor for why we call this here + this->setSizes(); + return; +} + +void RangedSlider::setPrecision(int precision) +{ + _prec = precision; + update(); +} + +void RangedSlider::setBackgroundBrush( QBrush const& brush ) +{ + _bgBrush = brush; + update(); +} + +void RangedSlider::setPreferredRangeBrush( QBrush const& brush ) +{ + _prefRangeBrush = brush; + update(); +} + +void RangedSlider::setPreferredRangePen( QPen const& pen ) +{ + _prefRangePen = pen; + update(); +} + +void RangedSlider::setMarkerBrush( QBrush const& brush ) +{ + _markerBrush = brush; + update(); +} + +void RangedSlider::setMarkerText( QString const& text ) +{ + _markerText = text; + update(); +} + +void RangedSlider::setMarkerTextIsValue(bool val) +{ + _markerTextIsValue = val; + update(); +} + +void RangedSlider::setTickMarks( double primaryInterval, int secondaryTicks ) +{ + _secondaryTicks = (secondaryTicks<1)? 1 : secondaryTicks; + _tickInterval = primaryInterval/_secondaryTicks; + + update(); +} + +void RangedSlider::recalculateHeightInPixels() const { + // + // We need to be able to tell Qt about our minimum and preferred size in pixels. This is something that's going + // to depend on the dots-per-inch (DPI) resolution of the monitor we're being displayed on. Advice at + // https://doc.qt.io/qt-5/highdpi.html is that, for best High DPI display support, we should replace hard-coded + // sizes in layouts and drawing code with values calculated from font metrics or screen size. For this widget, we + // use font sizes (as described in more detail later in this comment) as it seems simpler and the height of the + // widget really is determined by the size of the text it contains. + // + // In theory, someone might be running the app on a system with multiple screens with different DPI resolutions, + // so, in principle we ought to do redo size calculations every time the widget is moved, in case it moves from one + // screen to another and the resolution changes. In practice, I'm not sure how big a requirement this is. So, for + // now, have just tried to organise things so that it would, in principle, be possible to implement such behaviour + // in future. + // + // We want height to be fixed, as the slider does not get more readable if you make it taller. So minimum and + // preferred height are the same. + // + // For width, We are OK for them to expand and contract horizontally, within reason, as the size of the main window + // changes. Minimum and preferred widths are a bit of a rule-of-thumb, but 2× and 4× height are a sensible stab. + // + // Firstly we have to determine what the fixed height is. We could query the DPI resolution and size of the current + // screen, but the simplest thing is to use font sizes. The slider is basically two lines of characters high. Top + // line is the "indicator" text that sits above the slider visual in the middle of the "preferred range". Bottom + // line is the slider visual and the "value" text that sits to the right of the slider visual. The fonts for both + // bits of text are set in device-independent points in the constructor, and we can just query their height etc in + // pixels. + // + // Secondly, the way we tell Qt about minimum and preferred sizes is slightly different: + // • setMinimumSize() tells Qt not to make us smaller than the specified size when resizing windows etc, HOWEVER + // it does not determine what size we are initially drawn + // • instead, Qt calls sizeHint() when doing initial layout, and we must override this to supply our desired + // initial dimensions. NB: Although it makes no sense, there is nothing to stop this method returning dimensions + // below the minimums already set via setMinimumSize(). (AIUI, sizeHint() is also called on resize events to + // find our preferred dimensions.) + // + // The final wrinkle is that the height of the font sort of depends what you mean. Strictly, using the inter-line + // spacing (= height plus leading, though the latter is often 0) gives you enough space to show any character of the + // font. It is helpful for the indicator text to have a bit of space below it before we draw the graphical bit, and + // it's not a large font in any case. However, for large value text font we don't necessarily need all this space + // because we currently only show digits and decimal points, which don't require space below the baseline. However, + // assumptions about space below the baseline are locale-specific, so, say, using ascent() instead of lineSpacing() + // could end up painting us into a corner. + // + QFontMetrics indicatorTextFontMetrics(this->indicatorTextFont); + QFontMetrics valueTextFontMetrics(this->valueTextFont); + this->heightInPixels = indicatorTextFontMetrics.lineSpacing() + valueTextFontMetrics.lineSpacing(); + return; +} + +void RangedSlider::setSizes() { + // Caller's responsibility to have recently called this->recalculateHeightInPixels(). (See comment in that function + // for how we choose minimum width.) + this->setMinimumSize(2 * this->heightInPixels, this->heightInPixels); + + this->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Fixed ); + + // There no particular reason to limit our horizontal size, so, in principle, this call asks that there be no such + // (practical) limit. + this->setMaximumWidth(QWIDGETSIZE_MAX); + + return; +} + +QSize RangedSlider::sizeHint() const +{ + this->recalculateHeightInPixels(); + return QSize(4 * this->heightInPixels, this->heightInPixels); +} + +void RangedSlider::mouseMoveEvent(QMouseEvent* event) +{ + event->accept(); + + QPoint tipPoint( mapToGlobal(QPoint(0,0)) ); + QToolTip::showText( tipPoint, _tooltipText, this ); +} + +void RangedSlider::paintEvent(QPaintEvent* event) +{ + // + // Simplistically, the high-level layout of the slider is: + // + // |-------------------------------------------------------------| + // | Indicator text | B L A N K | + // |------------------------------------------------+------------| + // | <--------------- Graphical Area -------------> | Value text | + // |-------------------------------------------------------------| + // + // The graphical area has: + // - a background rectangle of the full width of the area, representing the range from this->_min to this->_max + // - a foreground rectangle showing the sub-range of this background from this->_prefMin to this->_prefMax + // - a line ("the indicator") showing where this->_val lies in the (this->_min to this->_max) range + // + // The indicator text sits above the indicator line and shows either its value (this->_valText) or some textual + // description (eg "Slightly Malty" on the IBU/GU scale) which comes from this->_markerText. + // + // In principle, we could have the value text a slightly different height than the graphical area - eg to help + // squeeze into smaller available vertical space on small screens (as we know there is blank space of + // indicatorTextHeight pixels above the space for the value text). + // + // The value text also shows this->_valText. + // + + QFontMetrics indicatorTextFontMetrics(this->indicatorTextFont); + int indicatorTextHeight = indicatorTextFontMetrics.lineSpacing(); + + // The heights of the slider graphic and the value text are usually the same, but we calculate them differently in + // case, in future, we want to squeeze things up a bit. + QFontMetrics valueTextFontMetrics(this->valueTextFont); + int valueTextHeight = valueTextFontMetrics.lineSpacing(); + + int graphicalAreaHeight = this->height() - indicatorTextHeight; + + // Although the Qt calls take an x- and a y- radius, we want the radius on the rectangle corners to be the same + // vertically and horizontally, so only define one measure here. + int rectangleCornerRadius = graphicalAreaHeight / 4; + + static const QPalette palette(QApplication::palette()); + static const int indicatorLineWidth = 4; + static const QColor fgRectColor(0,127,0); + static const QColor indicatorTextColor(0,0,0); + static const QColor valueTextColor(0,127,0); + + // We need to allow for the width of the text that displays to the right of the slider showing the current value. + // If there were just one slider, we might ask Qt for the width of this text with one of the following calls: + // const int valueTextWidth = valueTextFontMetrics.width(_valText); // Pre Qt 5.13 + // const int valueTextWidth = valueTextFontMetrics.horizontalAdvance(_valText); // Since Qt 5.13 + // However, we want all the sliders to have exact same width, so we choose some representative text to measure the + // width of. We assume that all sliders show no more than 4 digits and a decimal point, and then add a space to + // ensure a gap between the value text and the graphical area. (Note that digits are all the same width in the font + // we are using. + int valueTextWidth = +#if QT_VERSION < QT_VERSION_CHECK(5,13,0) + valueTextFontMetrics.width(" 1.000"); +#else + valueTextFontMetrics.horizontalAdvance(" 1.000"); +#endif + + QLinearGradient glassGrad( QPointF(0,0), QPointF(0,graphicalAreaHeight) ); + glassGrad.setColorAt( 0, QColor(255,255,255,127) ); + glassGrad.setColorAt( 1, QColor(255,255,255,0) ); + QBrush glassBrush(glassGrad); + + // Per https://doc.qt.io/qt-5/highdpi.html, for best High DPI display support, we need to: + // • Always use the qreal versions of the QPainter drawing API + // • Size windows and dialogs in relation to the corresponding screen size + // • Replace hard-coded sizes in layouts and drawing code with values calculated from font metrics or screen size + QPainter painter(this); + + // Work out the left-to-right (ie x-coordinate) positions of things in the graphical area + double graphicalAreaWidth = this->width() - valueTextWidth; + double range = this->_max - this->_min; + double fgRectLeft = graphicalAreaWidth * ((this->_prefMin - this->_min )/range); + double fgRectWidth = graphicalAreaWidth * ((this->_prefMax - this->_prefMin)/range); + double indicatorLineMiddle = graphicalAreaWidth * ((this->_val - this->_min )/range); + double indicatorLineLeft = indicatorLineMiddle - (indicatorLineWidth / 2); + + // Make sure all coordinates are valid. + fgRectLeft = qBound(0.0, fgRectLeft, graphicalAreaWidth); + fgRectWidth = qBound(0.0, fgRectWidth, graphicalAreaWidth - fgRectLeft); + indicatorLineMiddle = qBound(0.0, indicatorLineMiddle, graphicalAreaWidth - (indicatorLineWidth / 2)); + indicatorLineLeft = qBound(0.0, indicatorLineLeft, graphicalAreaWidth - indicatorLineWidth); + + // The left-to-right position of the indicator text (also known as marker text) depends on where the slider is. + // First we ask the painter what size rectangle it will need to display this text + painter.setPen(indicatorTextColor); + painter.setFont(this->indicatorTextFont); + QRectF indicatorTextRect = painter.boundingRect(QRectF(), + Qt::AlignCenter | Qt::AlignBottom, + this->_markerTextIsValue ? this->_valText : this->_markerText); + + // Then we use the size of this rectangle to try to make the middle of the text sit over the indicator marker on + // the slider - but bounding things so that the text doesn't go off the edge of the slider. + double indicatorTextLeft = qBound(0.0, + indicatorLineMiddle - (indicatorTextRect.width() / 2), + graphicalAreaWidth - indicatorTextRect.width()); + + // Now we can draw the indicator text + painter.drawText( + indicatorTextLeft, 0, + indicatorTextRect.width(), indicatorTextRect.height(), + Qt::AlignCenter | Qt::AlignBottom, + this->_markerTextIsValue ? this->_valText : this->_markerText + ); + + // Next draw the value text + // We work out its vertical position relative to the bottom of the graphical area in case (in future) we want to be + // able to use some of the blank space above it (to the right of the indicator text). + painter.setPen(valueTextColor); + painter.setFont(this->valueTextFont); + painter.drawText(graphicalAreaWidth, this->height() - valueTextHeight, + valueTextWidth, valueTextHeight, + Qt::AlignRight | Qt::AlignVCenter, + this->_valText ); + + // All the rest of what we need to do is inside the graphical area, so move the origin to the top-left corner of it + painter.translate(0, indicatorTextRect.height()); + painter.setPen(Qt::NoPen); + + // Make sure anything we draw "inside" the "glass rectangle" stays inside. + QPainterPath clipRect; + clipRect.addRoundedRect( QRectF(0, 0, graphicalAreaWidth, graphicalAreaHeight), + rectangleCornerRadius, + rectangleCornerRadius ); + painter.setClipPath(clipRect); + + // Draw the background rectangle. + painter.setBrush(_bgBrush); + painter.setRenderHint(QPainter::Antialiasing); + painter.drawRoundedRect( QRectF(0, 0, graphicalAreaWidth, graphicalAreaHeight), + rectangleCornerRadius, + rectangleCornerRadius ); + painter.setRenderHint(QPainter::Antialiasing, false); + + // Draw the style "foreground" rectangle. + painter.save(); + painter.setBrush(_prefRangeBrush); + painter.setPen(_prefRangePen); + painter.setRenderHint(QPainter::Antialiasing); + painter.drawRoundedRect( QRectF(fgRectLeft, 0, fgRectWidth, graphicalAreaHeight), + rectangleCornerRadius, + rectangleCornerRadius ); + painter.restore(); + + // Draw the indicator. + painter.setBrush(_markerBrush); + painter.drawRect( QRectF(indicatorLineLeft, 0, indicatorLineWidth, graphicalAreaHeight) ); + + // Draw a white-to-clear gradient to suggest "glassy." + painter.setBrush(glassBrush); + painter.setRenderHint(QPainter::Antialiasing); + painter.drawRoundedRect( QRectF(0, 0, graphicalAreaWidth, graphicalAreaHeight), + rectangleCornerRadius, + rectangleCornerRadius ); + painter.setRenderHint(QPainter::Antialiasing, false); + + // Draw the ticks. + painter.setPen(Qt::black); + if( _tickInterval > 0.0 ) + { + int secTick = 1; + for( double currentTick = _min+_tickInterval; _max - currentTick > _tickInterval-1e-6; currentTick += _tickInterval ) + { + painter.translate( graphicalAreaWidth/(_max-_min) * _tickInterval, 0); + if( secTick == _secondaryTicks ) + { + painter.drawLine( QPointF(0,0.25*graphicalAreaHeight), QPointF(0,0.75*graphicalAreaHeight) ); + secTick = 1; + } + else + { + painter.drawLine( QPointF(0,0.333*graphicalAreaHeight), QPointF(0,0.666*graphicalAreaHeight) ); + ++secTick; + } + } + } + + return; +} + + +void RangedSlider::moveEvent(QMoveEvent *event) { + // If we've moved, we might be on a new screen with a different DPI resolution... + // .:TBD:. This almost certainly needs further work and further testing. It's far from clear whether our font size + // querying will give different answers just because the app has been moved from one screen to another. + this->recalculateHeightInPixels(); + + QWidget::moveEvent(event); + return; +} diff -r 409d9c7214be -r fb0bb9a2a7e1 src/RangedSlider.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/RangedSlider.h Wed Mar 30 15:31:57 2022 +0200 @@ -0,0 +1,172 @@ +/* + * RangedSlider.h is part bmsapp. + * + * Original written for Brewtarget, and is Copyright the following + * authors 2009-2020 + * - Matt Young + * - Mik Firestone + * - Philip G. Lee + * + * bmsapp 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. + * + * bmsapp 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 RANGEDSLIDER_H +#define RANGEDSLIDER_H + +#include +#include +#include +#include +#include +class QPaintEvent; +class QMouseEvent; + +/** + * @brief Widget to display a number with an optional range on a type of read-only slider. + * @author Philip G. Lee + */ +class RangedSlider : public QWidget +{ + Q_OBJECT + +public: + RangedSlider(QWidget* parent=0); + + Q_PROPERTY( double value READ value WRITE setValue ) + + double value() const { return _val; } + + //! \brief Set the background brush for the widget. + void setBackgroundBrush( QBrush const& brush ); + //! \brief Set the brush for the preffered range. + void setPreferredRangeBrush( QBrush const& brush ); + //! \brief Set the pen for the preferred range + void setPreferredRangePen( QPen const& pen ); + //! \brief Set the brush for the marker. + void setMarkerBrush( QBrush const& brush ); + //! \brief Set the text displayed above the marker. + void setMarkerText( QString const& text ); + //! \brief If true, the marker text will always be updated to the value given by \c setValue(). + void setMarkerTextIsValue(bool val); + + /*! + * \brief Set the tick mark intervals. + * + * If either parameter is <= 0, then the tick marks are not drawn. + * + * \param primaryInterval How often to draw big tick marks. + * \param secondaryTicks Number of secondary ticks per primary tick. + */ + void setTickMarks( double primaryInterval, int secondaryTicks = 1 ); + + //! \brief Set the \c precision for displaying values. + void setPrecision(int precision); + + //! \brief Reimplemented from QWidget. + virtual QSize sizeHint() const; + +public slots: + + //! \brief Set the \c value for the indicator. + void setValue(double value); + + /*! + * \brief Set the range of values considered to be *best* + * + * \param range \c range.first and \c range.second are the min and max + * values for the preferred range resp. + */ + void setPreferredRange(QPair range); + + /*! + * \brief Set the range of values that the widget displays + * + * \param range \c range.first and \c range.second are the min and max + * values for the preferred range resp. + */ + void setRange(QPair range); + + //! \brief Convenience method for setting the widget range + void setRange( double min, double max ); + + //! \brief Convenience method for setting the preferred range + // Note that this is completely unrelated to "preferred size". + void setPreferredRange( double min, double max ); + +protected: + //! \brief Reimplemented from QWidget. + virtual void paintEvent(QPaintEvent* event); + //! \brief Reimplemented from QWidget for popup on mouseover. + virtual void mouseMoveEvent(QMouseEvent* event); + //! \brief Reimplemented from QWidget. + virtual void moveEvent(QMoveEvent *event); + +private: + /** + * Sets minimum / maximum sizes and resize policy + */ + void setSizes(); + void recalculateHeightInPixels() const; + + /** + * Minimum value the widget displays + */ + double _min; + /** + * Maximum value the widget displays + */ + double _max; + + /** + * Minimum value for the "best" sub-range + */ + double _prefMin; + /** + * Maximum value for the "best" sub-range + */ + double _prefMax; + double _val; + QString _valText; + QString _markerText; + int _prec; + double _tickInterval; + int _secondaryTicks; + QString _tooltipText; + QBrush _bgBrush; + QBrush _prefRangeBrush; + QPen _prefRangePen; + QBrush _markerBrush; + bool _markerTextIsValue; + + /** + * The font used for showing the value at the right-hand side of the slider + */ + QFont const valueTextFont; + + /** + * The font used for showing the indicator above the "needle" on the slider. Often this is just showing the same as + * the value - eg OG, FG, ABV - but sometimes it's something else - eg descriptive text such as "slightly hoppy" for + * the IBU/GU reading. + */ + QFont const indicatorTextFont; + + /** + * Since preferred and minimum dimensions are all based off a height we need to calculate based on the resolution of + * the current display (see more detailed comment in implementation of recalculateSizes()), it is useful to store + * that height here. However, its value is not really part of the current value/state of the object, hence mutable + * (ie OK to change in a const function). + */ + mutable int heightInPixels; +}; + +#endif diff -r 409d9c7214be -r fb0bb9a2a7e1 src/RecipesTree.cpp --- a/src/RecipesTree.cpp Mon Mar 28 16:54:08 2022 +0200 +++ b/src/RecipesTree.cpp Wed Mar 30 15:31:57 2022 +0200 @@ -16,6 +16,7 @@ */ #include "RecipesTree.h" #include "MainWindow.h" +#include "EditRecipe.h" #include "config.h" @@ -394,11 +395,11 @@ void RecipesTree::edit(int recno) { qDebug() << "edit" << recno; -// EditMisc dialog(recno, this); + EditRecipe dialog(recno, this); /* Signal from editor if a refresh is needed */ -// connect(&dialog, SIGNAL(entry_changed()), this, SLOT(refreshTable())); -// dialog.setModal(true); -// dialog.exec(); + connect(&dialog, SIGNAL(entry_changed()), this, SLOT(refreshTable())); + dialog.setModal(true); + dialog.exec(); } diff -r 409d9c7214be -r fb0bb9a2a7e1 src/Utils.cpp --- a/src/Utils.cpp Mon Mar 28 16:54:08 2022 +0200 +++ b/src/Utils.cpp Wed Mar 30 15:31:57 2022 +0200 @@ -57,7 +57,7 @@ QString Utils::hours_to_string(int hours) { - int dd, hh, ww; + int dd, hh; if (hours == 1) return QObject::tr("1 hour"); diff -r 409d9c7214be -r fb0bb9a2a7e1 ui/EditRecipe.ui --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ui/EditRecipe.ui Wed Mar 30 15:31:57 2022 +0200 @@ -0,0 +1,497 @@ + + + EditRecipe + + + + 0 + 0 + 1152 + 560 + + + + Dialog + + + + + + + + 90 + 510 + 80 + 23 + + + + + 0 + 0 + + + + Quit + + + + :icons/silk/door_out.png:icons/silk/door_out.png + + + + + true + + + + 940 + 510 + 80 + 23 + + + + Save + + + + :icons/silk/disk.png:icons/silk/disk.png + + + + + true + + + + 520 + 510 + 80 + 23 + + + + Delete + + + + :icons/silk/delete.png:icons/silk/delete.png + + + + + + 0 + 0 + 1131 + 501 + + + + QTabWidget::North + + + QTabWidget::Rounded + + + 0 + + + Qt::ElideNone + + + true + + + false + + + false + + + + + :/icons/bms/beerstyles.png:/icons/bms/beerstyles.png + + + Generic + + + + + 0 + 20 + 131 + 20 + + + + Recipe name: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 0 + 50 + 131 + 20 + + + + Recipe notes: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 140 + 20 + 741 + 23 + + + + + + + 140 + 50 + 881 + 81 + + + + + + + 900 + 20 + 131 + 20 + + + + Read only: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 1040 + 20 + 61 + 21 + + + + Yes + + + + + + 0 + 140 + 131 + 20 + + + + Brew type: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 140 + 140 + 181 + 23 + + + + + + + 380 + 140 + 131 + 20 + + + + Efficiency: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 380 + 170 + 131 + 20 + + + + Boil time: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 760 + 140 + 131 + 20 + + + + Batch size: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 760 + 170 + 131 + 20 + + + + Boil size: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 520 + 140 + 101 + 24 + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + true + + + % + + + 1 + + + 0.100000000000000 + + + + + + 520 + 170 + 101 + 24 + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + true + + + min + + + 1536 + + + + + + 900 + 140 + 101 + 24 + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + true + + + L + + + 1 + + + 100000.000000000000000 + + + 0.500000000000000 + + + + + + 900 + 170 + 101 + 24 + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + true + + + L + + + 1 + + + 100000.000000000000000 + + + 0.500000000000000 + + + + + + + :/icons/bms/graan.png:/icons/bms/graan.png + + + Fermentables + + + + + + :/icons/bms/hop.png:/icons/bms/hop.png + + + Hops + + + + + + :/icons/bms/peper.png:/icons/bms/peper.png + + + Miscs + + + + + + :/icons/bms/erlenmeyer.png:/icons/bms/erlenmeyer.png + + + Yeasts + + + + + + :/icons/bms/mash.png:/icons/bms/mash.png + + + Mash + + + + + + :/icons/bms/water.png:/icons/bms/water.png + + + Water + + + + + + + 300 + 510 + 80 + 23 + + + + Export + + + + :/icons/silk/disk_multiple.png:/icons/silk/disk_multiple.png + + + + + + 730 + 510 + 80 + 23 + + + + Print + + + + :/icons/silk/printer.png:/icons/silk/printer.png + + + + + + + + quitButton + deleteButton + saveButton + + + + + +