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.

Wed, 30 Mar 2022 15:31:57 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Wed, 30 Mar 2022 15:31:57 +0200
changeset 92
fb0bb9a2a7e1
parent 91
409d9c7214be
child 93
4cfd0dd17fa5

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.

CMakeLists.txt file | annotate | diff | comparison | revisions
src/EditRecipe.cpp file | annotate | diff | comparison | revisions
src/EditRecipe.h file | annotate | diff | comparison | revisions
src/RangedSlider.cpp file | annotate | diff | comparison | revisions
src/RangedSlider.h file | annotate | diff | comparison | revisions
src/RecipesTree.cpp file | annotate | diff | comparison | revisions
src/Utils.cpp file | annotate | diff | comparison | revisions
ui/EditRecipe.ui file | annotate | diff | comparison | revisions
--- 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()
+
--- /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 <http://www.gnu.org/licenses/>.
+ */
+#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);
+}
--- /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 <QDialog>
+
+
+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
--- /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 <mfsy@yahoo.com>
+ * - Mik Firestone <mikfire@gmail.com>
+ * - Philip G. Lee <rocketman768@gmail.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include "RangedSlider.h"
+//#include "brewtarget.h"
+#include <QPaintEvent>
+#include <QPainter>
+#include <QColor>
+#include <QPalette>
+#include <QApplication>
+#include <QRectF>
+#include <QFont>
+#include <QFontMetrics>
+#include <QMouseEvent>
+#include <QLabel>
+#include <QToolTip>
+#include <QLinearGradient>
+#include <QPainterPath>
+
+#include <QDebug>
+
+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<double,double> minmax)
+{
+   setPreferredRange( minmax.first, minmax.second );
+}
+
+void RangedSlider::setRange( double min, double max )
+{
+   _min = min;
+   _max = max;
+   update();
+}
+
+void RangedSlider::setRange(QPair<double,double> 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;
+}
--- /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 <mfsy@yahoo.com>
+ * - Mik Firestone <mikfire@gmail.com>
+ * - Philip G. Lee <rocketman768@gmail.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+#ifndef RANGEDSLIDER_H
+#define RANGEDSLIDER_H
+
+#include <QWidget>
+#include <QSize>
+#include <QString>
+#include <QBrush>
+#include <QPen>
+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<double,double> 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<double,double> 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
--- 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();
 }
 
 
--- 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");
--- /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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>EditRecipe</class>
+ <widget class="QDialog" name="EditRecipe">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>1152</width>
+    <height>560</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Dialog</string>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="0" column="0">
+    <widget class="QWidget" name="topWidget" native="true">
+     <widget class="QPushButton" name="quitButton">
+      <property name="geometry">
+       <rect>
+        <x>90</x>
+        <y>510</y>
+        <width>80</width>
+        <height>23</height>
+       </rect>
+      </property>
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <property name="text">
+       <string>Quit</string>
+      </property>
+      <property name="icon">
+       <iconset>
+        <normaloff>:icons/silk/door_out.png</normaloff>:icons/silk/door_out.png</iconset>
+      </property>
+     </widget>
+     <widget class="QPushButton" name="saveButton">
+      <property name="enabled">
+       <bool>true</bool>
+      </property>
+      <property name="geometry">
+       <rect>
+        <x>940</x>
+        <y>510</y>
+        <width>80</width>
+        <height>23</height>
+       </rect>
+      </property>
+      <property name="text">
+       <string>Save</string>
+      </property>
+      <property name="icon">
+       <iconset>
+        <normaloff>:icons/silk/disk.png</normaloff>:icons/silk/disk.png</iconset>
+      </property>
+     </widget>
+     <widget class="QPushButton" name="deleteButton">
+      <property name="enabled">
+       <bool>true</bool>
+      </property>
+      <property name="geometry">
+       <rect>
+        <x>520</x>
+        <y>510</y>
+        <width>80</width>
+        <height>23</height>
+       </rect>
+      </property>
+      <property name="text">
+       <string>Delete</string>
+      </property>
+      <property name="icon">
+       <iconset>
+        <normaloff>:icons/silk/delete.png</normaloff>:icons/silk/delete.png</iconset>
+      </property>
+     </widget>
+     <widget class="QTabWidget" name="tabWidget">
+      <property name="geometry">
+       <rect>
+        <x>0</x>
+        <y>0</y>
+        <width>1131</width>
+        <height>501</height>
+       </rect>
+      </property>
+      <property name="tabPosition">
+       <enum>QTabWidget::North</enum>
+      </property>
+      <property name="tabShape">
+       <enum>QTabWidget::Rounded</enum>
+      </property>
+      <property name="currentIndex">
+       <number>0</number>
+      </property>
+      <property name="elideMode">
+       <enum>Qt::ElideNone</enum>
+      </property>
+      <property name="usesScrollButtons">
+       <bool>true</bool>
+      </property>
+      <property name="documentMode">
+       <bool>false</bool>
+      </property>
+      <property name="tabBarAutoHide">
+       <bool>false</bool>
+      </property>
+      <widget class="QWidget" name="generic">
+       <attribute name="icon">
+        <iconset resource="../../../../../../home/mbroek/MyProjects/bmsapp/resources/icons.qrc">
+         <normaloff>:/icons/bms/beerstyles.png</normaloff>:/icons/bms/beerstyles.png</iconset>
+       </attribute>
+       <attribute name="title">
+        <string>Generic</string>
+       </attribute>
+       <widget class="QLabel" name="nameLabel">
+        <property name="geometry">
+         <rect>
+          <x>0</x>
+          <y>20</y>
+          <width>131</width>
+          <height>20</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Recipe name:</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+       </widget>
+       <widget class="QLabel" name="notesLabel">
+        <property name="geometry">
+         <rect>
+          <x>0</x>
+          <y>50</y>
+          <width>131</width>
+          <height>20</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Recipe notes:</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+       </widget>
+       <widget class="QLineEdit" name="nameEdit">
+        <property name="geometry">
+         <rect>
+          <x>140</x>
+          <y>20</y>
+          <width>741</width>
+          <height>23</height>
+         </rect>
+        </property>
+       </widget>
+       <widget class="QPlainTextEdit" name="notesEdit">
+        <property name="geometry">
+         <rect>
+          <x>140</x>
+          <y>50</y>
+          <width>881</width>
+          <height>81</height>
+         </rect>
+        </property>
+       </widget>
+       <widget class="QLabel" name="lockedLabel">
+        <property name="geometry">
+         <rect>
+          <x>900</x>
+          <y>20</y>
+          <width>131</width>
+          <height>20</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Read only:</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+       </widget>
+       <widget class="QCheckBox" name="lockedEdit">
+        <property name="geometry">
+         <rect>
+          <x>1040</x>
+          <y>20</y>
+          <width>61</width>
+          <height>21</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Yes</string>
+        </property>
+       </widget>
+       <widget class="QLabel" name="typeLabel">
+        <property name="geometry">
+         <rect>
+          <x>0</x>
+          <y>140</y>
+          <width>131</width>
+          <height>20</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Brew type:</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+       </widget>
+       <widget class="QComboBox" name="typeEdit">
+        <property name="geometry">
+         <rect>
+          <x>140</x>
+          <y>140</y>
+          <width>181</width>
+          <height>23</height>
+         </rect>
+        </property>
+       </widget>
+       <widget class="QLabel" name="efficiencyLabel">
+        <property name="geometry">
+         <rect>
+          <x>380</x>
+          <y>140</y>
+          <width>131</width>
+          <height>20</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Efficiency:</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+       </widget>
+       <widget class="QLabel" name="boil_timeLabel">
+        <property name="geometry">
+         <rect>
+          <x>380</x>
+          <y>170</y>
+          <width>131</width>
+          <height>20</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Boil time:</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+       </widget>
+       <widget class="QLabel" name="batch_sizeLabel">
+        <property name="geometry">
+         <rect>
+          <x>760</x>
+          <y>140</y>
+          <width>131</width>
+          <height>20</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Batch size:</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+       </widget>
+       <widget class="QLabel" name="boil_sizeLabel">
+        <property name="geometry">
+         <rect>
+          <x>760</x>
+          <y>170</y>
+          <width>131</width>
+          <height>20</height>
+         </rect>
+        </property>
+        <property name="text">
+         <string>Boil size:</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+       </widget>
+       <widget class="QDoubleSpinBox" name="efficiencyEdit">
+        <property name="geometry">
+         <rect>
+          <x>520</x>
+          <y>140</y>
+          <width>101</width>
+          <height>24</height>
+         </rect>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+        <property name="accelerated">
+         <bool>true</bool>
+        </property>
+        <property name="suffix">
+         <string> %</string>
+        </property>
+        <property name="decimals">
+         <number>1</number>
+        </property>
+        <property name="singleStep">
+         <double>0.100000000000000</double>
+        </property>
+       </widget>
+       <widget class="QSpinBox" name="boil_timeEdit">
+        <property name="geometry">
+         <rect>
+          <x>520</x>
+          <y>170</y>
+          <width>101</width>
+          <height>24</height>
+         </rect>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+        <property name="accelerated">
+         <bool>true</bool>
+        </property>
+        <property name="suffix">
+         <string> min</string>
+        </property>
+        <property name="maximum">
+         <number>1536</number>
+        </property>
+       </widget>
+       <widget class="QDoubleSpinBox" name="batch_sizeEdit">
+        <property name="geometry">
+         <rect>
+          <x>900</x>
+          <y>140</y>
+          <width>101</width>
+          <height>24</height>
+         </rect>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+        <property name="accelerated">
+         <bool>true</bool>
+        </property>
+        <property name="suffix">
+         <string> L</string>
+        </property>
+        <property name="decimals">
+         <number>1</number>
+        </property>
+        <property name="maximum">
+         <double>100000.000000000000000</double>
+        </property>
+        <property name="singleStep">
+         <double>0.500000000000000</double>
+        </property>
+       </widget>
+       <widget class="QDoubleSpinBox" name="boil_sizeEdit">
+        <property name="geometry">
+         <rect>
+          <x>900</x>
+          <y>170</y>
+          <width>101</width>
+          <height>24</height>
+         </rect>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+        <property name="accelerated">
+         <bool>true</bool>
+        </property>
+        <property name="suffix">
+         <string> L</string>
+        </property>
+        <property name="decimals">
+         <number>1</number>
+        </property>
+        <property name="maximum">
+         <double>100000.000000000000000</double>
+        </property>
+        <property name="singleStep">
+         <double>0.500000000000000</double>
+        </property>
+       </widget>
+      </widget>
+      <widget class="QWidget" name="fermentables">
+       <attribute name="icon">
+        <iconset resource="../../../../../../home/mbroek/MyProjects/bmsapp/resources/icons.qrc">
+         <normaloff>:/icons/bms/graan.png</normaloff>:/icons/bms/graan.png</iconset>
+       </attribute>
+       <attribute name="title">
+        <string>Fermentables</string>
+       </attribute>
+      </widget>
+      <widget class="QWidget" name="hops">
+       <attribute name="icon">
+        <iconset resource="../../../../../../home/mbroek/MyProjects/bmsapp/resources/icons.qrc">
+         <normaloff>:/icons/bms/hop.png</normaloff>:/icons/bms/hop.png</iconset>
+       </attribute>
+       <attribute name="title">
+        <string>Hops</string>
+       </attribute>
+      </widget>
+      <widget class="QWidget" name="miscs">
+       <attribute name="icon">
+        <iconset resource="../../../../../../home/mbroek/MyProjects/bmsapp/resources/icons.qrc">
+         <normaloff>:/icons/bms/peper.png</normaloff>:/icons/bms/peper.png</iconset>
+       </attribute>
+       <attribute name="title">
+        <string>Miscs</string>
+       </attribute>
+      </widget>
+      <widget class="QWidget" name="yeasts">
+       <attribute name="icon">
+        <iconset resource="../../../../../../home/mbroek/MyProjects/bmsapp/resources/icons.qrc">
+         <normaloff>:/icons/bms/erlenmeyer.png</normaloff>:/icons/bms/erlenmeyer.png</iconset>
+       </attribute>
+       <attribute name="title">
+        <string>Yeasts</string>
+       </attribute>
+      </widget>
+      <widget class="QWidget" name="mash">
+       <attribute name="icon">
+        <iconset resource="../../../../../../home/mbroek/MyProjects/bmsapp/resources/icons.qrc">
+         <normaloff>:/icons/bms/mash.png</normaloff>:/icons/bms/mash.png</iconset>
+       </attribute>
+       <attribute name="title">
+        <string>Mash</string>
+       </attribute>
+      </widget>
+      <widget class="QWidget" name="water">
+       <attribute name="icon">
+        <iconset resource="../../../../../../home/mbroek/MyProjects/bmsapp/resources/icons.qrc">
+         <normaloff>:/icons/bms/water.png</normaloff>:/icons/bms/water.png</iconset>
+       </attribute>
+       <attribute name="title">
+        <string>Water</string>
+       </attribute>
+      </widget>
+     </widget>
+     <widget class="QPushButton" name="exportButton">
+      <property name="geometry">
+       <rect>
+        <x>300</x>
+        <y>510</y>
+        <width>80</width>
+        <height>23</height>
+       </rect>
+      </property>
+      <property name="text">
+       <string>Export</string>
+      </property>
+      <property name="icon">
+       <iconset resource="../../../../../../home/mbroek/MyProjects/bmsapp/resources/icons.qrc">
+        <normaloff>:/icons/silk/disk_multiple.png</normaloff>:/icons/silk/disk_multiple.png</iconset>
+      </property>
+     </widget>
+     <widget class="QPushButton" name="printButton">
+      <property name="geometry">
+       <rect>
+        <x>730</x>
+        <y>510</y>
+        <width>80</width>
+        <height>23</height>
+       </rect>
+      </property>
+      <property name="text">
+       <string>Print</string>
+      </property>
+      <property name="icon">
+       <iconset resource="../../../../../../home/mbroek/MyProjects/bmsapp/resources/icons.qrc">
+        <normaloff>:/icons/silk/printer.png</normaloff>:/icons/silk/printer.png</iconset>
+      </property>
+     </widget>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <tabstops>
+  <tabstop>quitButton</tabstop>
+  <tabstop>deleteButton</tabstop>
+  <tabstop>saveButton</tabstop>
+ </tabstops>
+ <resources>
+  <include location="../../../../../../home/mbroek/MyProjects/bmsapp/resources/icons.qrc"/>
+ </resources>
+ <connections/>
+</ui>

mercurial