diff options
author | Tomaz Canabrava <tomaz.canabrava@intel.com> | 2015-09-03 14:20:19 -0300 |
---|---|---|
committer | Dirk Hohndel <dirk@hohndel.org> | 2015-10-30 10:36:49 -0700 |
commit | e49d6213ad129284a45d53c3fcdc03249e84efe2 (patch) | |
tree | 2946a666ab38af3375e7bb2b8c5dd887d4a7f9a1 /desktop-widgets | |
parent | 588abd019fb2ed3f607682f2b6c7fe86a7a5bb90 (diff) | |
download | subsurface-e49d6213ad129284a45d53c3fcdc03249e84efe2.tar.gz |
Move qt-ui to desktop-widgets
Since we have now destkop and mobile versions, 'qt-ui' was a very
poor name choice for a folder that contains only destkop-enabled
widgets.
Also, move the graphicsview-common.h/cpp to subsurface-core because
it doesn't depend on qgraphicsview, it merely implements all the
colors that we use throughout Subsurface, and we will use colors on both
desktop and mobile versions
Same thing applies for metrics.h/cpp
Signed-off-by: Tomaz Canabrava <tomaz.canabrava@intel.com>
Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
Diffstat (limited to 'desktop-widgets')
137 files changed, 38208 insertions, 0 deletions
diff --git a/desktop-widgets/CMakeLists.txt b/desktop-widgets/CMakeLists.txt new file mode 100644 index 000000000..2c373b83f --- /dev/null +++ b/desktop-widgets/CMakeLists.txt @@ -0,0 +1,111 @@ +# create the libraries +file(GLOB SUBSURFACE_UI *.ui) +qt5_wrap_ui(SUBSURFACE_UI_HDRS ${SUBSURFACE_UI}) +qt5_add_resources(SUBSURFACE_RESOURCES subsurface.qrc) +source_group("Subsurface Interface Files" FILES ${SUBSURFACE_UI}) + +if(BTSUPPORT) + set(BT_SRC_FILES btdeviceselectiondialog.cpp) +endif() + +include_directories(. + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_BINARY_DIR} +) + +# the interface, in C++ +set(SUBSURFACE_INTERFACE + updatemanager.cpp + about.cpp + divecomputermanagementdialog.cpp + divelistview.cpp + diveplanner.cpp + diveshareexportdialog.cpp + downloadfromdivecomputer.cpp + globe.cpp + kmessagewidget.cpp + maintab.cpp + mainwindow.cpp + modeldelegates.cpp + notificationwidget.cpp + preferences.cpp + simplewidgets.cpp + starwidget.cpp + subsurfacewebservices.cpp + tableview.cpp + divelogimportdialog.cpp + tagwidget.cpp + groupedlineedit.cpp + divelogexportdialog.cpp + divepicturewidget.cpp + usersurvey.cpp + configuredivecomputerdialog.cpp + undocommands.cpp + locationinformation.cpp + qtwaitingspinner.cpp +) + +if(NOT NO_USERMANUAL) + set(SUBSURFACE_INTERFACE ${SUBSURFACE_INTERFACE} + usermanual.cpp + ) +endif() + +if(NOT NO_PRINTING) + set(SUBSURFACE_INTERFACE ${SUBSURFACE_INTERFACE} + templateedit.cpp + printdialog.cpp + printoptions.cpp + printer.cpp + templatelayout.cpp + ) +endif() + +if (FBSUPPORT) + set(SUBSURFACE_INTERFACE ${SUBSURFACE_INTERFACE} + socialnetworks.cpp + ) +endif() + +if (BTSUPPORT) + set(SUBSURFACE_INTERFACE ${SUBSURFACE_INTERFACE} + btdeviceselectiondialog.cpp + ) +endif() + +source_group("Subsurface Interface" FILES ${SUBSURFACE_INTERFACE}) + +# the profile widget +set(SUBSURFACE_PROFILE_LIB_SRCS + profile/profilewidget2.cpp + profile/diverectitem.cpp + profile/divepixmapitem.cpp + profile/divelineitem.cpp + profile/divetextitem.cpp + profile/animationfunctions.cpp + profile/divecartesianaxis.cpp + profile/diveprofileitem.cpp + profile/diveeventitem.cpp + profile/divetooltipitem.cpp + profile/ruleritem.cpp + profile/tankitem.cpp +) +source_group("Subsurface Profile" FILES ${SUBSURFACE_PROFILE_LIB_SRCS}) + +# the yearly statistics widget. +set(SUBSURFACE_STATISTICS_LIB_SRCS + statistics/statisticswidget.cpp + statistics/yearstatistics.cpp + statistics/statisticsbar.cpp + statistics/monthstatistics.cpp +) +source_group("Subsurface Statistics" FILES ${SUBSURFACE_STATISTICS_LIB_SRCS}) + +add_library(subsurface_profile STATIC ${SUBSURFACE_PROFILE_LIB_SRCS}) +target_link_libraries(subsurface_profile ${QT_LIBRARIES}) +add_library(subsurface_statistics STATIC ${SUBSURFACE_STATISTICS_LIB_SRCS}) +target_link_libraries(subsurface_statistics ${QT_LIBRARIES}) +add_library(subsurface_generated_ui STATIC ${SUBSURFACE_UI_HDRS}) +target_link_libraries(subsurface_generated_ui ${QT_LIBRARIES}) +add_library(subsurface_interface STATIC ${SUBSURFACE_INTERFACE}) +target_link_libraries(subsurface_interface ${QT_LIBRARIES} ${MARBLE_LIBRARIES}) diff --git a/desktop-widgets/about.cpp b/desktop-widgets/about.cpp new file mode 100644 index 000000000..e0df55980 --- /dev/null +++ b/desktop-widgets/about.cpp @@ -0,0 +1,40 @@ +#include "about.h" +#include "version.h" +#include <QDesktopServices> +#include <QUrl> +#include <QShortcut> + +SubsurfaceAbout::SubsurfaceAbout(QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f) +{ + ui.setupUi(this); + + setWindowModality(Qt::ApplicationModal); + QString versionString(subsurface_git_version()); + QStringList readableVersions = QStringList() << "4.4.96" << "4.5 Beta 1" << + "4.4.97" << "4.5 Beta 2" << + "4.4.98" << "4.5 Beta 3"; + if (readableVersions.contains(versionString)) + versionString = readableVersions[readableVersions.indexOf(versionString) + 1]; + + ui.aboutLabel->setText(tr("<span style='font-size: 18pt; font-weight: bold;'>" + "Subsurface %1 </span><br><br>" + "Multi-platform divelog software<br>" + "<span style='font-size: 8pt'>" + "Linus Torvalds, Dirk Hohndel, Tomaz Canabrava, and others, 2011-2015" + "</span>").arg(versionString)); + + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); +} + +void SubsurfaceAbout::on_licenseButton_clicked() +{ + QDesktopServices::openUrl(QUrl("http://www.gnu.org/licenses/gpl-2.0.txt")); +} + +void SubsurfaceAbout::on_websiteButton_clicked() +{ + QDesktopServices::openUrl(QUrl("http://subsurface-divelog.org")); +} diff --git a/desktop-widgets/about.h b/desktop-widgets/about.h new file mode 100644 index 000000000..47423aea2 --- /dev/null +++ b/desktop-widgets/about.h @@ -0,0 +1,21 @@ +#ifndef ABOUT_H +#define ABOUT_H + +#include <QDialog> +#include "ui_about.h" + +class SubsurfaceAbout : public QDialog { + Q_OBJECT + +public: + explicit SubsurfaceAbout(QWidget *parent = 0, Qt::WindowFlags f = 0); +private +slots: + void on_licenseButton_clicked(); + void on_websiteButton_clicked(); + +private: + Ui::SubsurfaceAbout ui; +}; + +#endif // ABOUT_H diff --git a/desktop-widgets/about.ui b/desktop-widgets/about.ui new file mode 100644 index 000000000..0c1735e26 --- /dev/null +++ b/desktop-widgets/about.ui @@ -0,0 +1,136 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SubsurfaceAbout</class> + <widget class="QDialog" name="SubsurfaceAbout"> + <property name="windowModality"> + <enum>Qt::WindowModal</enum> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>359</width> + <height>423</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>About Subsurface</string> + </property> + <property name="windowIcon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/subsurface-icon</normaloff>:/subsurface-icon</iconset> + </property> + <property name="modal"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout" stretch="0,1,0"> + <item> + <widget class="QLabel" name="subsurfaceIcon"> + <property name="text"> + <string/> + </property> + <property name="pixmap"> + <pixmap resource="../subsurface.qrc">:/subsurface-icon</pixmap> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="aboutLabel"> + <property name="text"> + <string/> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + <property name="margin"> + <number>10</number> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="licenseButton"> + <property name="text"> + <string>&License</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="websiteButton"> + <property name="text"> + <string>&Website</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="closeButton"> + <property name="text"> + <string>&Close</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + <resources> + <include location="../subsurface.qrc"/> + </resources> + <connections> + <connection> + <sender>closeButton</sender> + <signal>clicked()</signal> + <receiver>SubsurfaceAbout</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>290</x> + <y>411</y> + </hint> + <hint type="destinationlabel"> + <x>340</x> + <y>409</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/desktop-widgets/btdeviceselectiondialog.cpp b/desktop-widgets/btdeviceselectiondialog.cpp new file mode 100644 index 000000000..fb2cbc1f3 --- /dev/null +++ b/desktop-widgets/btdeviceselectiondialog.cpp @@ -0,0 +1,656 @@ +#include <QShortcut> +#include <QDebug> +#include <QMessageBox> +#include <QMenu> + +#include "ui_btdeviceselectiondialog.h" +#include "btdeviceselectiondialog.h" + +#if defined(Q_OS_WIN) +Q_DECLARE_METATYPE(QBluetoothDeviceDiscoveryAgent::Error) +#endif +#if QT_VERSION < 0x050500 +Q_DECLARE_METATYPE(QBluetoothDeviceInfo) +#endif + +BtDeviceSelectionDialog::BtDeviceSelectionDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::BtDeviceSelectionDialog), + remoteDeviceDiscoveryAgent(0) +{ + ui->setupUi(this); + + // Quit button callbacks + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), this, SLOT(reject())); + connect(ui->quit, SIGNAL(clicked()), this, SLOT(reject())); + + // Translate the UI labels + ui->localDeviceDetails->setTitle(tr("Local Bluetooth device details")); + ui->selectDeviceLabel->setText(tr("Select device:")); + ui->deviceAddressLabel->setText(tr("Address:")); + ui->deviceNameLabel->setText(tr("Name:")); + ui->deviceState->setText(tr("Bluetooth powered on")); + ui->changeDeviceState->setText(tr("Turn on/off")); + ui->discoveredDevicesLabel->setText(tr("Discovered devices")); + ui->scan->setText(tr("Scan")); + ui->clear->setText(tr("Clear")); + ui->save->setText(tr("Save")); + ui->quit->setText(tr("Quit")); + + // Disable the save button because there is no device selected + ui->save->setEnabled(false); + + // Add event for item selection + connect(ui->discoveredDevicesList, SIGNAL(itemClicked(QListWidgetItem*)), + this, SLOT(itemClicked(QListWidgetItem*))); + +#if defined(Q_OS_WIN) + ULONG ulRetCode = SUCCESS; + WSADATA WSAData = { 0 }; + + // Initialize WinSock and ask for version 2.2. + ulRetCode = WSAStartup(MAKEWORD(2, 2), &WSAData); + if (ulRetCode != SUCCESS) { + QMessageBox::StandardButton warningBox; + warningBox = QMessageBox::critical(this, "Bluetooth", + tr("Could not initialize Winsock version 2.2"), QMessageBox::Ok); + return; + } + + // Initialize the device discovery agent + initializeDeviceDiscoveryAgent(); + + // On Windows we cannot select a device or show information about the local device + ui->localDeviceDetails->hide(); +#else + // Initialize the local Bluetooth device + localDevice = new QBluetoothLocalDevice(); + + // Populate the list with local bluetooth devices + QList<QBluetoothHostInfo> localAvailableDevices = localDevice->allDevices(); + int availableDevicesSize = localAvailableDevices.size(); + + if (availableDevicesSize > 1) { + int defaultDeviceIndex = -1; + + for (int it = 0; it < availableDevicesSize; it++) { + QBluetoothHostInfo localAvailableDevice = localAvailableDevices.at(it); + ui->localSelectedDevice->addItem(localAvailableDevice.name(), + QVariant::fromValue(localAvailableDevice.address())); + + if (localDevice->address() == localAvailableDevice.address()) + defaultDeviceIndex = it; + } + + // Positionate the current index to the default device and register to index changes events + ui->localSelectedDevice->setCurrentIndex(defaultDeviceIndex); + connect(ui->localSelectedDevice, SIGNAL(currentIndexChanged(int)), + this, SLOT(localDeviceChanged(int))); + } else { + // If there is only one local Bluetooth adapter hide the combobox and the label + ui->selectDeviceLabel->hide(); + ui->localSelectedDevice->hide(); + } + + // Update the UI information about the local device + updateLocalDeviceInformation(); + + // Initialize the device discovery agent + if (localDevice->isValid()) + initializeDeviceDiscoveryAgent(); +#endif +} + +BtDeviceSelectionDialog::~BtDeviceSelectionDialog() +{ + delete ui; + +#if defined(Q_OS_WIN) + // Terminate the use of Winsock 2 DLL + WSACleanup(); +#else + // Clean the local device + delete localDevice; +#endif + if (remoteDeviceDiscoveryAgent) { + // Clean the device discovery agent + if (remoteDeviceDiscoveryAgent->isActive()) { + remoteDeviceDiscoveryAgent->stop(); +#if defined(Q_OS_WIN) + remoteDeviceDiscoveryAgent->wait(); +#endif + } + + delete remoteDeviceDiscoveryAgent; + } +} + +void BtDeviceSelectionDialog::on_changeDeviceState_clicked() +{ +#if defined(Q_OS_WIN) + // TODO add implementation +#else + if (localDevice->hostMode() == QBluetoothLocalDevice::HostPoweredOff) { + ui->dialogStatus->setText(tr("Trying to turn on the local Bluetooth device...")); + localDevice->powerOn(); + } else { + ui->dialogStatus->setText(tr("Trying to turn off the local Bluetooth device...")); + localDevice->setHostMode(QBluetoothLocalDevice::HostPoweredOff); + } +#endif +} + +void BtDeviceSelectionDialog::on_save_clicked() +{ + // Get the selected device. There will be always a selected device if the save button is enabled. + QListWidgetItem *currentItem = ui->discoveredDevicesList->currentItem(); + QBluetoothDeviceInfo remoteDeviceInfo = currentItem->data(Qt::UserRole).value<QBluetoothDeviceInfo>(); + + // Save the selected device + selectedRemoteDeviceInfo = QSharedPointer<QBluetoothDeviceInfo>(new QBluetoothDeviceInfo(remoteDeviceInfo)); + + if (remoteDeviceDiscoveryAgent->isActive()) { + // Stop the SDP agent if the clear button is pressed and enable the Scan button + remoteDeviceDiscoveryAgent->stop(); +#if defined(Q_OS_WIN) + remoteDeviceDiscoveryAgent->wait(); +#endif + ui->scan->setEnabled(true); + } + + // Close the device selection dialog and set the result code to Accepted + accept(); +} + +void BtDeviceSelectionDialog::on_clear_clicked() +{ + ui->dialogStatus->setText(tr("Remote devices list was cleared.")); + ui->discoveredDevicesList->clear(); + ui->save->setEnabled(false); + + if (remoteDeviceDiscoveryAgent->isActive()) { + // Stop the SDP agent if the clear button is pressed and enable the Scan button + remoteDeviceDiscoveryAgent->stop(); +#if defined(Q_OS_WIN) + remoteDeviceDiscoveryAgent->wait(); +#endif + ui->scan->setEnabled(true); + } +} + +void BtDeviceSelectionDialog::on_scan_clicked() +{ + ui->dialogStatus->setText(tr("Scanning for remote devices...")); + ui->discoveredDevicesList->clear(); + remoteDeviceDiscoveryAgent->start(); + ui->scan->setEnabled(false); +} + +void BtDeviceSelectionDialog::remoteDeviceScanFinished() +{ + if (remoteDeviceDiscoveryAgent->error() == QBluetoothDeviceDiscoveryAgent::NoError) { + ui->dialogStatus->setText(tr("Scanning finished successfully.")); + } else { + deviceDiscoveryError(remoteDeviceDiscoveryAgent->error()); + } + + ui->scan->setEnabled(true); +} + +void BtDeviceSelectionDialog::hostModeStateChanged(QBluetoothLocalDevice::HostMode mode) +{ +#if defined(Q_OS_WIN) + // TODO add implementation +#else + bool on = !(mode == QBluetoothLocalDevice::HostPoweredOff); + + //: %1 will be replaced with "turned on" or "turned off" + ui->dialogStatus->setText(tr("The local Bluetooth device was %1.") + .arg(on? tr("turned on") : tr("turned off"))); + ui->deviceState->setChecked(on); + ui->scan->setEnabled(on); +#endif +} + +void BtDeviceSelectionDialog::addRemoteDevice(const QBluetoothDeviceInfo &remoteDeviceInfo) +{ +#if defined(Q_OS_WIN) + // On Windows we cannot obtain the pairing status so we set only the name and the address of the device + QString deviceLabel = QString("%1 (%2)").arg(remoteDeviceInfo.name(), + remoteDeviceInfo.address().toString()); + QColor pairingColor = QColor(Qt::white); +#else + // By default we use the status label and the color for the UNPAIRED state + QColor pairingColor = QColor(Qt::red); + QString pairingStatusLabel = tr("UNPAIRED"); + QBluetoothLocalDevice::Pairing pairingStatus = localDevice->pairingStatus(remoteDeviceInfo.address()); + + if (pairingStatus == QBluetoothLocalDevice::Paired) { + pairingStatusLabel = tr("PAIRED"); + pairingColor = QColor(Qt::gray); + } else if (pairingStatus == QBluetoothLocalDevice::AuthorizedPaired) { + pairingStatusLabel = tr("AUTHORIZED_PAIRED"); + pairingColor = QColor(Qt::blue); + } + + QString deviceLabel = tr("%1 (%2) [State: %3]").arg(remoteDeviceInfo.name(), + remoteDeviceInfo.address().toString(), + pairingStatusLabel); +#endif + // Create the new item, set its information and add it to the list + QListWidgetItem *item = new QListWidgetItem(deviceLabel); + + item->setData(Qt::UserRole, QVariant::fromValue(remoteDeviceInfo)); + item->setBackgroundColor(pairingColor); + + ui->discoveredDevicesList->addItem(item); +} + +void BtDeviceSelectionDialog::itemClicked(QListWidgetItem *item) +{ + // By default we assume that the devices are paired + QBluetoothDeviceInfo remoteDeviceInfo = item->data(Qt::UserRole).value<QBluetoothDeviceInfo>(); + QString statusMessage = tr("The device %1 can be used for connection. You can press the Save button.") + .arg(remoteDeviceInfo.address().toString()); + bool enableSaveButton = true; + +#if !defined(Q_OS_WIN) + // On other platforms than Windows we can obtain the pairing status so if the devices are not paired we disable the button + QBluetoothLocalDevice::Pairing pairingStatus = localDevice->pairingStatus(remoteDeviceInfo.address()); + + if (pairingStatus == QBluetoothLocalDevice::Unpaired) { + statusMessage = tr("The device %1 must be paired in order to be used. Please use the context menu for pairing options.") + .arg(remoteDeviceInfo.address().toString()); + enableSaveButton = false; + } +#endif + // Update the status message and the save button + ui->dialogStatus->setText(statusMessage); + ui->save->setEnabled(enableSaveButton); +} + +void BtDeviceSelectionDialog::localDeviceChanged(int index) +{ +#if defined(Q_OS_WIN) + // TODO add implementation +#else + QBluetoothAddress localDeviceSelectedAddress = ui->localSelectedDevice->itemData(index, Qt::UserRole).value<QBluetoothAddress>(); + + // Delete the old localDevice + if (localDevice) + delete localDevice; + + // Create a new local device using the selected address + localDevice = new QBluetoothLocalDevice(localDeviceSelectedAddress); + + ui->dialogStatus->setText(tr("The local device was changed.")); + + // Clear the discovered devices list + on_clear_clicked(); + + // Update the UI information about the local device + updateLocalDeviceInformation(); + + // Initialize the device discovery agent + if (localDevice->isValid()) + initializeDeviceDiscoveryAgent(); +#endif +} + +void BtDeviceSelectionDialog::displayPairingMenu(const QPoint &pos) +{ +#if defined(Q_OS_WIN) + // TODO add implementation +#else + QMenu menu(this); + QAction *pairAction = menu.addAction(tr("Pair")); + QAction *removePairAction = menu.addAction(tr("Remove pairing")); + QAction *chosenAction = menu.exec(ui->discoveredDevicesList->viewport()->mapToGlobal(pos)); + QListWidgetItem *currentItem = ui->discoveredDevicesList->currentItem(); + QBluetoothDeviceInfo currentRemoteDeviceInfo = currentItem->data(Qt::UserRole).value<QBluetoothDeviceInfo>(); + QBluetoothLocalDevice::Pairing pairingStatus = localDevice->pairingStatus(currentRemoteDeviceInfo.address()); + + //TODO: disable the actions + if (pairingStatus == QBluetoothLocalDevice::Unpaired) { + pairAction->setEnabled(true); + removePairAction->setEnabled(false); + } else { + pairAction->setEnabled(false); + removePairAction->setEnabled(true); + } + + if (chosenAction == pairAction) { + ui->dialogStatus->setText(tr("Trying to pair device %1") + .arg(currentRemoteDeviceInfo.address().toString())); + localDevice->requestPairing(currentRemoteDeviceInfo.address(), QBluetoothLocalDevice::Paired); + } else if (chosenAction == removePairAction) { + ui->dialogStatus->setText(tr("Trying to unpair device %1") + .arg(currentRemoteDeviceInfo.address().toString())); + localDevice->requestPairing(currentRemoteDeviceInfo.address(), QBluetoothLocalDevice::Unpaired); + } +#endif +} + +void BtDeviceSelectionDialog::pairingFinished(const QBluetoothAddress &address, QBluetoothLocalDevice::Pairing pairing) +{ + // Determine the color, the new pairing status and the log message. By default we assume that the devices are UNPAIRED. + QString remoteDeviceStringAddress = address.toString(); + QColor pairingColor = QColor(Qt::red); + QString pairingStatusLabel = tr("UNPAIRED"); + QString dialogStatusMessage = tr("Device %1 was unpaired.").arg(remoteDeviceStringAddress); + bool enableSaveButton = false; + + if (pairing == QBluetoothLocalDevice::Paired) { + pairingStatusLabel = tr("PAIRED"); + pairingColor = QColor(Qt::gray); + enableSaveButton = true; + dialogStatusMessage = tr("Device %1 was paired.").arg(remoteDeviceStringAddress); + } else if (pairing == QBluetoothLocalDevice::AuthorizedPaired) { + pairingStatusLabel = tr("AUTHORIZED_PAIRED"); + pairingColor = QColor(Qt::blue); + enableSaveButton = true; + dialogStatusMessage = tr("Device %1 was paired and is authorized.").arg(remoteDeviceStringAddress); + } + + // Find the items which represent the BTH device and update their state + QList<QListWidgetItem *> items = ui->discoveredDevicesList->findItems(remoteDeviceStringAddress, Qt::MatchContains); + QRegularExpression pairingExpression = QRegularExpression(QString("%1|%2|%3").arg(tr("PAIRED"), + tr("AUTHORIZED_PAIRED"), + tr("UNPAIRED"))); + + for (int i = 0; i < items.count(); ++i) { + QListWidgetItem *item = items.at(i); + QString updatedDeviceLabel = item->text().replace(QRegularExpression(pairingExpression), + pairingStatusLabel); + + item->setText(updatedDeviceLabel); + item->setBackgroundColor(pairingColor); + } + + // Check if the updated device is the selected one from the list and inform the user that it can/cannot start the download mode + QListWidgetItem *currentItem = ui->discoveredDevicesList->currentItem(); + + if (currentItem != NULL && currentItem->text().contains(remoteDeviceStringAddress, Qt::CaseInsensitive)) { + if (pairing == QBluetoothLocalDevice::Unpaired) { + dialogStatusMessage = tr("The device %1 must be paired in order to be used. Please use the context menu for pairing options.") + .arg(remoteDeviceStringAddress); + } else { + dialogStatusMessage = tr("The device %1 can now be used for connection. You can press the Save button.") + .arg(remoteDeviceStringAddress); + } + } + + // Update the save button and the dialog status message + ui->save->setEnabled(enableSaveButton); + ui->dialogStatus->setText(dialogStatusMessage); +} + +void BtDeviceSelectionDialog::error(QBluetoothLocalDevice::Error error) +{ + ui->dialogStatus->setText(tr("Local device error: %1.") + .arg((error == QBluetoothLocalDevice::PairingError)? tr("Pairing error. If the remote device requires a custom PIN code, " + "please try to pair the devices using your operating system. ") + : tr("Unknown error"))); +} + +void BtDeviceSelectionDialog::deviceDiscoveryError(QBluetoothDeviceDiscoveryAgent::Error error) +{ + QString errorDescription; + + switch (error) { + case QBluetoothDeviceDiscoveryAgent::PoweredOffError: + errorDescription = tr("The Bluetooth adaptor is powered off, power it on before doing discovery."); + break; + case QBluetoothDeviceDiscoveryAgent::InputOutputError: + errorDescription = tr("Writing to or reading from the device resulted in an error."); + break; + default: +#if defined(Q_OS_WIN) + errorDescription = remoteDeviceDiscoveryAgent->errorToString(); +#else + errorDescription = tr("An unknown error has occurred."); +#endif + break; + } + + ui->dialogStatus->setText(tr("Device discovery error: %1.").arg(errorDescription)); +} + +QString BtDeviceSelectionDialog::getSelectedDeviceAddress() +{ + if (selectedRemoteDeviceInfo) { + return selectedRemoteDeviceInfo.data()->address().toString(); + } + + return QString(); +} + +QString BtDeviceSelectionDialog::getSelectedDeviceName() +{ + if (selectedRemoteDeviceInfo) { + return selectedRemoteDeviceInfo.data()->name(); + } + + return QString(); +} + +void BtDeviceSelectionDialog::updateLocalDeviceInformation() +{ +#if defined(Q_OS_WIN) + // TODO add implementation +#else + // Check if the selected Bluetooth device can be accessed + if (!localDevice->isValid()) { + QString na = tr("Not available"); + + // Update the UI information + ui->deviceAddress->setText(na); + ui->deviceName->setText(na); + + // Announce the user that there is a problem with the selected local Bluetooth adapter + ui->dialogStatus->setText(tr("The local Bluetooth adapter cannot be accessed.")); + + // Disable the buttons + ui->save->setEnabled(false); + ui->scan->setEnabled(false); + ui->clear->setEnabled(false); + ui->changeDeviceState->setEnabled(false); + + return; + } + + // Set UI information about the local device + ui->deviceAddress->setText(localDevice->address().toString()); + ui->deviceName->setText(localDevice->name()); + + connect(localDevice, SIGNAL(hostModeStateChanged(QBluetoothLocalDevice::HostMode)), + this, SLOT(hostModeStateChanged(QBluetoothLocalDevice::HostMode))); + + // Initialize the state of the local device and activate/deactive the scan button + hostModeStateChanged(localDevice->hostMode()); + + // Add context menu for devices to be able to pair them + ui->discoveredDevicesList->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->discoveredDevicesList, SIGNAL(customContextMenuRequested(QPoint)), + this, SLOT(displayPairingMenu(QPoint))); + connect(localDevice, SIGNAL(pairingFinished(QBluetoothAddress, QBluetoothLocalDevice::Pairing)), + this, SLOT(pairingFinished(QBluetoothAddress, QBluetoothLocalDevice::Pairing))); + + connect(localDevice, SIGNAL(error(QBluetoothLocalDevice::Error)), + this, SLOT(error(QBluetoothLocalDevice::Error))); +#endif +} + +void BtDeviceSelectionDialog::initializeDeviceDiscoveryAgent() +{ +#if defined(Q_OS_WIN) + // Register QBluetoothDeviceInfo metatype + qRegisterMetaType<QBluetoothDeviceInfo>(); + + // Register QBluetoothDeviceDiscoveryAgent metatype (Needed for QBluetoothDeviceDiscoveryAgent::Error) + qRegisterMetaType<QBluetoothDeviceDiscoveryAgent::Error>(); + + // Intialize the discovery agent + remoteDeviceDiscoveryAgent = new WinBluetoothDeviceDiscoveryAgent(this); +#else + // Intialize the discovery agent + remoteDeviceDiscoveryAgent = new QBluetoothDeviceDiscoveryAgent(localDevice->address()); + + // Test if the discovery agent was successfully created + if (remoteDeviceDiscoveryAgent->error() == QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError) { + ui->dialogStatus->setText(tr("The device discovery agent was not created because the %1 address does not " + "match the physical adapter address of any local Bluetooth device.") + .arg(localDevice->address().toString())); + ui->scan->setEnabled(false); + ui->clear->setEnabled(false); + return; + } +#endif + connect(remoteDeviceDiscoveryAgent, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo)), + this, SLOT(addRemoteDevice(QBluetoothDeviceInfo))); + connect(remoteDeviceDiscoveryAgent, SIGNAL(finished()), + this, SLOT(remoteDeviceScanFinished())); + connect(remoteDeviceDiscoveryAgent, SIGNAL(error(QBluetoothDeviceDiscoveryAgent::Error)), + this, SLOT(deviceDiscoveryError(QBluetoothDeviceDiscoveryAgent::Error))); +} + +#if defined(Q_OS_WIN) +WinBluetoothDeviceDiscoveryAgent::WinBluetoothDeviceDiscoveryAgent(QObject *parent) : QThread(parent) +{ + // Initialize the internal flags by their default values + running = false; + stopped = false; + lastError = QBluetoothDeviceDiscoveryAgent::NoError; + lastErrorToString = tr("No error"); +} + +WinBluetoothDeviceDiscoveryAgent::~WinBluetoothDeviceDiscoveryAgent() +{ +} + +bool WinBluetoothDeviceDiscoveryAgent::isActive() const +{ + return running; +} + +QString WinBluetoothDeviceDiscoveryAgent::errorToString() const +{ + return lastErrorToString; +} + +QBluetoothDeviceDiscoveryAgent::Error WinBluetoothDeviceDiscoveryAgent::error() const +{ + return lastError; +} + +void WinBluetoothDeviceDiscoveryAgent::run() +{ + // Initialize query for device and start the lookup service + WSAQUERYSET queryset; + HANDLE hLookup; + int result = SUCCESS; + + running = true; + lastError = QBluetoothDeviceDiscoveryAgent::NoError; + lastErrorToString = tr("No error"); + + memset(&queryset, 0, sizeof(WSAQUERYSET)); + queryset.dwSize = sizeof(WSAQUERYSET); + queryset.dwNameSpace = NS_BTH; + + // The LUP_CONTAINERS flag is used to signal that we are doing a device inquiry + // while LUP_FLUSHCACHE flag is used to flush the device cache for all inquiries + // and to do a fresh lookup instead. + result = WSALookupServiceBegin(&queryset, LUP_CONTAINERS | LUP_FLUSHCACHE, &hLookup); + + if (result != SUCCESS) { + // Get the last error and emit a signal + lastErrorToString = qt_error_string(); + lastError = QBluetoothDeviceDiscoveryAgent::PoweredOffError; + emit error(lastError); + + // Announce that the inquiry finished and restore the stopped flag + running = false; + stopped = false; + + return; + } + + // Declare the necessary variables to collect the information + BYTE buffer[4096]; + DWORD bufferLength = sizeof(buffer); + WSAQUERYSET *pResults = (WSAQUERYSET*)&buffer; + + memset(buffer, 0, sizeof(buffer)); + + pResults->dwSize = sizeof(WSAQUERYSET); + pResults->dwNameSpace = NS_BTH; + pResults->lpBlob = NULL; + + //Start looking for devices + while (result == SUCCESS && !stopped){ + // LUP_RETURN_NAME and LUP_RETURN_ADDR flags are used to return the name and the address of the discovered device + result = WSALookupServiceNext(hLookup, LUP_RETURN_NAME | LUP_RETURN_ADDR, &bufferLength, pResults); + + if (result == SUCCESS) { + // Found a device + QString deviceAddress(BTH_ADDR_BUF_LEN, Qt::Uninitialized); + DWORD addressSize = BTH_ADDR_BUF_LEN; + + // Collect the address of the device from the WSAQUERYSET + SOCKADDR_BTH *socketBthAddress = (SOCKADDR_BTH *) pResults->lpcsaBuffer->RemoteAddr.lpSockaddr; + + // Convert the BTH_ADDR to string + if (WSAAddressToStringW((LPSOCKADDR) socketBthAddress, + sizeof (*socketBthAddress), + NULL, + reinterpret_cast<wchar_t*>(deviceAddress.data()), + &addressSize + ) != 0) { + // Get the last error and emit a signal + lastErrorToString = qt_error_string(); + lastError = QBluetoothDeviceDiscoveryAgent::UnknownError; + emit(lastError); + + break; + } + + // Remove the round parentheses + deviceAddress.remove(')'); + deviceAddress.remove('('); + + // Save the name of the discovered device and truncate the address + QString deviceName = QString(pResults->lpszServiceInstanceName); + deviceAddress.truncate(BTH_ADDR_PRETTY_STRING_LEN); + + // Create an object with information about the discovered device + QBluetoothDeviceInfo deviceInfo = QBluetoothDeviceInfo(QBluetoothAddress(deviceAddress), deviceName, 0); + + // Raise a signal with information about the found remote device + emit deviceDiscovered(deviceInfo); + } else { + // Get the last error and emit a signal + lastErrorToString = qt_error_string(); + lastError = QBluetoothDeviceDiscoveryAgent::UnknownError; + emit(lastError); + } + } + + // Announce that the inquiry finished and restore the stopped flag + running = false; + stopped = false; + + // Restore the error status + lastError = QBluetoothDeviceDiscoveryAgent::NoError; + + // End the lookup service + WSALookupServiceEnd(hLookup); +} + +void WinBluetoothDeviceDiscoveryAgent::stop() +{ + // Stop the inqury + stopped = true; +} +#endif diff --git a/desktop-widgets/btdeviceselectiondialog.h b/desktop-widgets/btdeviceselectiondialog.h new file mode 100644 index 000000000..7651f164b --- /dev/null +++ b/desktop-widgets/btdeviceselectiondialog.h @@ -0,0 +1,91 @@ +#ifndef BTDEVICESELECTIONDIALOG_H +#define BTDEVICESELECTIONDIALOG_H + +#include <QDialog> +#include <QListWidgetItem> +#include <QPointer> +#include <QtBluetooth/QBluetoothLocalDevice> +#include <QtBluetooth/qbluetoothglobal.h> +#include <QtBluetooth/QBluetoothDeviceDiscoveryAgent> + +#if defined(Q_OS_WIN) + #include <QThread> + #include <winsock2.h> + #include <ws2bth.h> + + #define SUCCESS 0 + #define BTH_ADDR_BUF_LEN 40 + #define BTH_ADDR_PRETTY_STRING_LEN 17 // there are 6 two-digit hex values and 5 colons + + #undef ERROR // this is already declared in our headers + #undef IGNORE // this is already declared in our headers + #undef DC_VERSION // this is already declared in libdivecomputer header +#endif + +namespace Ui { + class BtDeviceSelectionDialog; +} + +#if defined(Q_OS_WIN) +class WinBluetoothDeviceDiscoveryAgent : public QThread { + Q_OBJECT +signals: + void deviceDiscovered(const QBluetoothDeviceInfo &info); + void error(QBluetoothDeviceDiscoveryAgent::Error error); + +public: + WinBluetoothDeviceDiscoveryAgent(QObject *parent); + ~WinBluetoothDeviceDiscoveryAgent(); + bool isActive() const; + QString errorToString() const; + QBluetoothDeviceDiscoveryAgent::Error error() const; + virtual void run(); + virtual void stop(); + +private: + bool running; + bool stopped; + QString lastErrorToString; + QBluetoothDeviceDiscoveryAgent::Error lastError; +}; +#endif + +class BtDeviceSelectionDialog : public QDialog { + Q_OBJECT + +public: + explicit BtDeviceSelectionDialog(QWidget *parent = 0); + ~BtDeviceSelectionDialog(); + QString getSelectedDeviceAddress(); + QString getSelectedDeviceName(); + +private slots: + void on_changeDeviceState_clicked(); + void on_save_clicked(); + void on_clear_clicked(); + void on_scan_clicked(); + void remoteDeviceScanFinished(); + void hostModeStateChanged(QBluetoothLocalDevice::HostMode mode); + void addRemoteDevice(const QBluetoothDeviceInfo &remoteDeviceInfo); + void itemClicked(QListWidgetItem *item); + void displayPairingMenu(const QPoint &pos); + void pairingFinished(const QBluetoothAddress &address,QBluetoothLocalDevice::Pairing pairing); + void error(QBluetoothLocalDevice::Error error); + void deviceDiscoveryError(QBluetoothDeviceDiscoveryAgent::Error error); + void localDeviceChanged(int); + +private: + Ui::BtDeviceSelectionDialog *ui; +#if defined(Q_OS_WIN) + WinBluetoothDeviceDiscoveryAgent *remoteDeviceDiscoveryAgent; +#else + QBluetoothLocalDevice *localDevice; + QBluetoothDeviceDiscoveryAgent *remoteDeviceDiscoveryAgent; +#endif + QSharedPointer<QBluetoothDeviceInfo> selectedRemoteDeviceInfo; + + void updateLocalDeviceInformation(); + void initializeDeviceDiscoveryAgent(); +}; + +#endif // BTDEVICESELECTIONDIALOG_H diff --git a/desktop-widgets/btdeviceselectiondialog.ui b/desktop-widgets/btdeviceselectiondialog.ui new file mode 100644 index 000000000..4aa83cf1c --- /dev/null +++ b/desktop-widgets/btdeviceselectiondialog.ui @@ -0,0 +1,224 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>BtDeviceSelectionDialog</class> + <widget class="QDialog" name="BtDeviceSelectionDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>735</width> + <height>460</height> + </rect> + </property> + <property name="windowTitle"> + <string>Remote Bluetooth device selection</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="discoveredDevicesLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>Discovered devices</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <layout class="QHBoxLayout" name="dialogControls"> + <item> + <widget class="QPushButton" name="save"> + <property name="text"> + <string>Save</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="quit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Quit</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="1" column="0" rowspan="2"> + <layout class="QVBoxLayout" name="remoteDevicesSection"> + <item> + <widget class="QListWidget" name="discoveredDevicesList"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <layout class="QVBoxLayout" name="scanningControls"> + <item> + <widget class="QPushButton" name="scan"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Scan</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="clear"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Clear</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item row="1" column="1"> + <widget class="QGroupBox" name="localDeviceDetails"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="title"> + <string>Local Bluetooth device details</string> + </property> + <property name="flat"> + <bool>false</bool> + </property> + <layout class="QFormLayout" name="formLayout_2"> + <item row="2" column="0"> + <widget class="QLabel" name="deviceNameLabel"> + <property name="text"> + <string>Name: </string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="deviceName"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="deviceAddressLabel"> + <property name="text"> + <string>Address:</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QLineEdit" name="deviceAddress"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QCheckBox" name="deviceState"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>Bluetooth powered on</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QPushButton" name="changeDeviceState"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <weight>50</weight> + <bold>false</bold> + </font> + </property> + <property name="text"> + <string>Turn on/off</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QComboBox" name="localSelectedDevice"/> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="selectDeviceLabel"> + <property name="text"> + <string>Select device:</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="dialogStatus"> + <property name="text"> + <string/> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/desktop-widgets/configuredivecomputerdialog.cpp b/desktop-widgets/configuredivecomputerdialog.cpp new file mode 100644 index 000000000..ddb9450de --- /dev/null +++ b/desktop-widgets/configuredivecomputerdialog.cpp @@ -0,0 +1,1257 @@ +#include "configuredivecomputerdialog.h" + +#include "helpers.h" +#include "mainwindow.h" +#include "display.h" + +#include <QFileDialog> +#include <QMessageBox> +#include <QSettings> +#include <QNetworkReply> +#include <QProgressDialog> + +struct product { + const char *product; + dc_descriptor_t *descriptor; + struct product *next; +}; + +struct vendor { + const char *vendor; + struct product *productlist; + struct vendor *next; +}; + +struct mydescriptor { + const char *vendor; + const char *product; + dc_family_t type; + unsigned int model; +}; + +GasSpinBoxItemDelegate::GasSpinBoxItemDelegate(QObject *parent, column_type type) : QStyledItemDelegate(parent), type(type) +{ +} +GasSpinBoxItemDelegate::~GasSpinBoxItemDelegate() +{ +} + +QWidget *GasSpinBoxItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + // Create the spinbox and give it it's settings + QSpinBox *sb = new QSpinBox(parent); + if (type == PERCENT) { + sb->setMinimum(0); + sb->setMaximum(100); + sb->setSuffix("%"); + } else if (type == DEPTH) { + sb->setMinimum(0); + sb->setMaximum(255); + sb->setSuffix(" m"); + } else if (type == SETPOINT) { + sb->setMinimum(0); + sb->setMaximum(255); + sb->setSuffix(" cbar"); + } + return sb; +} + +void GasSpinBoxItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + if (QSpinBox *sb = qobject_cast<QSpinBox *>(editor)) + sb->setValue(index.data(Qt::EditRole).toInt()); + else + QStyledItemDelegate::setEditorData(editor, index); +} + + +void GasSpinBoxItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + if (QSpinBox *sb = qobject_cast<QSpinBox *>(editor)) + model->setData(index, sb->value(), Qt::EditRole); + else + QStyledItemDelegate::setModelData(editor, model, index); +} + +GasTypeComboBoxItemDelegate::GasTypeComboBoxItemDelegate(QObject *parent, computer_type type) : QStyledItemDelegate(parent), type(type) +{ +} +GasTypeComboBoxItemDelegate::~GasTypeComboBoxItemDelegate() +{ +} + +QWidget *GasTypeComboBoxItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + // Create the combobox and populate it + QComboBox *cb = new QComboBox(parent); + cb->addItem(QString("Disabled")); + if (type == OSTC3) { + cb->addItem(QString("First")); + cb->addItem(QString("Travel")); + cb->addItem(QString("Deco")); + } else if (type == OSTC) { + cb->addItem(QString("Active")); + cb->addItem(QString("First")); + } + return cb; +} + +void GasTypeComboBoxItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + if (QComboBox *cb = qobject_cast<QComboBox *>(editor)) + cb->setCurrentIndex(index.data(Qt::EditRole).toInt()); + else + QStyledItemDelegate::setEditorData(editor, index); +} + + +void GasTypeComboBoxItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + if (QComboBox *cb = qobject_cast<QComboBox *>(editor)) + model->setData(index, cb->currentIndex(), Qt::EditRole); + else + QStyledItemDelegate::setModelData(editor, model, index); +} + +ConfigureDiveComputerDialog::ConfigureDiveComputerDialog(QWidget *parent) : QDialog(parent), + config(0), +#ifdef BT_SUPPORT + deviceDetails(0), + btDeviceSelectionDialog(0) +#else + deviceDetails(0) +#endif +{ + ui.setupUi(this); + + deviceDetails = new DeviceDetails(this); + config = new ConfigureDiveComputer(); + connect(config, SIGNAL(progress(int)), ui.progressBar, SLOT(setValue(int))); + connect(config, SIGNAL(error(QString)), this, SLOT(configError(QString))); + connect(config, SIGNAL(message(QString)), this, SLOT(configMessage(QString))); + connect(config, SIGNAL(deviceDetailsChanged(DeviceDetails *)), + this, SLOT(deviceDetailsReceived(DeviceDetails *))); + connect(ui.retrieveDetails, SIGNAL(clicked()), this, SLOT(readSettings())); + connect(ui.resetButton, SIGNAL(clicked()), this, SLOT(resetSettings())); + ui.chooseLogFile->setEnabled(ui.logToFile->isChecked()); + connect(ui.chooseLogFile, SIGNAL(clicked()), this, SLOT(pickLogFile())); + connect(ui.logToFile, SIGNAL(stateChanged(int)), this, SLOT(checkLogFile(int))); + connect(ui.connectButton, SIGNAL(clicked()), this, SLOT(dc_open())); + connect(ui.disconnectButton, SIGNAL(clicked()), this, SLOT(dc_close())); +#ifdef BT_SUPPORT + connect(ui.bluetoothMode, SIGNAL(clicked(bool)), this, SLOT(selectRemoteBluetoothDevice())); +#else + ui.bluetoothMode->setVisible(false); +#endif + + memset(&device_data, 0, sizeof(device_data)); + fill_computer_list(); + if (default_dive_computer_device) + ui.device->setEditText(default_dive_computer_device); + + ui.DiveComputerList->setCurrentRow(0); + on_DiveComputerList_currentRowChanged(0); + + ui.ostc3GasTable->setItemDelegateForColumn(1, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::PERCENT)); + ui.ostc3GasTable->setItemDelegateForColumn(2, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::PERCENT)); + ui.ostc3GasTable->setItemDelegateForColumn(3, new GasTypeComboBoxItemDelegate(this, GasTypeComboBoxItemDelegate::OSTC3)); + ui.ostc3GasTable->setItemDelegateForColumn(4, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::DEPTH)); + ui.ostc3DilTable->setItemDelegateForColumn(3, new GasTypeComboBoxItemDelegate(this, GasTypeComboBoxItemDelegate::OSTC3)); + ui.ostc3DilTable->setItemDelegateForColumn(4, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::DEPTH)); + ui.ostc3SetPointTable->setItemDelegateForColumn(1, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::SETPOINT)); + ui.ostc3SetPointTable->setItemDelegateForColumn(2, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::DEPTH)); + ui.ostcGasTable->setItemDelegateForColumn(1, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::PERCENT)); + ui.ostcGasTable->setItemDelegateForColumn(2, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::PERCENT)); + ui.ostcGasTable->setItemDelegateForColumn(3, new GasTypeComboBoxItemDelegate(this, GasTypeComboBoxItemDelegate::OSTC)); + ui.ostcGasTable->setItemDelegateForColumn(4, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::DEPTH)); + ui.ostcDilTable->setItemDelegateForColumn(3, new GasTypeComboBoxItemDelegate(this, GasTypeComboBoxItemDelegate::OSTC)); + ui.ostcDilTable->setItemDelegateForColumn(4, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::DEPTH)); + ui.ostcSetPointTable->setItemDelegateForColumn(1, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::SETPOINT)); + ui.ostcSetPointTable->setItemDelegateForColumn(2, new GasSpinBoxItemDelegate(this, GasSpinBoxItemDelegate::DEPTH)); + + QSettings settings; + settings.beginGroup("ConfigureDiveComputerDialog"); + settings.beginGroup("ostc3GasTable"); + for (int i = 0; i < ui.ostc3GasTable->columnCount(); i++) { + QVariant width = settings.value(QString("colwidth%1").arg(i)); + if (width.isValid()) + ui.ostc3GasTable->setColumnWidth(i, width.toInt()); + } + settings.endGroup(); + settings.beginGroup("ostc3DilTable"); + for (int i = 0; i < ui.ostc3DilTable->columnCount(); i++) { + QVariant width = settings.value(QString("colwidth%1").arg(i)); + if (width.isValid()) + ui.ostc3DilTable->setColumnWidth(i, width.toInt()); + } + settings.endGroup(); + settings.beginGroup("ostc3SetPointTable"); + for (int i = 0; i < ui.ostc3SetPointTable->columnCount(); i++) { + QVariant width = settings.value(QString("colwidth%1").arg(i)); + if (width.isValid()) + ui.ostc3SetPointTable->setColumnWidth(i, width.toInt()); + } + settings.endGroup(); + + settings.beginGroup("ostcGasTable"); + for (int i = 0; i < ui.ostcGasTable->columnCount(); i++) { + QVariant width = settings.value(QString("colwidth%1").arg(i)); + if (width.isValid()) + ui.ostcGasTable->setColumnWidth(i, width.toInt()); + } + settings.endGroup(); + settings.beginGroup("ostcDilTable"); + for (int i = 0; i < ui.ostcDilTable->columnCount(); i++) { + QVariant width = settings.value(QString("colwidth%1").arg(i)); + if (width.isValid()) + ui.ostcDilTable->setColumnWidth(i, width.toInt()); + } + settings.endGroup(); + settings.beginGroup("ostcSetPointTable"); + for (int i = 0; i < ui.ostcSetPointTable->columnCount(); i++) { + QVariant width = settings.value(QString("colwidth%1").arg(i)); + if (width.isValid()) + ui.ostcSetPointTable->setColumnWidth(i, width.toInt()); + } + settings.endGroup(); + settings.endGroup(); +} + +OstcFirmwareCheck::OstcFirmwareCheck(QString product) : parent(0) +{ + QUrl url; + memset(&devData, 1, sizeof(devData)); + if (product == "OSTC 3") { + url = QUrl("http://www.heinrichsweikamp.net/autofirmware/ostc3_changelog.txt"); + latestFirmwareHexFile = QString("http://www.heinrichsweikamp.net/autofirmware/ostc3_firmware.hex"); + } else if (product == "OSTC Sport") { + url = QUrl("http://www.heinrichsweikamp.net/autofirmware/ostc_sport_changelog.txt"); + latestFirmwareHexFile = QString("http://www.heinrichsweikamp.net/autofirmware/ostc_sport_firmware.hex"); + } else { // not one of the known dive computers + return; + } + connect(&manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(parseOstcFwVersion(QNetworkReply *))); + QNetworkRequest download(url); + manager.get(download); +} + +void OstcFirmwareCheck::parseOstcFwVersion(QNetworkReply *reply) +{ + QString parse = reply->readAll(); + int firstOpenBracket = parse.indexOf('['); + int firstCloseBracket = parse.indexOf(']'); + latestFirmwareAvailable = parse.mid(firstOpenBracket + 1, firstCloseBracket - firstOpenBracket - 1); + disconnect(&manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(parseOstcFwVersion(QNetworkReply *))); +} + +void OstcFirmwareCheck::checkLatest(QWidget *_parent, device_data_t *data) +{ + devData = *data; + parent = _parent; + // If we didn't find a current firmware version stop this hole thing here. + if (latestFirmwareAvailable.isEmpty()) + return; + + // for now libdivecomputer gives us the firmware on device undecoded as integer + // for the OSTC that means highbyte.lowbyte is the version number + int firmwareOnDevice = devData.libdc_firmware; + QString firmwareOnDeviceString = QString("%1.%2").arg(firmwareOnDevice / 256).arg(firmwareOnDevice % 256); + + // Convert the latestFirmwareAvailable to a integear we can compare with + QStringList fwParts = latestFirmwareAvailable.split("."); + int latestFirmwareAvailableNumber = fwParts[0].toInt() * 256 + fwParts[1].toInt(); + if (latestFirmwareAvailableNumber > firmwareOnDevice) { + QMessageBox response(parent); + QString message = tr("You should update the firmware on your dive computer: you have version %1 but the latest stable version is %2") + .arg(firmwareOnDeviceString) + .arg(latestFirmwareAvailable); + if (strcmp(data->product, "OSTC Sport") == 0) + message += tr("\n\nPlease start Bluetooth on your OSTC Sport and do the same preparations as for a logbook download before continuing with the update"); + response.addButton(tr("Not now"), QMessageBox::RejectRole); + response.addButton(tr("Update firmware"), QMessageBox::AcceptRole); + response.setText(message); + response.setWindowTitle(tr("Firmware upgrade notice")); + response.setIcon(QMessageBox::Question); + response.setWindowModality(Qt::WindowModal); + int ret = response.exec(); + if (ret == QMessageBox::Accepted) + upgradeFirmware(); + } +} + +void OstcFirmwareCheck::upgradeFirmware() +{ + // start download of latestFirmwareHexFile + QString saveFileName = latestFirmwareHexFile; + saveFileName.replace("http://www.heinrichsweikamp.net/autofirmware/", ""); + saveFileName.replace("firmware", latestFirmwareAvailable); + QString filename = existing_filename ?: prefs.default_filename; + QFileInfo fi(filename); + filename = fi.absolutePath().append(QDir::separator()).append(saveFileName); + storeFirmware = QFileDialog::getSaveFileName(parent, tr("Save the downloaded firmware as"), + filename, tr("HEX files (*.hex)")); + if (storeFirmware.isEmpty()) + return; + + connect(&manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(saveOstcFirmware(QNetworkReply *))); + QNetworkRequest download(latestFirmwareHexFile); + manager.get(download); +} + +void OstcFirmwareCheck::saveOstcFirmware(QNetworkReply *reply) +{ + // firmware is downloaded + // call config->startFirmwareUpdate() with that file and the device data + + QByteArray firmwareData = reply->readAll(); + QFile file(storeFirmware); + file.open(QIODevice::WriteOnly); + file.write(firmwareData); + file.close(); + QProgressDialog *dialog = new QProgressDialog("Updating firmware", "", 0, 100); + dialog->setCancelButton(0); + dialog->setAutoClose(true); + ConfigureDiveComputer *config = new ConfigureDiveComputer(); + connect(config, SIGNAL(message(QString)), dialog, SLOT(setLabelText(QString))); + connect(config, SIGNAL(error(QString)), dialog, SLOT(setLabelText(QString))); + connect(config, SIGNAL(progress(int)), dialog, SLOT(setValue(int))); + connect(dialog, SIGNAL(finished(int)), config, SLOT(dc_close())); + config->dc_open(&devData); + config->startFirmwareUpdate(storeFirmware, &devData); +} + +ConfigureDiveComputerDialog::~ConfigureDiveComputerDialog() +{ + delete config; +} + +void ConfigureDiveComputerDialog::closeEvent(QCloseEvent *event) +{ + dc_close(); + + QSettings settings; + settings.beginGroup("ConfigureDiveComputerDialog"); + settings.beginGroup("ostc3GasTable"); + for (int i = 0; i < ui.ostc3GasTable->columnCount(); i++) + settings.setValue(QString("colwidth%1").arg(i), ui.ostc3GasTable->columnWidth(i)); + settings.endGroup(); + settings.beginGroup("ostc3DilTable"); + for (int i = 0; i < ui.ostc3DilTable->columnCount(); i++) + settings.setValue(QString("colwidth%1").arg(i), ui.ostc3DilTable->columnWidth(i)); + settings.endGroup(); + settings.beginGroup("ostc3SetPointTable"); + for (int i = 0; i < ui.ostc3SetPointTable->columnCount(); i++) + settings.setValue(QString("colwidth%1").arg(i), ui.ostc3SetPointTable->columnWidth(i)); + settings.endGroup(); + + settings.beginGroup("ostcGasTable"); + for (int i = 0; i < ui.ostcGasTable->columnCount(); i++) + settings.setValue(QString("colwidth%1").arg(i), ui.ostcGasTable->columnWidth(i)); + settings.endGroup(); + settings.beginGroup("ostcDilTable"); + for (int i = 0; i < ui.ostcDilTable->columnCount(); i++) + settings.setValue(QString("colwidth%1").arg(i), ui.ostcDilTable->columnWidth(i)); + settings.endGroup(); + settings.beginGroup("ostcSetPointTable"); + for (int i = 0; i < ui.ostcSetPointTable->columnCount(); i++) + settings.setValue(QString("colwidth%1").arg(i), ui.ostcSetPointTable->columnWidth(i)); + settings.endGroup(); + settings.endGroup(); +} + + +static void fillDeviceList(const char *name, void *data) +{ + QComboBox *comboBox = (QComboBox *)data; + comboBox->addItem(name); +} + +void ConfigureDiveComputerDialog::fill_device_list(int dc_type) +{ + int deviceIndex; + ui.device->clear(); + deviceIndex = enumerate_devices(fillDeviceList, ui.device, dc_type); + if (deviceIndex >= 0) + ui.device->setCurrentIndex(deviceIndex); +} + +void ConfigureDiveComputerDialog::fill_computer_list() +{ + dc_iterator_t *iterator = NULL; + dc_descriptor_t *descriptor = NULL; + + struct mydescriptor *mydescriptor; + + dc_descriptor_iterator(&iterator); + while (dc_iterator_next(iterator, &descriptor) == DC_STATUS_SUCCESS) { + const char *vendor = dc_descriptor_get_vendor(descriptor); + const char *product = dc_descriptor_get_product(descriptor); + + if (!vendorList.contains(vendor)) + vendorList.append(vendor); + + if (!productList[vendor].contains(product)) + productList[vendor].push_back(product); + + descriptorLookup[QString(vendor) + QString(product)] = descriptor; + } + dc_iterator_free(iterator); + + mydescriptor = (struct mydescriptor *)malloc(sizeof(struct mydescriptor)); + mydescriptor->vendor = "Uemis"; + mydescriptor->product = "Zurich"; + mydescriptor->type = DC_FAMILY_NULL; + mydescriptor->model = 0; + + if (!vendorList.contains("Uemis")) + vendorList.append("Uemis"); + + if (!productList["Uemis"].contains("Zurich")) + productList["Uemis"].push_back("Zurich"); + + descriptorLookup["UemisZurich"] = (dc_descriptor_t *)mydescriptor; + + qSort(vendorList); +} + +void ConfigureDiveComputerDialog::populateDeviceDetails() +{ + switch (ui.dcStackedWidget->currentIndex()) { + case 0: + populateDeviceDetailsOSTC3(); + break; + case 1: + populateDeviceDetailsSuuntoVyper(); + break; + case 2: + populateDeviceDetailsOSTC(); + break; + } +} + +#define GET_INT_FROM(_field, _default) ((_field) != NULL) ? (_field)->data(Qt::EditRole).toInt() : (_default) + +void ConfigureDiveComputerDialog::populateDeviceDetailsOSTC3() +{ + deviceDetails->customText = ui.customTextLlineEdit->text(); + deviceDetails->diveMode = ui.diveModeComboBox->currentIndex(); + deviceDetails->saturation = ui.saturationSpinBox->value(); + deviceDetails->desaturation = ui.desaturationSpinBox->value(); + deviceDetails->lastDeco = ui.lastDecoSpinBox->value(); + deviceDetails->brightness = ui.brightnessComboBox->currentIndex(); + deviceDetails->units = ui.unitsComboBox->currentIndex(); + deviceDetails->samplingRate = ui.samplingRateComboBox->currentIndex(); + deviceDetails->salinity = ui.salinitySpinBox->value(); + deviceDetails->diveModeColor = ui.diveModeColour->currentIndex(); + deviceDetails->language = ui.languageComboBox->currentIndex(); + deviceDetails->dateFormat = ui.dateFormatComboBox->currentIndex(); + deviceDetails->compassGain = ui.compassGainComboBox->currentIndex(); + deviceDetails->syncTime = ui.dateTimeSyncCheckBox->isChecked(); + deviceDetails->safetyStop = ui.safetyStopCheckBox->isChecked(); + deviceDetails->gfHigh = ui.gfHighSpinBox->value(); + deviceDetails->gfLow = ui.gfLowSpinBox->value(); + deviceDetails->pressureSensorOffset = ui.pressureSensorOffsetSpinBox->value(); + deviceDetails->ppO2Min = ui.ppO2MinSpinBox->value(); + deviceDetails->ppO2Max = ui.ppO2MaxSpinBox->value(); + deviceDetails->futureTTS = ui.futureTTSSpinBox->value(); + deviceDetails->ccrMode = ui.ccrModeComboBox->currentIndex(); + deviceDetails->decoType = ui.decoTypeComboBox->currentIndex(); + deviceDetails->aGFSelectable = ui.aGFSelectableCheckBox->isChecked(); + deviceDetails->aGFHigh = ui.aGFHighSpinBox->value(); + deviceDetails->aGFLow = ui.aGFLowSpinBox->value(); + deviceDetails->calibrationGas = ui.calibrationGasSpinBox->value(); + deviceDetails->flipScreen = ui.flipScreenCheckBox->isChecked(); + deviceDetails->setPointFallback = ui.setPointFallbackCheckBox->isChecked(); + deviceDetails->leftButtonSensitivity = ui.leftButtonSensitivity->value(); + deviceDetails->rightButtonSensitivity = ui.rightButtonSensitivity->value(); + deviceDetails->bottomGasConsumption = ui.bottomGasConsumption->value(); + deviceDetails->decoGasConsumption = ui.decoGasConsumption->value(); + deviceDetails->modWarning = ui.modWarning->isChecked(); + deviceDetails->dynamicAscendRate = ui.dynamicAscendRate->isChecked(); + deviceDetails->graphicalSpeedIndicator = ui.graphicalSpeedIndicator->isChecked(); + deviceDetails->alwaysShowppO2 = ui.alwaysShowppO2->isChecked(); + + //set gas values + gas gas1; + gas gas2; + gas gas3; + gas gas4; + gas gas5; + + gas1.oxygen = GET_INT_FROM(ui.ostc3GasTable->item(0, 1), 21); + gas1.helium = GET_INT_FROM(ui.ostc3GasTable->item(0, 2), 0); + gas1.type = GET_INT_FROM(ui.ostc3GasTable->item(0, 3), 0); + gas1.depth = GET_INT_FROM(ui.ostc3GasTable->item(0, 4), 0); + + gas2.oxygen = GET_INT_FROM(ui.ostc3GasTable->item(1, 1), 21); + gas2.helium = GET_INT_FROM(ui.ostc3GasTable->item(1, 2), 0); + gas2.type = GET_INT_FROM(ui.ostc3GasTable->item(1, 3), 0); + gas2.depth = GET_INT_FROM(ui.ostc3GasTable->item(1, 4), 0); + + gas3.oxygen = GET_INT_FROM(ui.ostc3GasTable->item(2, 1), 21); + gas3.helium = GET_INT_FROM(ui.ostc3GasTable->item(2, 2), 0); + gas3.type = GET_INT_FROM(ui.ostc3GasTable->item(2, 3), 0); + gas3.depth = GET_INT_FROM(ui.ostc3GasTable->item(2, 4), 0); + + gas4.oxygen = GET_INT_FROM(ui.ostc3GasTable->item(3, 1), 21); + gas4.helium = GET_INT_FROM(ui.ostc3GasTable->item(3, 2), 0); + gas4.type = GET_INT_FROM(ui.ostc3GasTable->item(3, 3), 0); + gas4.depth = GET_INT_FROM(ui.ostc3GasTable->item(3, 4), 0); + + gas5.oxygen = GET_INT_FROM(ui.ostc3GasTable->item(4, 1), 21); + gas5.helium = GET_INT_FROM(ui.ostc3GasTable->item(4, 2), 0); + gas5.type = GET_INT_FROM(ui.ostc3GasTable->item(4, 3), 0); + gas5.depth = GET_INT_FROM(ui.ostc3GasTable->item(4, 4), 0); + + deviceDetails->gas1 = gas1; + deviceDetails->gas2 = gas2; + deviceDetails->gas3 = gas3; + deviceDetails->gas4 = gas4; + deviceDetails->gas5 = gas5; + + //set dil values + gas dil1; + gas dil2; + gas dil3; + gas dil4; + gas dil5; + + dil1.oxygen = GET_INT_FROM(ui.ostc3DilTable->item(0, 1), 21); + dil1.helium = GET_INT_FROM(ui.ostc3DilTable->item(0, 2), 0); + dil1.type = GET_INT_FROM(ui.ostc3DilTable->item(0, 3), 0); + dil1.depth = GET_INT_FROM(ui.ostc3DilTable->item(0, 4), 0); + + dil2.oxygen = GET_INT_FROM(ui.ostc3DilTable->item(1, 1), 21); + dil2.helium = GET_INT_FROM(ui.ostc3DilTable->item(1, 2), 0); + dil2.type = GET_INT_FROM(ui.ostc3DilTable->item(1, 3), 0); + dil2.depth = GET_INT_FROM(ui.ostc3DilTable->item(1, 4), 0); + + dil3.oxygen = GET_INT_FROM(ui.ostc3DilTable->item(2, 1), 21); + dil3.helium = GET_INT_FROM(ui.ostc3DilTable->item(2, 2), 0); + dil3.type = GET_INT_FROM(ui.ostc3DilTable->item(2, 3), 0); + dil3.depth = GET_INT_FROM(ui.ostc3DilTable->item(2, 4), 0); + + dil4.oxygen = GET_INT_FROM(ui.ostc3DilTable->item(3, 1), 21); + dil4.helium = GET_INT_FROM(ui.ostc3DilTable->item(3, 2), 0); + dil4.type = GET_INT_FROM(ui.ostc3DilTable->item(3, 3), 0); + dil4.depth = GET_INT_FROM(ui.ostc3DilTable->item(3, 4), 0); + + dil5.oxygen = GET_INT_FROM(ui.ostc3DilTable->item(4, 1), 21); + dil5.helium = GET_INT_FROM(ui.ostc3DilTable->item(4, 2), 0); + dil5.type = GET_INT_FROM(ui.ostc3DilTable->item(4, 3), 0); + dil5.depth = GET_INT_FROM(ui.ostc3DilTable->item(4, 4), 0); + + deviceDetails->dil1 = dil1; + deviceDetails->dil2 = dil2; + deviceDetails->dil3 = dil3; + deviceDetails->dil4 = dil4; + deviceDetails->dil5 = dil5; + + //set set point details + setpoint sp1; + setpoint sp2; + setpoint sp3; + setpoint sp4; + setpoint sp5; + + sp1.sp = GET_INT_FROM(ui.ostc3SetPointTable->item(0, 1), 70); + sp1.depth = GET_INT_FROM(ui.ostc3SetPointTable->item(0, 2), 0); + + sp2.sp = GET_INT_FROM(ui.ostc3SetPointTable->item(1, 1), 90); + sp2.depth = GET_INT_FROM(ui.ostc3SetPointTable->item(1, 2), 20); + + sp3.sp = GET_INT_FROM(ui.ostc3SetPointTable->item(2, 1), 100); + sp3.depth = GET_INT_FROM(ui.ostc3SetPointTable->item(2, 2), 33); + + sp4.sp = GET_INT_FROM(ui.ostc3SetPointTable->item(3, 1), 120); + sp4.depth = GET_INT_FROM(ui.ostc3SetPointTable->item(3, 2), 50); + + sp5.sp = GET_INT_FROM(ui.ostc3SetPointTable->item(4, 1), 140); + sp5.depth = GET_INT_FROM(ui.ostc3SetPointTable->item(4, 2), 70); + + deviceDetails->sp1 = sp1; + deviceDetails->sp2 = sp2; + deviceDetails->sp3 = sp3; + deviceDetails->sp4 = sp4; + deviceDetails->sp5 = sp5; +} + +void ConfigureDiveComputerDialog::populateDeviceDetailsOSTC() +{ + deviceDetails->customText = ui.customTextLlineEdit_3->text(); + deviceDetails->saturation = ui.saturationSpinBox_3->value(); + deviceDetails->desaturation = ui.desaturationSpinBox_3->value(); + deviceDetails->lastDeco = ui.lastDecoSpinBox_3->value(); + deviceDetails->samplingRate = ui.samplingRateSpinBox_3->value(); + deviceDetails->salinity = ui.salinityDoubleSpinBox_3->value() * 100; + deviceDetails->dateFormat = ui.dateFormatComboBox_3->currentIndex(); + deviceDetails->syncTime = ui.dateTimeSyncCheckBox_3->isChecked(); + deviceDetails->safetyStop = ui.safetyStopCheckBox_3->isChecked(); + deviceDetails->gfHigh = ui.gfHighSpinBox_3->value(); + deviceDetails->gfLow = ui.gfLowSpinBox_3->value(); + deviceDetails->ppO2Min = ui.ppO2MinSpinBox_3->value(); + deviceDetails->ppO2Max = ui.ppO2MaxSpinBox_3->value(); + deviceDetails->futureTTS = ui.futureTTSSpinBox_3->value(); + deviceDetails->decoType = ui.decoTypeComboBox_3->currentIndex(); + deviceDetails->aGFSelectable = ui.aGFSelectableCheckBox_3->isChecked(); + deviceDetails->aGFHigh = ui.aGFHighSpinBox_3->value(); + deviceDetails->aGFLow = ui.aGFLowSpinBox_3->value(); + deviceDetails->bottomGasConsumption = ui.bottomGasConsumption_3->value(); + deviceDetails->decoGasConsumption = ui.decoGasConsumption_3->value(); + deviceDetails->graphicalSpeedIndicator = ui.graphicalSpeedIndicator_3->isChecked(); + + //set gas values + gas gas1; + gas gas2; + gas gas3; + gas gas4; + gas gas5; + + gas1.oxygen = GET_INT_FROM(ui.ostcGasTable->item(0, 1), 21); + gas1.helium = GET_INT_FROM(ui.ostcGasTable->item(0, 2), 0); + gas1.type = GET_INT_FROM(ui.ostcGasTable->item(0, 3), 0); + gas1.depth = GET_INT_FROM(ui.ostcGasTable->item(0, 4), 0); + + gas2.oxygen = GET_INT_FROM(ui.ostcGasTable->item(1, 1), 21); + gas2.helium = GET_INT_FROM(ui.ostcGasTable->item(1, 2), 0); + gas2.type = GET_INT_FROM(ui.ostcGasTable->item(1, 3), 0); + gas2.depth = GET_INT_FROM(ui.ostcGasTable->item(1, 4), 0); + + gas3.oxygen = GET_INT_FROM(ui.ostcGasTable->item(2, 1), 21); + gas3.helium = GET_INT_FROM(ui.ostcGasTable->item(2, 2), 0); + gas3.type = GET_INT_FROM(ui.ostcGasTable->item(2, 3), 0); + gas3.depth = GET_INT_FROM(ui.ostcGasTable->item(2, 4), 0); + + gas4.oxygen = GET_INT_FROM(ui.ostcGasTable->item(3, 1), 21); + gas4.helium = GET_INT_FROM(ui.ostcGasTable->item(3, 2), 0); + gas4.type = GET_INT_FROM(ui.ostcGasTable->item(3, 3), 0); + gas4.depth = GET_INT_FROM(ui.ostcGasTable->item(3, 4), 0); + + gas5.oxygen = GET_INT_FROM(ui.ostcGasTable->item(4, 1), 21); + gas5.helium = GET_INT_FROM(ui.ostcGasTable->item(4, 2), 0); + gas5.type = GET_INT_FROM(ui.ostcGasTable->item(4, 3), 0); + gas5.depth = GET_INT_FROM(ui.ostcGasTable->item(4, 4), 0); + + deviceDetails->gas1 = gas1; + deviceDetails->gas2 = gas2; + deviceDetails->gas3 = gas3; + deviceDetails->gas4 = gas4; + deviceDetails->gas5 = gas5; + + //set dil values + gas dil1; + gas dil2; + gas dil3; + gas dil4; + gas dil5; + + dil1.oxygen = GET_INT_FROM(ui.ostcDilTable->item(0, 1), 21); + dil1.helium = GET_INT_FROM(ui.ostcDilTable->item(0, 2), 0); + dil1.type = GET_INT_FROM(ui.ostcDilTable->item(0, 3), 0); + dil1.depth = GET_INT_FROM(ui.ostcDilTable->item(0, 4), 0); + + dil2.oxygen = GET_INT_FROM(ui.ostcDilTable->item(1, 1), 21); + dil2.helium = GET_INT_FROM(ui.ostcDilTable->item(1, 2), 0); + dil2.type = GET_INT_FROM(ui.ostcDilTable->item(1, 3), 0); + dil2.depth = GET_INT_FROM(ui.ostcDilTable->item(1, 4), 0); + + dil3.oxygen = GET_INT_FROM(ui.ostcDilTable->item(2, 1), 21); + dil3.helium = GET_INT_FROM(ui.ostcDilTable->item(2, 2), 0); + dil3.type = GET_INT_FROM(ui.ostcDilTable->item(2, 3), 0); + dil3.depth = GET_INT_FROM(ui.ostcDilTable->item(2, 4), 0); + + dil4.oxygen = GET_INT_FROM(ui.ostcDilTable->item(3, 1), 21); + dil4.helium = GET_INT_FROM(ui.ostcDilTable->item(3, 2), 0); + dil4.type = GET_INT_FROM(ui.ostcDilTable->item(3, 3), 0); + dil4.depth = GET_INT_FROM(ui.ostcDilTable->item(3, 4), 0); + + dil5.oxygen = GET_INT_FROM(ui.ostcDilTable->item(4, 1), 21); + dil5.helium = GET_INT_FROM(ui.ostcDilTable->item(4, 2), 0); + dil5.type = GET_INT_FROM(ui.ostcDilTable->item(4, 3), 0); + dil5.depth = GET_INT_FROM(ui.ostcDilTable->item(4, 4), 0); + + deviceDetails->dil1 = dil1; + deviceDetails->dil2 = dil2; + deviceDetails->dil3 = dil3; + deviceDetails->dil4 = dil4; + deviceDetails->dil5 = dil5; + + //set set point details + setpoint sp1; + setpoint sp2; + setpoint sp3; + setpoint sp4; + setpoint sp5; + + sp1.sp = GET_INT_FROM(ui.ostcSetPointTable->item(0, 1), 70); + sp1.depth = GET_INT_FROM(ui.ostcSetPointTable->item(0, 2), 0); + + sp2.sp = GET_INT_FROM(ui.ostcSetPointTable->item(1, 1), 90); + sp2.depth = GET_INT_FROM(ui.ostcSetPointTable->item(1, 2), 20); + + sp3.sp = GET_INT_FROM(ui.ostcSetPointTable->item(2, 1), 100); + sp3.depth = GET_INT_FROM(ui.ostcSetPointTable->item(2, 2), 33); + + sp4.sp = GET_INT_FROM(ui.ostcSetPointTable->item(3, 1), 120); + sp4.depth = GET_INT_FROM(ui.ostcSetPointTable->item(3, 2), 50); + + sp5.sp = GET_INT_FROM(ui.ostcSetPointTable->item(4, 1), 140); + sp5.depth = GET_INT_FROM(ui.ostcSetPointTable->item(4, 2), 70); + + deviceDetails->sp1 = sp1; + deviceDetails->sp2 = sp2; + deviceDetails->sp3 = sp3; + deviceDetails->sp4 = sp4; + deviceDetails->sp5 = sp5; +} + +void ConfigureDiveComputerDialog::populateDeviceDetailsSuuntoVyper() +{ + deviceDetails->customText = ui.customTextLlineEdit_1->text(); + deviceDetails->samplingRate = ui.samplingRateComboBox_1->currentIndex() == 3 ? 60 : (ui.samplingRateComboBox_1->currentIndex() + 1) * 10; + deviceDetails->altitude = ui.altitudeRangeComboBox->currentIndex(); + deviceDetails->personalSafety = ui.personalSafetyComboBox->currentIndex(); + deviceDetails->timeFormat = ui.timeFormatComboBox->currentIndex(); + deviceDetails->units = ui.unitsComboBox_1->currentIndex(); + deviceDetails->diveMode = ui.diveModeComboBox_1->currentIndex(); + deviceDetails->lightEnabled = ui.lightCheckBox->isChecked(); + deviceDetails->light = ui.lightSpinBox->value(); + deviceDetails->alarmDepthEnabled = ui.alarmDepthCheckBox->isChecked(); + deviceDetails->alarmDepth = units_to_depth(ui.alarmDepthDoubleSpinBox->value()); + deviceDetails->alarmTimeEnabled = ui.alarmTimeCheckBox->isChecked(); + deviceDetails->alarmTime = ui.alarmTimeSpinBox->value(); +} + +void ConfigureDiveComputerDialog::readSettings() +{ + ui.progressBar->setValue(0); + ui.progressBar->setFormat("%p%"); + ui.progressBar->setTextVisible(true); + // Fw update is no longer a option, needs to be done on a untouched device + ui.updateFirmwareButton->setEnabled(false); + + config->readSettings(&device_data); +} + +void ConfigureDiveComputerDialog::resetSettings() +{ + ui.progressBar->setValue(0); + ui.progressBar->setFormat("%p%"); + ui.progressBar->setTextVisible(true); + + config->resetSettings(&device_data); +} + +void ConfigureDiveComputerDialog::configMessage(QString msg) +{ + ui.progressBar->setFormat(msg); +} + +void ConfigureDiveComputerDialog::configError(QString err) +{ + ui.progressBar->setFormat("Error: " + err); +} + +void ConfigureDiveComputerDialog::getDeviceData() +{ + device_data.devname = strdup(ui.device->currentText().toUtf8().data()); + device_data.vendor = strdup(selected_vendor.toUtf8().data()); + device_data.product = strdup(selected_product.toUtf8().data()); + + device_data.descriptor = descriptorLookup[selected_vendor + selected_product]; + device_data.deviceid = device_data.diveid = 0; + + set_default_dive_computer_device(device_data.devname); +} + +void ConfigureDiveComputerDialog::on_cancel_clicked() +{ + this->close(); +} + +void ConfigureDiveComputerDialog::on_saveSettingsPushButton_clicked() +{ + ui.progressBar->setValue(0); + ui.progressBar->setFormat("%p%"); + ui.progressBar->setTextVisible(true); + + populateDeviceDetails(); + config->saveDeviceDetails(deviceDetails, &device_data); +} + +void ConfigureDiveComputerDialog::deviceDetailsReceived(DeviceDetails *newDeviceDetails) +{ + deviceDetails = newDeviceDetails; + reloadValues(); +} + +void ConfigureDiveComputerDialog::reloadValues() +{ + // Enable the buttons to do operations on this data + ui.saveSettingsPushButton->setEnabled(true); + ui.backupButton->setEnabled(true); + + switch (ui.dcStackedWidget->currentIndex()) { + case 0: + reloadValuesOSTC3(); + break; + case 1: + reloadValuesSuuntoVyper(); + break; + case 2: + reloadValuesOSTC(); + break; + } +} + +void ConfigureDiveComputerDialog::reloadValuesOSTC3() +{ + ui.serialNoLineEdit->setText(deviceDetails->serialNo); + ui.firmwareVersionLineEdit->setText(deviceDetails->firmwareVersion); + ui.customTextLlineEdit->setText(deviceDetails->customText); + ui.modelLineEdit->setText(deviceDetails->model); + ui.diveModeComboBox->setCurrentIndex(deviceDetails->diveMode); + ui.saturationSpinBox->setValue(deviceDetails->saturation); + ui.desaturationSpinBox->setValue(deviceDetails->desaturation); + ui.lastDecoSpinBox->setValue(deviceDetails->lastDeco); + ui.brightnessComboBox->setCurrentIndex(deviceDetails->brightness); + ui.unitsComboBox->setCurrentIndex(deviceDetails->units); + ui.samplingRateComboBox->setCurrentIndex(deviceDetails->samplingRate); + ui.salinitySpinBox->setValue(deviceDetails->salinity); + ui.diveModeColour->setCurrentIndex(deviceDetails->diveModeColor); + ui.languageComboBox->setCurrentIndex(deviceDetails->language); + ui.dateFormatComboBox->setCurrentIndex(deviceDetails->dateFormat); + ui.compassGainComboBox->setCurrentIndex(deviceDetails->compassGain); + ui.safetyStopCheckBox->setChecked(deviceDetails->safetyStop); + ui.gfHighSpinBox->setValue(deviceDetails->gfHigh); + ui.gfLowSpinBox->setValue(deviceDetails->gfLow); + ui.pressureSensorOffsetSpinBox->setValue(deviceDetails->pressureSensorOffset); + ui.ppO2MinSpinBox->setValue(deviceDetails->ppO2Min); + ui.ppO2MaxSpinBox->setValue(deviceDetails->ppO2Max); + ui.futureTTSSpinBox->setValue(deviceDetails->futureTTS); + ui.ccrModeComboBox->setCurrentIndex(deviceDetails->ccrMode); + ui.decoTypeComboBox->setCurrentIndex(deviceDetails->decoType); + ui.aGFSelectableCheckBox->setChecked(deviceDetails->aGFSelectable); + ui.aGFHighSpinBox->setValue(deviceDetails->aGFHigh); + ui.aGFLowSpinBox->setValue(deviceDetails->aGFLow); + ui.calibrationGasSpinBox->setValue(deviceDetails->calibrationGas); + ui.flipScreenCheckBox->setChecked(deviceDetails->flipScreen); + ui.setPointFallbackCheckBox->setChecked(deviceDetails->setPointFallback); + ui.leftButtonSensitivity->setValue(deviceDetails->leftButtonSensitivity); + ui.rightButtonSensitivity->setValue(deviceDetails->rightButtonSensitivity); + ui.bottomGasConsumption->setValue(deviceDetails->bottomGasConsumption); + ui.decoGasConsumption->setValue(deviceDetails->decoGasConsumption); + ui.modWarning->setChecked(deviceDetails->modWarning); + ui.dynamicAscendRate->setChecked(deviceDetails->dynamicAscendRate); + ui.graphicalSpeedIndicator->setChecked(deviceDetails->graphicalSpeedIndicator); + ui.alwaysShowppO2->setChecked(deviceDetails->alwaysShowppO2); + + //load gas 1 values + ui.ostc3GasTable->setItem(0, 1, new QTableWidgetItem(QString::number(deviceDetails->gas1.oxygen))); + ui.ostc3GasTable->setItem(0, 2, new QTableWidgetItem(QString::number(deviceDetails->gas1.helium))); + ui.ostc3GasTable->setItem(0, 3, new QTableWidgetItem(QString::number(deviceDetails->gas1.type))); + ui.ostc3GasTable->setItem(0, 4, new QTableWidgetItem(QString::number(deviceDetails->gas1.depth))); + + //load gas 2 values + ui.ostc3GasTable->setItem(1, 1, new QTableWidgetItem(QString::number(deviceDetails->gas2.oxygen))); + ui.ostc3GasTable->setItem(1, 2, new QTableWidgetItem(QString::number(deviceDetails->gas2.helium))); + ui.ostc3GasTable->setItem(1, 3, new QTableWidgetItem(QString::number(deviceDetails->gas2.type))); + ui.ostc3GasTable->setItem(1, 4, new QTableWidgetItem(QString::number(deviceDetails->gas2.depth))); + + //load gas 3 values + ui.ostc3GasTable->setItem(2, 1, new QTableWidgetItem(QString::number(deviceDetails->gas3.oxygen))); + ui.ostc3GasTable->setItem(2, 2, new QTableWidgetItem(QString::number(deviceDetails->gas3.helium))); + ui.ostc3GasTable->setItem(2, 3, new QTableWidgetItem(QString::number(deviceDetails->gas3.type))); + ui.ostc3GasTable->setItem(2, 4, new QTableWidgetItem(QString::number(deviceDetails->gas3.depth))); + + //load gas 4 values + ui.ostc3GasTable->setItem(3, 1, new QTableWidgetItem(QString::number(deviceDetails->gas4.oxygen))); + ui.ostc3GasTable->setItem(3, 2, new QTableWidgetItem(QString::number(deviceDetails->gas4.helium))); + ui.ostc3GasTable->setItem(3, 3, new QTableWidgetItem(QString::number(deviceDetails->gas4.type))); + ui.ostc3GasTable->setItem(3, 4, new QTableWidgetItem(QString::number(deviceDetails->gas4.depth))); + + //load gas 5 values + ui.ostc3GasTable->setItem(4, 1, new QTableWidgetItem(QString::number(deviceDetails->gas5.oxygen))); + ui.ostc3GasTable->setItem(4, 2, new QTableWidgetItem(QString::number(deviceDetails->gas5.helium))); + ui.ostc3GasTable->setItem(4, 3, new QTableWidgetItem(QString::number(deviceDetails->gas5.type))); + ui.ostc3GasTable->setItem(4, 4, new QTableWidgetItem(QString::number(deviceDetails->gas5.depth))); + + //load dil 1 values + ui.ostc3DilTable->setItem(0, 1, new QTableWidgetItem(QString::number(deviceDetails->dil1.oxygen))); + ui.ostc3DilTable->setItem(0, 2, new QTableWidgetItem(QString::number(deviceDetails->dil1.helium))); + ui.ostc3DilTable->setItem(0, 3, new QTableWidgetItem(QString::number(deviceDetails->dil1.type))); + ui.ostc3DilTable->setItem(0, 4, new QTableWidgetItem(QString::number(deviceDetails->dil1.depth))); + + //load dil 2 values + ui.ostc3DilTable->setItem(1, 1, new QTableWidgetItem(QString::number(deviceDetails->dil2.oxygen))); + ui.ostc3DilTable->setItem(1, 2, new QTableWidgetItem(QString::number(deviceDetails->dil2.helium))); + ui.ostc3DilTable->setItem(1, 3, new QTableWidgetItem(QString::number(deviceDetails->dil2.type))); + ui.ostc3DilTable->setItem(1, 4, new QTableWidgetItem(QString::number(deviceDetails->dil2.depth))); + + //load dil 3 values + ui.ostc3DilTable->setItem(2, 1, new QTableWidgetItem(QString::number(deviceDetails->dil3.oxygen))); + ui.ostc3DilTable->setItem(2, 2, new QTableWidgetItem(QString::number(deviceDetails->dil3.helium))); + ui.ostc3DilTable->setItem(2, 3, new QTableWidgetItem(QString::number(deviceDetails->dil3.type))); + ui.ostc3DilTable->setItem(2, 4, new QTableWidgetItem(QString::number(deviceDetails->dil3.depth))); + + //load dil 4 values + ui.ostc3DilTable->setItem(3, 1, new QTableWidgetItem(QString::number(deviceDetails->dil4.oxygen))); + ui.ostc3DilTable->setItem(3, 2, new QTableWidgetItem(QString::number(deviceDetails->dil4.helium))); + ui.ostc3DilTable->setItem(3, 3, new QTableWidgetItem(QString::number(deviceDetails->dil4.type))); + ui.ostc3DilTable->setItem(3, 4, new QTableWidgetItem(QString::number(deviceDetails->dil4.depth))); + + //load dil 5 values + ui.ostc3DilTable->setItem(4, 1, new QTableWidgetItem(QString::number(deviceDetails->dil5.oxygen))); + ui.ostc3DilTable->setItem(4, 2, new QTableWidgetItem(QString::number(deviceDetails->dil5.helium))); + ui.ostc3DilTable->setItem(4, 3, new QTableWidgetItem(QString::number(deviceDetails->dil5.type))); + ui.ostc3DilTable->setItem(4, 4, new QTableWidgetItem(QString::number(deviceDetails->dil5.depth))); + + //load set point 1 values + ui.ostc3SetPointTable->setItem(0, 1, new QTableWidgetItem(QString::number(deviceDetails->sp1.sp))); + ui.ostc3SetPointTable->setItem(0, 2, new QTableWidgetItem(QString::number(deviceDetails->sp1.depth))); + + //load set point 2 values + ui.ostc3SetPointTable->setItem(1, 1, new QTableWidgetItem(QString::number(deviceDetails->sp2.sp))); + ui.ostc3SetPointTable->setItem(1, 2, new QTableWidgetItem(QString::number(deviceDetails->sp2.depth))); + + //load set point 3 values + ui.ostc3SetPointTable->setItem(2, 1, new QTableWidgetItem(QString::number(deviceDetails->sp3.sp))); + ui.ostc3SetPointTable->setItem(2, 2, new QTableWidgetItem(QString::number(deviceDetails->sp3.depth))); + + //load set point 4 values + ui.ostc3SetPointTable->setItem(3, 1, new QTableWidgetItem(QString::number(deviceDetails->sp4.sp))); + ui.ostc3SetPointTable->setItem(3, 2, new QTableWidgetItem(QString::number(deviceDetails->sp4.depth))); + + //load set point 5 values + ui.ostc3SetPointTable->setItem(4, 1, new QTableWidgetItem(QString::number(deviceDetails->sp5.sp))); + ui.ostc3SetPointTable->setItem(4, 2, new QTableWidgetItem(QString::number(deviceDetails->sp5.depth))); +} + +void ConfigureDiveComputerDialog::reloadValuesOSTC() +{ + /* +# Not in OSTC +setBrightness +setCalibrationGas +setCompassGain +setDiveMode - Bult into setDecoType +setDiveModeColor - Lots of different colors +setFlipScreen +setLanguage +setPressureSensorOffset +setUnits +setSetPointFallback +setCcrMode +# Not in OSTC3 +setNumberOfDives +*/ + ui.serialNoLineEdit_3->setText(deviceDetails->serialNo); + ui.firmwareVersionLineEdit_3->setText(deviceDetails->firmwareVersion); + ui.customTextLlineEdit_3->setText(deviceDetails->customText); + ui.saturationSpinBox_3->setValue(deviceDetails->saturation); + ui.desaturationSpinBox_3->setValue(deviceDetails->desaturation); + ui.lastDecoSpinBox_3->setValue(deviceDetails->lastDeco); + ui.samplingRateSpinBox_3->setValue(deviceDetails->samplingRate); + ui.salinityDoubleSpinBox_3->setValue((double)deviceDetails->salinity / 100.0); + ui.dateFormatComboBox_3->setCurrentIndex(deviceDetails->dateFormat); + ui.safetyStopCheckBox_3->setChecked(deviceDetails->safetyStop); + ui.gfHighSpinBox_3->setValue(deviceDetails->gfHigh); + ui.gfLowSpinBox_3->setValue(deviceDetails->gfLow); + ui.ppO2MinSpinBox_3->setValue(deviceDetails->ppO2Min); + ui.ppO2MaxSpinBox_3->setValue(deviceDetails->ppO2Max); + ui.futureTTSSpinBox_3->setValue(deviceDetails->futureTTS); + ui.decoTypeComboBox_3->setCurrentIndex(deviceDetails->decoType); + ui.aGFSelectableCheckBox_3->setChecked(deviceDetails->aGFSelectable); + ui.aGFHighSpinBox_3->setValue(deviceDetails->aGFHigh); + ui.aGFLowSpinBox_3->setValue(deviceDetails->aGFLow); + ui.numberOfDivesSpinBox_3->setValue(deviceDetails->numberOfDives); + ui.bottomGasConsumption_3->setValue(deviceDetails->bottomGasConsumption); + ui.decoGasConsumption_3->setValue(deviceDetails->decoGasConsumption); + ui.graphicalSpeedIndicator_3->setChecked(deviceDetails->graphicalSpeedIndicator); + + //load gas 1 values + ui.ostcGasTable->setItem(0, 1, new QTableWidgetItem(QString::number(deviceDetails->gas1.oxygen))); + ui.ostcGasTable->setItem(0, 2, new QTableWidgetItem(QString::number(deviceDetails->gas1.helium))); + ui.ostcGasTable->setItem(0, 3, new QTableWidgetItem(QString::number(deviceDetails->gas1.type))); + ui.ostcGasTable->setItem(0, 4, new QTableWidgetItem(QString::number(deviceDetails->gas1.depth))); + + //load gas 2 values + ui.ostcGasTable->setItem(1, 1, new QTableWidgetItem(QString::number(deviceDetails->gas2.oxygen))); + ui.ostcGasTable->setItem(1, 2, new QTableWidgetItem(QString::number(deviceDetails->gas2.helium))); + ui.ostcGasTable->setItem(1, 3, new QTableWidgetItem(QString::number(deviceDetails->gas2.type))); + ui.ostcGasTable->setItem(1, 4, new QTableWidgetItem(QString::number(deviceDetails->gas2.depth))); + + //load gas 3 values + ui.ostcGasTable->setItem(2, 1, new QTableWidgetItem(QString::number(deviceDetails->gas3.oxygen))); + ui.ostcGasTable->setItem(2, 2, new QTableWidgetItem(QString::number(deviceDetails->gas3.helium))); + ui.ostcGasTable->setItem(2, 3, new QTableWidgetItem(QString::number(deviceDetails->gas3.type))); + ui.ostcGasTable->setItem(2, 4, new QTableWidgetItem(QString::number(deviceDetails->gas3.depth))); + + //load gas 4 values + ui.ostcGasTable->setItem(3, 1, new QTableWidgetItem(QString::number(deviceDetails->gas4.oxygen))); + ui.ostcGasTable->setItem(3, 2, new QTableWidgetItem(QString::number(deviceDetails->gas4.helium))); + ui.ostcGasTable->setItem(3, 3, new QTableWidgetItem(QString::number(deviceDetails->gas4.type))); + ui.ostcGasTable->setItem(3, 4, new QTableWidgetItem(QString::number(deviceDetails->gas4.depth))); + + //load gas 5 values + ui.ostcGasTable->setItem(4, 1, new QTableWidgetItem(QString::number(deviceDetails->gas5.oxygen))); + ui.ostcGasTable->setItem(4, 2, new QTableWidgetItem(QString::number(deviceDetails->gas5.helium))); + ui.ostcGasTable->setItem(4, 3, new QTableWidgetItem(QString::number(deviceDetails->gas5.type))); + ui.ostcGasTable->setItem(4, 4, new QTableWidgetItem(QString::number(deviceDetails->gas5.depth))); + + //load dil 1 values + ui.ostcDilTable->setItem(0, 1, new QTableWidgetItem(QString::number(deviceDetails->dil1.oxygen))); + ui.ostcDilTable->setItem(0, 2, new QTableWidgetItem(QString::number(deviceDetails->dil1.helium))); + ui.ostcDilTable->setItem(0, 3, new QTableWidgetItem(QString::number(deviceDetails->dil1.type))); + ui.ostcDilTable->setItem(0, 4, new QTableWidgetItem(QString::number(deviceDetails->dil1.depth))); + + //load dil 2 values + ui.ostcDilTable->setItem(1, 1, new QTableWidgetItem(QString::number(deviceDetails->dil2.oxygen))); + ui.ostcDilTable->setItem(1, 2, new QTableWidgetItem(QString::number(deviceDetails->dil2.helium))); + ui.ostcDilTable->setItem(1, 3, new QTableWidgetItem(QString::number(deviceDetails->dil2.type))); + ui.ostcDilTable->setItem(1, 4, new QTableWidgetItem(QString::number(deviceDetails->dil2.depth))); + + //load dil 3 values + ui.ostcDilTable->setItem(2, 1, new QTableWidgetItem(QString::number(deviceDetails->dil3.oxygen))); + ui.ostcDilTable->setItem(2, 2, new QTableWidgetItem(QString::number(deviceDetails->dil3.helium))); + ui.ostcDilTable->setItem(2, 3, new QTableWidgetItem(QString::number(deviceDetails->dil3.type))); + ui.ostcDilTable->setItem(2, 4, new QTableWidgetItem(QString::number(deviceDetails->dil3.depth))); + + //load dil 4 values + ui.ostcDilTable->setItem(3, 1, new QTableWidgetItem(QString::number(deviceDetails->dil4.oxygen))); + ui.ostcDilTable->setItem(3, 2, new QTableWidgetItem(QString::number(deviceDetails->dil4.helium))); + ui.ostcDilTable->setItem(3, 3, new QTableWidgetItem(QString::number(deviceDetails->dil4.type))); + ui.ostcDilTable->setItem(3, 4, new QTableWidgetItem(QString::number(deviceDetails->dil4.depth))); + + //load dil 5 values + ui.ostcDilTable->setItem(4, 1, new QTableWidgetItem(QString::number(deviceDetails->dil5.oxygen))); + ui.ostcDilTable->setItem(4, 2, new QTableWidgetItem(QString::number(deviceDetails->dil5.helium))); + ui.ostcDilTable->setItem(4, 3, new QTableWidgetItem(QString::number(deviceDetails->dil5.type))); + ui.ostcDilTable->setItem(4, 4, new QTableWidgetItem(QString::number(deviceDetails->dil5.depth))); + + //load set point 1 values + ui.ostcSetPointTable->setItem(0, 1, new QTableWidgetItem(QString::number(deviceDetails->sp1.sp))); + ui.ostcSetPointTable->setItem(0, 2, new QTableWidgetItem(QString::number(deviceDetails->sp1.depth))); + + //load set point 2 values + ui.ostcSetPointTable->setItem(1, 1, new QTableWidgetItem(QString::number(deviceDetails->sp2.sp))); + ui.ostcSetPointTable->setItem(1, 2, new QTableWidgetItem(QString::number(deviceDetails->sp2.depth))); + + //load set point 3 values + ui.ostcSetPointTable->setItem(2, 1, new QTableWidgetItem(QString::number(deviceDetails->sp3.sp))); + ui.ostcSetPointTable->setItem(2, 2, new QTableWidgetItem(QString::number(deviceDetails->sp3.depth))); + + //load set point 4 values + ui.ostcSetPointTable->setItem(3, 1, new QTableWidgetItem(QString::number(deviceDetails->sp4.sp))); + ui.ostcSetPointTable->setItem(3, 2, new QTableWidgetItem(QString::number(deviceDetails->sp4.depth))); + + //load set point 5 values + ui.ostcSetPointTable->setItem(4, 1, new QTableWidgetItem(QString::number(deviceDetails->sp5.sp))); + ui.ostcSetPointTable->setItem(4, 2, new QTableWidgetItem(QString::number(deviceDetails->sp5.depth))); +} + +void ConfigureDiveComputerDialog::reloadValuesSuuntoVyper() +{ + const char *depth_unit; + ui.maxDepthDoubleSpinBox->setValue(get_depth_units(deviceDetails->maxDepth, NULL, &depth_unit)); + ui.maxDepthDoubleSpinBox->setSuffix(depth_unit); + ui.totalTimeSpinBox->setValue(deviceDetails->totalTime); + ui.numberOfDivesSpinBox->setValue(deviceDetails->numberOfDives); + ui.modelLineEdit_1->setText(deviceDetails->model); + ui.firmwareVersionLineEdit_1->setText(deviceDetails->firmwareVersion); + ui.serialNoLineEdit_1->setText(deviceDetails->serialNo); + ui.customTextLlineEdit_1->setText(deviceDetails->customText); + ui.samplingRateComboBox_1->setCurrentIndex(deviceDetails->samplingRate == 60 ? 3 : (deviceDetails->samplingRate / 10) - 1); + ui.altitudeRangeComboBox->setCurrentIndex(deviceDetails->altitude); + ui.personalSafetyComboBox->setCurrentIndex(deviceDetails->personalSafety); + ui.timeFormatComboBox->setCurrentIndex(deviceDetails->timeFormat); + ui.unitsComboBox_1->setCurrentIndex(deviceDetails->units); + ui.diveModeComboBox_1->setCurrentIndex(deviceDetails->diveMode); + ui.lightCheckBox->setChecked(deviceDetails->lightEnabled); + ui.lightSpinBox->setValue(deviceDetails->light); + ui.alarmDepthCheckBox->setChecked(deviceDetails->alarmDepthEnabled); + ui.alarmDepthDoubleSpinBox->setValue(get_depth_units(deviceDetails->alarmDepth, NULL, &depth_unit)); + ui.alarmDepthDoubleSpinBox->setSuffix(depth_unit); + ui.alarmTimeCheckBox->setChecked(deviceDetails->alarmTimeEnabled); + ui.alarmTimeSpinBox->setValue(deviceDetails->alarmTime); +} + +void ConfigureDiveComputerDialog::on_backupButton_clicked() +{ + QString filename = existing_filename ?: prefs.default_filename; + QFileInfo fi(filename); + filename = fi.absolutePath().append(QDir::separator()).append("Backup.xml"); + QString backupPath = QFileDialog::getSaveFileName(this, tr("Backup dive computer settings"), + filename, tr("Backup files (*.xml)")); + if (!backupPath.isEmpty()) { + populateDeviceDetails(); + if (!config->saveXMLBackup(backupPath, deviceDetails, &device_data)) { + QMessageBox::critical(this, tr("XML backup error"), + tr("An error occurred while saving the backup file.\n%1") + .arg(config->lastError)); + } else { + QMessageBox::information(this, tr("Backup succeeded"), + tr("Your settings have been saved to: %1") + .arg(backupPath)); + } + } +} + +void ConfigureDiveComputerDialog::on_restoreBackupButton_clicked() +{ + QString filename = existing_filename ?: prefs.default_filename; + QFileInfo fi(filename); + filename = fi.absolutePath().append(QDir::separator()).append("Backup.xml"); + QString restorePath = QFileDialog::getOpenFileName(this, tr("Restore dive computer settings"), + filename, tr("Backup files (*.xml)")); + if (!restorePath.isEmpty()) { + // Fw update is no longer a option, needs to be done on a untouched device + ui.updateFirmwareButton->setEnabled(false); + if (!config->restoreXMLBackup(restorePath, deviceDetails)) { + QMessageBox::critical(this, tr("XML restore error"), + tr("An error occurred while restoring the backup file.\n%1") + .arg(config->lastError)); + } else { + reloadValues(); + QMessageBox::information(this, tr("Restore succeeded"), + tr("Your settings have been restored successfully.")); + } + } +} + +void ConfigureDiveComputerDialog::on_updateFirmwareButton_clicked() +{ + QString filename = existing_filename ?: prefs.default_filename; + QFileInfo fi(filename); + filename = fi.absolutePath(); + QString firmwarePath = QFileDialog::getOpenFileName(this, tr("Select firmware file"), + filename, tr("All files (*.*)")); + if (!firmwarePath.isEmpty()) { + ui.progressBar->setValue(0); + ui.progressBar->setFormat("%p%"); + ui.progressBar->setTextVisible(true); + + config->startFirmwareUpdate(firmwarePath, &device_data); + } +} + + +void ConfigureDiveComputerDialog::on_DiveComputerList_currentRowChanged(int currentRow) +{ + switch (currentRow) { + case 0: + selected_vendor = "Heinrichs Weikamp"; + selected_product = "OSTC 3"; + fw_upgrade_possible = true; + break; + case 1: + selected_vendor = "Suunto"; + selected_product = "Vyper"; + fw_upgrade_possible = false; + break; + case 2: + selected_vendor = "Heinrichs Weikamp"; + selected_product = "OSTC 2N"; + fw_upgrade_possible = true; + break; + default: + /* Not Supported */ + return; + } + + int dcType = DC_TYPE_SERIAL; + + + if (selected_vendor == QString("Uemis")) + dcType = DC_TYPE_UEMIS; + fill_device_list(dcType); +} + +void ConfigureDiveComputerDialog::checkLogFile(int state) +{ + ui.chooseLogFile->setEnabled(state == Qt::Checked); + device_data.libdc_log = (state == Qt::Checked); + if (state == Qt::Checked && logFile.isEmpty()) { + pickLogFile(); + } +} + +void ConfigureDiveComputerDialog::pickLogFile() +{ + QString filename = existing_filename ?: prefs.default_filename; + QFileInfo fi(filename); + filename = fi.absolutePath().append(QDir::separator()).append("subsurface.log"); + logFile = QFileDialog::getSaveFileName(this, tr("Choose file for divecomputer download logfile"), + filename, tr("Log files (*.log)")); + if (!logFile.isEmpty()) { + free(logfile_name); + logfile_name = strdup(logFile.toUtf8().data()); + } +} + +#ifdef BT_SUPPORT +void ConfigureDiveComputerDialog::selectRemoteBluetoothDevice() +{ + if (!btDeviceSelectionDialog) { + btDeviceSelectionDialog = new BtDeviceSelectionDialog(this); + connect(btDeviceSelectionDialog, SIGNAL(finished(int)), + this, SLOT(bluetoothSelectionDialogIsFinished(int))); + } + + btDeviceSelectionDialog->show(); +} + +void ConfigureDiveComputerDialog::bluetoothSelectionDialogIsFinished(int result) +{ + if (result == QDialog::Accepted) { + ui.device->setCurrentText(btDeviceSelectionDialog->getSelectedDeviceAddress()); + device_data.bluetooth_mode = true; + + ui.progressBar->setFormat("Connecting to device..."); + dc_open(); + } +} +#endif + +void ConfigureDiveComputerDialog::dc_open() +{ + getDeviceData(); + + QString err = config->dc_open(&device_data); + + if (err != "") + return ui.progressBar->setFormat(err); + + ui.retrieveDetails->setEnabled(true); + ui.resetButton->setEnabled(true); + ui.updateFirmwareButton->setEnabled(true); + ui.disconnectButton->setEnabled(true); + ui.restoreBackupButton->setEnabled(true); + ui.connectButton->setEnabled(false); + ui.bluetoothMode->setEnabled(false); + ui.DiveComputerList->setEnabled(false); + ui.logToFile->setEnabled(false); + if (fw_upgrade_possible) + ui.updateFirmwareButton->setEnabled(true); + ui.progressBar->setFormat("Connected to device"); +} + +void ConfigureDiveComputerDialog::dc_close() +{ + config->dc_close(&device_data); + + ui.retrieveDetails->setEnabled(false); + ui.resetButton->setEnabled(false); + ui.updateFirmwareButton->setEnabled(false); + ui.disconnectButton->setEnabled(false); + ui.connectButton->setEnabled(true); + ui.bluetoothMode->setEnabled(true); + ui.backupButton->setEnabled(false); + ui.saveSettingsPushButton->setEnabled(false); + ui.restoreBackupButton->setEnabled(false); + ui.DiveComputerList->setEnabled(true); + ui.logToFile->setEnabled(true); + ui.updateFirmwareButton->setEnabled(false); + ui.progressBar->setFormat("Disonnected from device"); + ui.progressBar->setValue(0); +} diff --git a/desktop-widgets/configuredivecomputerdialog.h b/desktop-widgets/configuredivecomputerdialog.h new file mode 100644 index 000000000..9ad30ac67 --- /dev/null +++ b/desktop-widgets/configuredivecomputerdialog.h @@ -0,0 +1,149 @@ +#ifndef CONFIGUREDIVECOMPUTERDIALOG_H +#define CONFIGUREDIVECOMPUTERDIALOG_H + +#include <QDialog> +#include <QStringListModel> +#include "ui_configuredivecomputerdialog.h" +#include "subsurface-core/libdivecomputer.h" +#include "configuredivecomputer.h" +#include <QStyledItemDelegate> +#include <QNetworkAccessManager> +#ifdef BT_SUPPORT +#include "btdeviceselectiondialog.h" +#endif + +class GasSpinBoxItemDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + enum column_type { + PERCENT, + DEPTH, + SETPOINT, + }; + + GasSpinBoxItemDelegate(QObject *parent = 0, column_type type = PERCENT); + ~GasSpinBoxItemDelegate(); + + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const; + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; + +private: + column_type type; +}; + +class GasTypeComboBoxItemDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + enum computer_type { + OSTC3, + OSTC, + }; + + GasTypeComboBoxItemDelegate(QObject *parent = 0, computer_type type = OSTC3); + ~GasTypeComboBoxItemDelegate(); + + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const; + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; + +private: + computer_type type; +}; + +class ConfigureDiveComputerDialog : public QDialog { + Q_OBJECT + +public: + explicit ConfigureDiveComputerDialog(QWidget *parent = 0); + ~ConfigureDiveComputerDialog(); + +protected: + void closeEvent(QCloseEvent *event); + +private +slots: + void checkLogFile(int state); + void pickLogFile(); + void readSettings(); + void resetSettings(); + void configMessage(QString msg); + void configError(QString err); + void on_cancel_clicked(); + void on_saveSettingsPushButton_clicked(); + void deviceDetailsReceived(DeviceDetails *newDeviceDetails); + void reloadValues(); + void on_backupButton_clicked(); + + void on_restoreBackupButton_clicked(); + + + void on_updateFirmwareButton_clicked(); + + void on_DiveComputerList_currentRowChanged(int currentRow); + + void dc_open(); + void dc_close(); + +#ifdef BT_SUPPORT + void bluetoothSelectionDialogIsFinished(int result); + void selectRemoteBluetoothDevice(); +#endif + +private: + Ui::ConfigureDiveComputerDialog ui; + + QString logFile; + + QStringList vendorList; + QHash<QString, QStringList> productList; + + ConfigureDiveComputer *config; + device_data_t device_data; + void getDeviceData(); + + QHash<QString, dc_descriptor_t *> descriptorLookup; + void fill_device_list(int dc_type); + void fill_computer_list(); + + DeviceDetails *deviceDetails; + void populateDeviceDetails(); + void populateDeviceDetailsOSTC3(); + void populateDeviceDetailsOSTC(); + void populateDeviceDetailsSuuntoVyper(); + void reloadValuesOSTC3(); + void reloadValuesOSTC(); + void reloadValuesSuuntoVyper(); + + QString selected_vendor; + QString selected_product; + bool fw_upgrade_possible; + +#ifdef BT_SUPPORT + BtDeviceSelectionDialog *btDeviceSelectionDialog; +#endif +}; + +class OstcFirmwareCheck : QObject { + Q_OBJECT +public: + explicit OstcFirmwareCheck(QString product); + void checkLatest(QWidget *parent, device_data_t *data); +public +slots: + void parseOstcFwVersion(QNetworkReply *reply); + void saveOstcFirmware(QNetworkReply *reply); + +private: + void upgradeFirmware(); + device_data_t devData; + QString latestFirmwareAvailable; + QString latestFirmwareHexFile; + QString storeFirmware; + QWidget *parent; + QNetworkAccessManager manager; +}; + +#endif // CONFIGUREDIVECOMPUTERDIALOG_H diff --git a/desktop-widgets/configuredivecomputerdialog.ui b/desktop-widgets/configuredivecomputerdialog.ui new file mode 100644 index 000000000..0986d71ca --- /dev/null +++ b/desktop-widgets/configuredivecomputerdialog.ui @@ -0,0 +1,2758 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ConfigureDiveComputerDialog</class> + <widget class="QDialog" name="ConfigureDiveComputerDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>842</width> + <height>614</height> + </rect> + </property> + <property name="windowTitle"> + <string>Configure dive computer</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label_1"> + <property name="text"> + <string>Device or mount point</string> + </property> + <property name="buddy"> + <cstring>device</cstring> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QComboBox" name="device"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="bluetoothMode"> + <property name="text"> + <string>Connect via Bluetooth</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="connectButton"> + <property name="text"> + <string>Connect</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="disconnectButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Disconnect</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QPushButton" name="retrieveDetails"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Retrieve available details</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="saveSettingsPushButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Read settings from backup file or from device before writing to the device</string> + </property> + <property name="text"> + <string>Save changes to device</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="backupButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Read settings from backup file or from device before writing to a backup file</string> + </property> + <property name="text"> + <string>Backup</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="restoreBackupButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Restore backup</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="updateFirmwareButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Update firmware</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QCheckBox" name="logToFile"> + <property name="text"> + <string>Save libdivecomputer logfile</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="chooseLogFile"> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="cancel"> + <property name="text"> + <string>Cancel</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QSplitter" name="splitter"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>2</verstretch> + </sizepolicy> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <widget class="QListWidget" name="DiveComputerList"> + <property name="maximumSize"> + <size> + <width>240</width> + <height>16777215</height> + </size> + </property> + <property name="font"> + <font> + <pointsize>12</pointsize> + </font> + </property> + <property name="iconSize"> + <size> + <width>64</width> + <height>64</height> + </size> + </property> + <item> + <property name="text"> + <string>OSTC 3,Sport,Cr,2</string> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/icons/ostc3.png</normaloff>:/icons/ostc3.png</iconset> + </property> + </item> + <item> + <property name="text"> + <string>Suunto Vyper family</string> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/icons/suunto_vyper.png</normaloff>:/icons/suunto_vyper.png</iconset> + </property> + </item> + <item> + <property name="text"> + <string>OSTC, Mk.2/2N/2C</string> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/icons/ostc2n.png</normaloff>:/icons/ostc2n.png</iconset> + </property> + </item> + </widget> + <widget class="QStackedWidget" name="dcStackedWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="page_ostc3"> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QTabWidget" name="tabWidget1"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="basicSettings"> + <attribute name="title"> + <string>Basic settings</string> + </attribute> + <layout class="QGridLayout" name="gridLayout"> + <item row="2" column="4"> + <widget class="QComboBox" name="languageComboBox"> + <item> + <property name="text"> + <string>English</string> + </property> + </item> + <item> + <property name="text"> + <string>German</string> + </property> + </item> + <item> + <property name="text"> + <string>French</string> + </property> + </item> + <item> + <property name="text"> + <string>Italian</string> + </property> + </item> + </widget> + </item> + <item row="6" column="4"> + <widget class="QComboBox" name="unitsComboBox"> + <item> + <property name="text"> + <string>m/°C</string> + </property> + </item> + <item> + <property name="text"> + <string>ft/°F</string> + </property> + </item> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Serial No.</string> + </property> + <property name="buddy"> + <cstring>serialNoLineEdit</cstring> + </property> + </widget> + </item> + <item row="0" column="1" colspan="2"> + <widget class="QLineEdit" name="serialNoLineEdit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Firmware version</string> + </property> + <property name="buddy"> + <cstring>firmwareVersionLineEdit</cstring> + </property> + </widget> + </item> + <item row="0" column="4"> + <widget class="QLineEdit" name="firmwareVersionLineEdit"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="3"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Language</string> + </property> + <property name="buddy"> + <cstring>languageComboBox</cstring> + </property> + </widget> + </item> + <item row="3" column="4"> + <widget class="QComboBox" name="dateFormatComboBox"> + <item> + <property name="text"> + <string>MMDDYY</string> + </property> + </item> + <item> + <property name="text"> + <string>DDMMYY</string> + </property> + </item> + <item> + <property name="text"> + <string>YYMMDD</string> + </property> + </item> + </widget> + </item> + <item row="5" column="4"> + <widget class="QComboBox" name="brightnessComboBox"> + <item> + <property name="text"> + <string>Eco</string> + </property> + </item> + <item> + <property name="text"> + <string>Medium</string> + </property> + </item> + <item> + <property name="text"> + <string>High</string> + </property> + </item> + </widget> + </item> + <item row="3" column="3"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Date format</string> + </property> + <property name="buddy"> + <cstring>dateFormatComboBox</cstring> + </property> + </widget> + </item> + <item row="5" column="3"> + <widget class="QLabel" name="label_10"> + <property name="text"> + <string>Brightness</string> + </property> + <property name="buddy"> + <cstring>brightnessComboBox</cstring> + </property> + </widget> + </item> + <item row="6" column="3"> + <widget class="QLabel" name="label_12"> + <property name="text"> + <string>Units</string> + </property> + <property name="buddy"> + <cstring>unitsComboBox</cstring> + </property> + </widget> + </item> + <item row="7" column="3"> + <widget class="QLabel" name="label_14"> + <property name="text"> + <string>Salinity (0-5%)</string> + </property> + <property name="buddy"> + <cstring>salinitySpinBox</cstring> + </property> + </widget> + </item> + <item row="7" column="4"> + <widget class="QSpinBox" name="salinitySpinBox"> + <property name="suffix"> + <string>%</string> + </property> + <property name="maximum"> + <number>5</number> + </property> + </widget> + </item> + <item row="8" column="4"> + <widget class="QComboBox" name="compassGainComboBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <item> + <property name="text"> + <string>230LSB/Gauss</string> + </property> + </item> + <item> + <property name="text"> + <string>330LSB/Gauss</string> + </property> + </item> + <item> + <property name="text"> + <string>390LSB/Gauss</string> + </property> + </item> + <item> + <property name="text"> + <string>440LSB/Gauss</string> + </property> + </item> + <item> + <property name="text"> + <string>660LSB/Gauss</string> + </property> + </item> + <item> + <property name="text"> + <string>820LSB/Gauss</string> + </property> + </item> + <item> + <property name="text"> + <string>1090LSB/Gauss</string> + </property> + </item> + <item> + <property name="text"> + <string>1370LSB/Gauss</string> + </property> + </item> + </widget> + </item> + <item row="9" column="2"> + <spacer name="verticalSpacer1"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>177</height> + </size> + </property> + </spacer> + </item> + <item row="11" column="3" colspan="2"> + <widget class="QPushButton" name="resetButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Reset device to default settings</string> + </property> + </widget> + </item> + <item row="8" column="3"> + <widget class="QLabel" name="label_15"> + <property name="text"> + <string>Compass gain</string> + </property> + <property name="buddy"> + <cstring>compassGainComboBox</cstring> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Custom text</string> + </property> + <property name="buddy"> + <cstring>customTextLlineEdit</cstring> + </property> + </widget> + </item> + <item row="1" column="1" colspan="2"> + <widget class="QLineEdit" name="customTextLlineEdit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maxLength"> + <number>60</number> + </property> + </widget> + </item> + <item row="1" column="3"> + <widget class="QLabel" name="label_62"> + <property name="text"> + <string>Computer model</string> + </property> + </widget> + </item> + <item row="1" column="4"> + <widget class="QLineEdit" name="modelLineEdit"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Dive mode</string> + </property> + <property name="buddy"> + <cstring>diveModeComboBox</cstring> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QComboBox" name="diveModeComboBox"> + <item> + <property name="text"> + <string>OC</string> + </property> + </item> + <item> + <property name="text"> + <string>CC</string> + </property> + </item> + <item> + <property name="text"> + <string>Gauge</string> + </property> + </item> + <item> + <property name="text"> + <string>Apnea</string> + </property> + </item> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_11"> + <property name="text"> + <string>Sampling rate</string> + </property> + <property name="buddy"> + <cstring>samplingRateComboBox</cstring> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QComboBox" name="samplingRateComboBox"> + <item> + <property name="text"> + <string>2s</string> + </property> + </item> + <item> + <property name="text"> + <string>10s</string> + </property> + </item> + </widget> + </item> + <item row="5" column="0"> + <widget class="QLabel" name="label_13"> + <property name="text"> + <string>Dive mode color</string> + </property> + <property name="buddy"> + <cstring>diveModeColour</cstring> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QComboBox" name="diveModeColour"> + <item> + <property name="text"> + <string>Standard</string> + </property> + </item> + <item> + <property name="text"> + <string>Red</string> + </property> + </item> + <item> + <property name="text"> + <string>Green</string> + </property> + </item> + <item> + <property name="text"> + <string>Blue</string> + </property> + </item> + </widget> + </item> + <item row="6" column="0" colspan="3"> + <widget class="QCheckBox" name="dateTimeSyncCheckBox"> + <property name="text"> + <string>Sync dive computer time with PC</string> + </property> + </widget> + </item> + <item row="7" column="0" colspan="3"> + <widget class="QCheckBox" name="safetyStopCheckBox"> + <property name="text"> + <string>Show safety stop</string> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="advancedSettings"> + <attribute name="title"> + <string>Advanced settings</string> + </attribute> + <layout class="QGridLayout" name="gridLayout1"> + <item row="10" column="3"> + <widget class="QLabel" name="label_43"> + <property name="text"> + <string>Left button sensitivity</string> + </property> + </widget> + </item> + <item row="11" column="0" colspan="2"> + <widget class="QCheckBox" name="alwaysShowppO2"> + <property name="text"> + <string>Always show ppO2</string> + </property> + </widget> + </item> + <item row="3" column="0" colspan="2"> + <widget class="QCheckBox" name="aGFSelectableCheckBox"> + <property name="text"> + <string>Alt GF can be selected underwater</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_31"> + <property name="text"> + <string>Future TTS</string> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QLabel" name="label_34"> + <property name="text"> + <string>Pressure sensor offset</string> + </property> + </widget> + </item> + <item row="1" column="3"> + <widget class="QLabel" name="label_32"> + <property name="text"> + <string>GFLow</string> + </property> + </widget> + </item> + <item row="1" column="4"> + <widget class="QSpinBox" name="gfLowSpinBox"> + <property name="suffix"> + <string>%</string> + </property> + <property name="minimum"> + <number>10</number> + </property> + <property name="maximum"> + <number>100</number> + </property> + <property name="value"> + <number>30</number> + </property> + </widget> + </item> + <item row="2" column="3"> + <widget class="QLabel" name="label_33"> + <property name="text"> + <string>GFHigh</string> + </property> + </widget> + </item> + <item row="2" column="4"> + <widget class="QSpinBox" name="gfHighSpinBox"> + <property name="suffix"> + <string>%</string> + </property> + <property name="minimum"> + <number>60</number> + </property> + <property name="maximum"> + <number>110</number> + </property> + <property name="value"> + <number>85</number> + </property> + </widget> + </item> + <item row="9" column="3"> + <widget class="QLabel" name="label_8"> + <property name="text"> + <string>Desaturation</string> + </property> + <property name="buddy"> + <cstring>desaturationSpinBox</cstring> + </property> + </widget> + </item> + <item row="9" column="4"> + <widget class="QSpinBox" name="desaturationSpinBox"> + <property name="suffix"> + <string>%</string> + </property> + <property name="minimum"> + <number>60</number> + </property> + <property name="maximum"> + <number>100</number> + </property> + <property name="value"> + <number>90</number> + </property> + </widget> + </item> + <item row="14" column="0"> + <spacer name="verticalSpacer_4"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="1"> + <widget class="QSpinBox" name="lastDecoSpinBox"> + <property name="suffix"> + <string> m</string> + </property> + <property name="minimum"> + <number>3</number> + </property> + <property name="maximum"> + <number>6</number> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label_28"> + <property name="text"> + <string>Decotype</string> + </property> + </widget> + </item> + <item row="3" column="4"> + <widget class="QSpinBox" name="aGFLowSpinBox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="suffix"> + <string>%</string> + </property> + <property name="minimum"> + <number>60</number> + </property> + <property name="maximum"> + <number>100</number> + </property> + <property name="value"> + <number>60</number> + </property> + </widget> + </item> + <item row="0" column="4"> + <widget class="QSpinBox" name="pressureSensorOffsetSpinBox"> + <property name="suffix"> + <string> mbar</string> + </property> + <property name="minimum"> + <number>-20</number> + </property> + <property name="maximum"> + <number>20</number> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="decoTypeComboBox"> + <property name="currentIndex"> + <number>1</number> + </property> + <item> + <property name="text"> + <string>ZH-L16</string> + </property> + </item> + <item> + <property name="text"> + <string>ZH-L16+GF</string> + </property> + </item> + </widget> + </item> + <item row="1" column="1"> + <widget class="QSpinBox" name="futureTTSSpinBox"> + <property name="suffix"> + <string> min</string> + </property> + <property name="maximum"> + <number>9</number> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_9"> + <property name="text"> + <string>Last deco</string> + </property> + <property name="buddy"> + <cstring>lastDecoSpinBox</cstring> + </property> + </widget> + </item> + <item row="8" column="4"> + <widget class="QSpinBox" name="saturationSpinBox"> + <property name="suffix"> + <string>%</string> + </property> + <property name="minimum"> + <number>100</number> + </property> + <property name="maximum"> + <number>140</number> + </property> + <property name="value"> + <number>110</number> + </property> + </widget> + </item> + <item row="3" column="3"> + <widget class="QLabel" name="label_36"> + <property name="text"> + <string>Alt GFLow</string> + </property> + </widget> + </item> + <item row="6" column="4"> + <widget class="QSpinBox" name="aGFHighSpinBox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="suffix"> + <string>%</string> + </property> + <property name="minimum"> + <number>70</number> + </property> + <property name="maximum"> + <number>120</number> + </property> + <property name="value"> + <number>85</number> + </property> + </widget> + </item> + <item row="6" column="3"> + <widget class="QLabel" name="label_37"> + <property name="text"> + <string>Alt GFHigh</string> + </property> + </widget> + </item> + <item row="8" column="3"> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>Saturation</string> + </property> + <property name="buddy"> + <cstring>saturationSpinBox</cstring> + </property> + </widget> + </item> + <item row="6" column="0"> + <widget class="QCheckBox" name="flipScreenCheckBox"> + <property name="text"> + <string>Flip screen</string> + </property> + </widget> + </item> + <item row="11" column="3"> + <widget class="QLabel" name="label_44"> + <property name="text"> + <string>Right button sensitivity</string> + </property> + </widget> + </item> + <item row="8" column="0"> + <widget class="QCheckBox" name="modWarning"> + <property name="text"> + <string>MOD warning</string> + </property> + </widget> + </item> + <item row="10" column="0" colspan="2"> + <widget class="QCheckBox" name="graphicalSpeedIndicator"> + <property name="text"> + <string>Graphical speed indicator</string> + </property> + </widget> + </item> + <item row="9" column="0" colspan="2"> + <widget class="QCheckBox" name="dynamicAscendRate"> + <property name="text"> + <string>Dynamic ascent rate</string> + </property> + </widget> + </item> + <item row="12" column="3"> + <widget class="QLabel" name="label_46"> + <property name="text"> + <string>Bottom gas consumption</string> + </property> + </widget> + </item> + <item row="13" column="3"> + <widget class="QLabel" name="label_48"> + <property name="text"> + <string>Deco gas consumption</string> + </property> + </widget> + </item> + <item row="10" column="4"> + <widget class="QSpinBox" name="leftButtonSensitivity"> + <property name="suffix"> + <string>%</string> + </property> + <property name="minimum"> + <number>20</number> + </property> + <property name="maximum"> + <number>100</number> + </property> + <property name="value"> + <number>40</number> + </property> + </widget> + </item> + <item row="11" column="4"> + <widget class="QSpinBox" name="rightButtonSensitivity"> + <property name="suffix"> + <string>%</string> + </property> + <property name="minimum"> + <number>20</number> + </property> + <property name="maximum"> + <number>100</number> + </property> + <property name="value"> + <number>40</number> + </property> + </widget> + </item> + <item row="12" column="4"> + <widget class="QSpinBox" name="bottomGasConsumption"> + <property name="suffix"> + <string> â„“/min</string> + </property> + <property name="minimum"> + <number>5</number> + </property> + <property name="maximum"> + <number>50</number> + </property> + <property name="value"> + <number>20</number> + </property> + </widget> + </item> + <item row="13" column="4"> + <widget class="QSpinBox" name="decoGasConsumption"> + <property name="suffix"> + <string> â„“/min</string> + </property> + <property name="minimum"> + <number>5</number> + </property> + <property name="maximum"> + <number>50</number> + </property> + <property name="value"> + <number>20</number> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="gasSettings"> + <attribute name="title"> + <string>Gas settings</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0" colspan="2"> + <widget class="QTableWidget" name="ostc3GasTable"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <column> + <property name="text"> + <string/> + </property> + </column> + <column> + <property name="text"> + <string>%Oâ‚‚</string> + </property> + </column> + <column> + <property name="text"> + <string>%He</string> + </property> + </column> + <column> + <property name="text"> + <string>Type</string> + </property> + </column> + <column> + <property name="text"> + <string>Change depth</string> + </property> + </column> + <item row="0" column="0"> + <property name="text"> + <string>Gas 1</string> + </property> + </item> + <item row="1" column="0"> + <property name="text"> + <string>Gas 2</string> + </property> + </item> + <item row="2" column="0"> + <property name="text"> + <string>Gas 3</string> + </property> + </item> + <item row="3" column="0"> + <property name="text"> + <string>Gas 4</string> + </property> + </item> + <item row="4" column="0"> + <property name="text"> + <string>Gas 5</string> + </property> + </item> + </widget> + </item> + <item row="0" column="2" colspan="2"> + <widget class="QTableWidget" name="ostc3DilTable"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <column> + <property name="text"> + <string/> + </property> + </column> + <column> + <property name="text"> + <string>%Oâ‚‚</string> + </property> + </column> + <column> + <property name="text"> + <string>%He</string> + </property> + </column> + <column> + <property name="text"> + <string>Type</string> + </property> + </column> + <column> + <property name="text"> + <string>Change depth</string> + </property> + </column> + <item row="0" column="0"> + <property name="text"> + <string>Dil 1</string> + </property> + </item> + <item row="1" column="0"> + <property name="text"> + <string>Dil 2</string> + </property> + </item> + <item row="2" column="0"> + <property name="text"> + <string>Dil 3</string> + </property> + </item> + <item row="3" column="0"> + <property name="text"> + <string>Dil 4</string> + </property> + </item> + <item row="4" column="0"> + <property name="text"> + <string>Dil 5</string> + </property> + </item> + </widget> + </item> + <item row="2" column="0" rowspan="5" colspan="2"> + <widget class="QTableWidget" name="ostc3SetPointTable"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <column> + <property name="text"> + <string/> + </property> + </column> + <column> + <property name="text"> + <string>Setpoint</string> + </property> + </column> + <column> + <property name="text"> + <string>Change depth</string> + </property> + </column> + <item row="0" column="0"> + <property name="text"> + <string>SP 1</string> + </property> + </item> + <item row="1" column="0"> + <property name="text"> + <string>SP 2</string> + </property> + </item> + <item row="2" column="0"> + <property name="text"> + <string>SP 3</string> + </property> + </item> + <item row="3" column="0"> + <property name="text"> + <string>SP 4</string> + </property> + </item> + <item row="4" column="0"> + <property name="text"> + <string>SP 5</string> + </property> + </item> + </widget> + </item> + <item row="2" column="2"> + <widget class="QLabel" name="label_35"> + <property name="text"> + <string>Oâ‚‚ in calibration gas</string> + </property> + </widget> + </item> + <item row="2" column="3"> + <widget class="QSpinBox" name="calibrationGasSpinBox"> + <property name="suffix"> + <string>%</string> + </property> + <property name="minimum"> + <number>21</number> + </property> + <property name="maximum"> + <number>100</number> + </property> + <property name="value"> + <number>21</number> + </property> + </widget> + </item> + <item row="3" column="2"> + <widget class="QComboBox" name="ccrModeComboBox"> + <item> + <property name="text"> + <string>Fixed setpoint</string> + </property> + </item> + <item> + <property name="text"> + <string>Sensor</string> + </property> + </item> + </widget> + </item> + <item row="3" column="3"> + <widget class="QCheckBox" name="setPointFallbackCheckBox"> + <property name="text"> + <string>Setpoint fallback</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="6" column="2"> + <spacer name="verticalSpacer_3"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item row="4" column="3"> + <widget class="QSpinBox" name="ppO2MaxSpinBox"> + <property name="suffix"> + <string> cbar</string> + </property> + <property name="minimum"> + <number>120</number> + </property> + <property name="maximum"> + <number>160</number> + </property> + <property name="value"> + <number>160</number> + </property> + </widget> + </item> + <item row="5" column="3"> + <widget class="QSpinBox" name="ppO2MinSpinBox"> + <property name="suffix"> + <string> cbar</string> + </property> + <property name="minimum"> + <number>16</number> + </property> + <property name="maximum"> + <number>19</number> + </property> + <property name="value"> + <number>19</number> + </property> + </widget> + </item> + <item row="4" column="2"> + <widget class="QLabel" name="label_29"> + <property name="text"> + <string>pOâ‚‚ max</string> + </property> + </widget> + </item> + <item row="5" column="2"> + <widget class="QLabel" name="label_30"> + <property name="text"> + <string>pOâ‚‚ min</string> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="page_vyper"> + <layout class="QVBoxLayout"> + <item> + <widget class="QTabWidget" name="tabWidget2"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="basicSettings2"> + <attribute name="title"> + <string>Basic settings</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="1" column="1" colspan="2"> + <widget class="QDoubleSpinBox" name="maxDepthDoubleSpinBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="maximum"> + <double>200.000000000000000</double> + </property> + </widget> + </item> + <item row="6" column="0"> + <widget class="QLabel" name="label_21"> + <property name="text"> + <string>Safety level</string> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QComboBox" name="altitudeRangeComboBox"> + <item> + <property name="text"> + <string notr="true">A0 (0m - 300m)</string> + </property> + </item> + <item> + <property name="text"> + <string notr="true">A1 (300m - 1500m)</string> + </property> + </item> + <item> + <property name="text"> + <string notr="true">A2 (1500m - 3000m)</string> + </property> + </item> + </widget> + </item> + <item row="5" column="0"> + <widget class="QLabel" name="label_20"> + <property name="text"> + <string>Altitude range</string> + </property> + </widget> + </item> + <item row="9" column="0"> + <widget class="QLabel" name="label_22"> + <property name="text"> + <string>Model</string> + </property> + </widget> + </item> + <item row="3" column="1" colspan="2"> + <widget class="QLineEdit" name="customTextLlineEdit_1"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maxLength"> + <number>30</number> + </property> + </widget> + </item> + <item row="1" column="3"> + <widget class="QLabel" name="label_16"> + <property name="text"> + <string>Number of dives</string> + </property> + </widget> + </item> + <item row="11" column="0"> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label1"> + <property name="text"> + <string>Serial No.</string> + </property> + <property name="buddy"> + <cstring>serialNoLineEdit_1</cstring> + </property> + </widget> + </item> + <item row="0" column="1" colspan="2"> + <widget class="QLineEdit" name="serialNoLineEdit_1"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QLabel" name="label_17"> + <property name="text"> + <string>Firmware version</string> + </property> + <property name="buddy"> + <cstring>firmwareVersionLineEdit_1</cstring> + </property> + </widget> + </item> + <item row="0" column="4"> + <widget class="QLineEdit" name="firmwareVersionLineEdit_1"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_18"> + <property name="text"> + <string>Max depth</string> + </property> + </widget> + </item> + <item row="1" column="4"> + <widget class="QSpinBox" name="numberOfDivesSpinBox"> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="maximum"> + <number>5000</number> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_19"> + <property name="text"> + <string>Custom text</string> + </property> + <property name="buddy"> + <cstring>customTextLlineEdit_1</cstring> + </property> + </widget> + </item> + <item row="9" column="1"> + <widget class="QComboBox" name="diveModeComboBox_1"> + <item> + <property name="text"> + <string notr="true" extracomment="Suunto computer mode">Air</string> + </property> + </item> + <item> + <property name="text"> + <string notr="true" extracomment="Suunto computer mode">Nitrox</string> + </property> + </item> + <item> + <property name="text"> + <string notr="true" extracomment="Suunto computer mode">Gauge</string> + </property> + </item> + </widget> + </item> + <item row="6" column="1"> + <widget class="QComboBox" name="personalSafetyComboBox"> + <item> + <property name="text"> + <string extracomment="Suunto safety level">P0 (none)</string> + </property> + </item> + <item> + <property name="text"> + <string extracomment="Suunto safety level">P1 (medium)</string> + </property> + </item> + <item> + <property name="text"> + <string extracomment="Suunto safety level">P2 (high)</string> + </property> + </item> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_23"> + <property name="text"> + <string>Sample rate</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QComboBox" name="samplingRateComboBox_1"> + <item> + <property name="text"> + <string>10s</string> + </property> + </item> + <item> + <property name="text"> + <string>20s</string> + </property> + </item> + <item> + <property name="text"> + <string>30s</string> + </property> + </item> + <item> + <property name="text"> + <string>60s</string> + </property> + </item> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_24"> + <property name="text"> + <string>Total dive time</string> + </property> + </widget> + </item> + <item row="2" column="3"> + <widget class="QLabel" name="label_25"> + <property name="text"> + <string>Computer model</string> + </property> + </widget> + </item> + <item row="2" column="4"> + <widget class="QLineEdit" name="modelLineEdit_1"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="1" colspan="2"> + <widget class="QSpinBox" name="totalTimeSpinBox"> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="suffix"> + <string>min</string> + </property> + <property name="minimum"> + <number>0</number> + </property> + <property name="maximum"> + <number>5000000</number> + </property> + </widget> + </item> + <item row="7" column="1"> + <widget class="QComboBox" name="timeFormatComboBox"> + <item> + <property name="text"> + <string>24h</string> + </property> + </item> + <item> + <property name="text"> + <string>12h</string> + </property> + </item> + </widget> + </item> + <item row="7" column="0"> + <widget class="QLabel" name="label_26"> + <property name="text"> + <string>Time format</string> + </property> + </widget> + </item> + <item row="8" column="0"> + <widget class="QLabel" name="label_27"> + <property name="text"> + <string>Units</string> + </property> + </widget> + </item> + <item row="8" column="1"> + <widget class="QComboBox" name="unitsComboBox_1"> + <item> + <property name="text"> + <string>Imperial</string> + </property> + </item> + <item> + <property name="text"> + <string>Metric</string> + </property> + </item> + </widget> + </item> + <item row="4" column="4"> + <widget class="QSpinBox" name="lightSpinBox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="suffix"> + <string>s</string> + </property> + </widget> + </item> + <item row="4" column="3"> + <widget class="QCheckBox" name="lightCheckBox"> + <property name="text"> + <string>Light</string> + </property> + </widget> + </item> + <item row="5" column="4"> + <widget class="QDoubleSpinBox" name="alarmDepthDoubleSpinBox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="maximum"> + <double>200.000000000000000</double> + </property> + </widget> + </item> + <item row="5" column="3"> + <widget class="QCheckBox" name="alarmDepthCheckBox"> + <property name="text"> + <string>Depth alarm</string> + </property> + </widget> + </item> + <item row="6" column="4"> + <widget class="QSpinBox" name="alarmTimeSpinBox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="suffix"> + <string>min</string> + </property> + <property name="maximum"> + <number>999</number> + </property> + </widget> + </item> + <item row="6" column="3"> + <widget class="QCheckBox" name="alarmTimeCheckBox"> + <property name="text"> + <string>Time alarm</string> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="page_ostc"> + <layout class="QVBoxLayout"> + <item> + <widget class="QTabWidget" name="tabWidget3"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="basicSettings3"> + <attribute name="title"> + <string>Basic settings</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_4"> + <item row="5" column="3"> + <widget class="QLabel" name="label_45"> + <property name="text"> + <string>Salinity</string> + </property> + <property name="buddy"> + <cstring>salinitySpinBox</cstring> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label_38"> + <property name="text"> + <string>Serial No.</string> + </property> + <property name="buddy"> + <cstring>serialNoLineEdit</cstring> + </property> + </widget> + </item> + <item row="0" column="1" colspan="2"> + <widget class="QLineEdit" name="serialNoLineEdit_3"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QLabel" name="label_39"> + <property name="text"> + <string>Firmware version</string> + </property> + <property name="buddy"> + <cstring>firmwareVersionLineEdit_3</cstring> + </property> + </widget> + </item> + <item row="0" column="4"> + <widget class="QLineEdit" name="firmwareVersionLineEdit_3"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_40"> + <property name="text"> + <string>Custom text</string> + </property> + <property name="buddy"> + <cstring>customTextLlineEdit_3</cstring> + </property> + </widget> + </item> + <item row="1" column="1" colspan="2"> + <widget class="QLineEdit" name="customTextLlineEdit_3"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maxLength"> + <number>23</number> + </property> + </widget> + </item> + <item row="5" column="4"> + <widget class="QDoubleSpinBox" name="salinityDoubleSpinBox_3"> + <property name="suffix"> + <string>kg/â„“</string> + </property> + <property name="minimum"> + <double>1.000000000000000</double> + </property> + <property name="maximum"> + <double>1.040000000000000</double> + </property> + <property name="singleStep"> + <double>0.010000000000000</double> + </property> + </widget> + </item> + <item row="7" column="2"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>177</height> + </size> + </property> + </spacer> + </item> + <item row="5" column="0" colspan="3"> + <widget class="QCheckBox" name="dateTimeSyncCheckBox_3"> + <property name="text"> + <string>Sync dive computer time with PC</string> + </property> + </widget> + </item> + <item row="6" column="0" colspan="3"> + <widget class="QCheckBox" name="safetyStopCheckBox_3"> + <property name="text"> + <string>Show safety stop</string> + </property> + </widget> + </item> + <item row="2" column="4"> + <widget class="QComboBox" name="dateFormatComboBox_3"> + <item> + <property name="text"> + <string>MM/DD/YY</string> + </property> + </item> + <item> + <property name="text"> + <string>DD/MM/YY</string> + </property> + </item> + <item> + <property name="text"> + <string>YY/MM/DD</string> + </property> + </item> + </widget> + </item> + <item row="1" column="3"> + <widget class="QLabel" name="label_41"> + <property name="text"> + <string>Number of dives</string> + </property> + </widget> + </item> + <item row="1" column="4"> + <widget class="QSpinBox" name="numberOfDivesSpinBox_3"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QSpinBox" name="samplingRateSpinBox_3"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>120</number> + </property> + <property name="value"> + <number>10</number> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_47"> + <property name="text"> + <string>Sampling rate</string> + </property> + <property name="buddy"> + <cstring>samplingRateComboBox</cstring> + </property> + </widget> + </item> + <item row="2" column="3"> + <widget class="QLabel" name="label_42"> + <property name="text"> + <string>Date format</string> + </property> + <property name="buddy"> + <cstring>dateFormatComboBox</cstring> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="advancedSettings_3"> + <attribute name="title"> + <string>Advanced settings</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_5"> + <item row="3" column="0" colspan="2"> + <widget class="QCheckBox" name="aGFSelectableCheckBox_3"> + <property name="text"> + <string>Alt GF can be selected underwater</string> + </property> + </widget> + </item> + <item row="10" column="3"> + <widget class="QLabel" name="label_53"> + <property name="text"> + <string>Desaturation</string> + </property> + <property name="buddy"> + <cstring>desaturationSpinBox</cstring> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_49"> + <property name="text"> + <string>Future TTS</string> + </property> + </widget> + </item> + <item row="10" column="4"> + <widget class="QSpinBox" name="desaturationSpinBox_3"> + <property name="suffix"> + <string>%</string> + </property> + <property name="minimum"> + <number>60</number> + </property> + <property name="maximum"> + <number>100</number> + </property> + <property name="value"> + <number>90</number> + </property> + </widget> + </item> + <item row="15" column="0"> + <spacer name="verticalSpacer_6"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="1"> + <widget class="QSpinBox" name="lastDecoSpinBox_3"> + <property name="suffix"> + <string> m</string> + </property> + <property name="minimum"> + <number>3</number> + </property> + <property name="maximum"> + <number>6</number> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label_54"> + <property name="text"> + <string>Decotype</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="decoTypeComboBox_3"> + <property name="currentIndex"> + <number>4</number> + </property> + <item> + <property name="text"> + <string>ZH-L16</string> + </property> + </item> + <item> + <property name="text"> + <string>Gauge</string> + </property> + </item> + <item> + <property name="text"> + <string>ZH-L16 CC</string> + </property> + </item> + <item> + <property name="text"> + <string>Apnoea</string> + </property> + </item> + <item> + <property name="text"> + <string>L16-GF OC</string> + </property> + </item> + <item> + <property name="text"> + <string>L16-GF CC</string> + </property> + </item> + <item> + <property name="text"> + <string>PSCR-GF</string> + </property> + </item> + </widget> + </item> + <item row="1" column="1"> + <widget class="QSpinBox" name="futureTTSSpinBox_3"> + <property name="suffix"> + <string> min</string> + </property> + <property name="maximum"> + <number>9</number> + </property> + </widget> + </item> + <item row="3" column="4"> + <widget class="QSpinBox" name="aGFLowSpinBox_3"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="suffix"> + <string>%</string> + </property> + <property name="minimum"> + <number>5</number> + </property> + <property name="maximum"> + <number>255</number> + </property> + <property name="value"> + <number>30</number> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_55"> + <property name="text"> + <string>Last deco</string> + </property> + <property name="buddy"> + <cstring>lastDecoSpinBox</cstring> + </property> + </widget> + </item> + <item row="9" column="4"> + <widget class="QSpinBox" name="saturationSpinBox_3"> + <property name="suffix"> + <string>%</string> + </property> + <property name="minimum"> + <number>100</number> + </property> + <property name="maximum"> + <number>140</number> + </property> + <property name="value"> + <number>110</number> + </property> + </widget> + </item> + <item row="3" column="3"> + <widget class="QLabel" name="label_57"> + <property name="text"> + <string>Alt GFLow</string> + </property> + </widget> + </item> + <item row="6" column="4"> + <widget class="QSpinBox" name="aGFHighSpinBox_3"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="suffix"> + <string>%</string> + </property> + <property name="minimum"> + <number>5</number> + </property> + <property name="maximum"> + <number>255</number> + </property> + <property name="value"> + <number>90</number> + </property> + </widget> + </item> + <item row="6" column="3"> + <widget class="QLabel" name="label_58"> + <property name="text"> + <string>Alt GFHigh</string> + </property> + </widget> + </item> + <item row="9" column="3"> + <widget class="QLabel" name="label_56"> + <property name="text"> + <string>Saturation</string> + </property> + <property name="buddy"> + <cstring>saturationSpinBox</cstring> + </property> + </widget> + </item> + <item row="2" column="3"> + <widget class="QLabel" name="label_52"> + <property name="text"> + <string>GFHigh</string> + </property> + </widget> + </item> + <item row="1" column="4"> + <widget class="QSpinBox" name="gfLowSpinBox_3"> + <property name="suffix"> + <string>%</string> + </property> + <property name="minimum"> + <number>10</number> + </property> + <property name="maximum"> + <number>100</number> + </property> + <property name="value"> + <number>30</number> + </property> + </widget> + </item> + <item row="1" column="3"> + <widget class="QLabel" name="label_51"> + <property name="text"> + <string>GFLow</string> + </property> + </widget> + </item> + <item row="2" column="4"> + <widget class="QSpinBox" name="gfHighSpinBox_3"> + <property name="suffix"> + <string>%</string> + </property> + <property name="minimum"> + <number>60</number> + </property> + <property name="maximum"> + <number>110</number> + </property> + <property name="value"> + <number>85</number> + </property> + </widget> + </item> + <item row="6" column="0" colspan="2"> + <widget class="QCheckBox" name="graphicalSpeedIndicator_3"> + <property name="text"> + <string>Graphical speed indicator</string> + </property> + </widget> + </item> + <item row="12" column="4"> + <widget class="QSpinBox" name="decoGasConsumption_3"> + <property name="suffix"> + <string> â„“/min</string> + </property> + <property name="minimum"> + <number>5</number> + </property> + <property name="maximum"> + <number>50</number> + </property> + <property name="value"> + <number>20</number> + </property> + </widget> + </item> + <item row="11" column="4"> + <widget class="QSpinBox" name="bottomGasConsumption_3"> + <property name="suffix"> + <string> â„“/min</string> + </property> + <property name="minimum"> + <number>5</number> + </property> + <property name="maximum"> + <number>50</number> + </property> + <property name="value"> + <number>20</number> + </property> + </widget> + </item> + <item row="11" column="3"> + <widget class="QLabel" name="label_50"> + <property name="text"> + <string>Bottom gas consumption</string> + </property> + </widget> + </item> + <item row="12" column="3"> + <widget class="QLabel" name="label_59"> + <property name="text"> + <string>Deco gas consumption</string> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="gasSettings_3"> + <attribute name="title"> + <string>Gas settings</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_6"> + <item row="0" column="0" colspan="2"> + <widget class="QTableWidget" name="ostcGasTable"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <column> + <property name="text"> + <string/> + </property> + </column> + <column> + <property name="text"> + <string>%Oâ‚‚</string> + </property> + </column> + <column> + <property name="text"> + <string>%He</string> + </property> + </column> + <column> + <property name="text"> + <string>Type</string> + </property> + </column> + <column> + <property name="text"> + <string>Change depth</string> + </property> + </column> + <item row="0" column="0"> + <property name="text"> + <string>Gas 1</string> + </property> + </item> + <item row="1" column="0"> + <property name="text"> + <string>Gas 2</string> + </property> + </item> + <item row="2" column="0"> + <property name="text"> + <string>Gas 3</string> + </property> + </item> + <item row="3" column="0"> + <property name="text"> + <string>Gas 4</string> + </property> + </item> + <item row="4" column="0"> + <property name="text"> + <string>Gas 5</string> + </property> + </item> + </widget> + </item> + <item row="0" column="2" colspan="2"> + <widget class="QTableWidget" name="ostcDilTable"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <column> + <property name="text"> + <string/> + </property> + </column> + <column> + <property name="text"> + <string>%Oâ‚‚</string> + </property> + </column> + <column> + <property name="text"> + <string>%He</string> + </property> + </column> + <column> + <property name="text"> + <string>Type</string> + </property> + </column> + <column> + <property name="text"> + <string>Change depth</string> + </property> + </column> + <item row="0" column="0"> + <property name="text"> + <string>Dil 1</string> + </property> + </item> + <item row="1" column="0"> + <property name="text"> + <string>Dil 2</string> + </property> + </item> + <item row="2" column="0"> + <property name="text"> + <string>Dil 3</string> + </property> + </item> + <item row="3" column="0"> + <property name="text"> + <string>Dil 4</string> + </property> + </item> + <item row="4" column="0"> + <property name="text"> + <string>Dil 5</string> + </property> + </item> + </widget> + </item> + <item row="2" column="0" rowspan="4" colspan="2"> + <widget class="QTableWidget" name="ostcSetPointTable"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <row> + <property name="text"> + <string/> + </property> + </row> + <column> + <property name="text"> + <string/> + </property> + </column> + <column> + <property name="text"> + <string>Setpoint</string> + </property> + </column> + <column> + <property name="text"> + <string>Change depth</string> + </property> + </column> + <item row="0" column="0"> + <property name="text"> + <string>SP 1</string> + </property> + </item> + <item row="1" column="0"> + <property name="text"> + <string>SP 2</string> + </property> + </item> + <item row="2" column="0"> + <property name="text"> + <string>SP 3</string> + </property> + </item> + </widget> + </item> + <item row="5" column="2"> + <spacer name="verticalSpacer_5"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item row="3" column="3"> + <widget class="QSpinBox" name="ppO2MaxSpinBox_3"> + <property name="suffix"> + <string> cbar</string> + </property> + <property name="minimum"> + <number>120</number> + </property> + <property name="maximum"> + <number>180</number> + </property> + <property name="value"> + <number>160</number> + </property> + </widget> + </item> + <item row="4" column="3"> + <widget class="QSpinBox" name="ppO2MinSpinBox_3"> + <property name="suffix"> + <string> cbar</string> + </property> + <property name="minimum"> + <number>16</number> + </property> + <property name="maximum"> + <number>21</number> + </property> + <property name="value"> + <number>19</number> + </property> + </widget> + </item> + <item row="3" column="2"> + <widget class="QLabel" name="label_60"> + <property name="text"> + <string>pOâ‚‚ max</string> + </property> + </widget> + </item> + <item row="4" column="2"> + <widget class="QLabel" name="label_61"> + <property name="text"> + <string>pOâ‚‚ min</string> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + </widget> + </widget> + </item> + <item> + <widget class="QProgressBar" name="progressBar"> + <property name="value"> + <number>0</number> + </property> + <property name="format"> + <string/> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>device</tabstop> + <tabstop>retrieveDetails</tabstop> + <tabstop>saveSettingsPushButton</tabstop> + <tabstop>backupButton</tabstop> + <tabstop>restoreBackupButton</tabstop> + <tabstop>cancel</tabstop> + </tabstops> + <resources> + <include location="../subsurface.qrc"/> + </resources> + <connections> + <connection> + <sender>DiveComputerList</sender> + <signal>currentRowChanged(int)</signal> + <receiver>dcStackedWidget</receiver> + <slot>setCurrentIndex(int)</slot> + <hints> + <hint type="sourcelabel"> + <x>20</x> + <y>20</y> + </hint> + <hint type="destinationlabel"> + <x>20</x> + <y>20</y> + </hint> + </hints> + </connection> + <connection> + <sender>lightCheckBox</sender> + <signal>toggled(bool)</signal> + <receiver>lightSpinBox</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>20</x> + <y>20</y> + </hint> + <hint type="destinationlabel"> + <x>20</x> + <y>20</y> + </hint> + </hints> + </connection> + <connection> + <sender>alarmDepthCheckBox</sender> + <signal>toggled(bool)</signal> + <receiver>alarmDepthDoubleSpinBox</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>20</x> + <y>20</y> + </hint> + <hint type="destinationlabel"> + <x>20</x> + <y>20</y> + </hint> + </hints> + </connection> + <connection> + <sender>alarmTimeCheckBox</sender> + <signal>toggled(bool)</signal> + <receiver>alarmTimeSpinBox</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>20</x> + <y>20</y> + </hint> + <hint type="destinationlabel"> + <x>20</x> + <y>20</y> + </hint> + </hints> + </connection> + <connection> + <sender>aGFSelectableCheckBox</sender> + <signal>toggled(bool)</signal> + <receiver>aGFHighSpinBox</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>340</x> + <y>229</y> + </hint> + <hint type="destinationlabel"> + <x>686</x> + <y>265</y> + </hint> + </hints> + </connection> + <connection> + <sender>aGFSelectableCheckBox</sender> + <signal>toggled(bool)</signal> + <receiver>aGFLowSpinBox</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>340</x> + <y>229</y> + </hint> + <hint type="destinationlabel"> + <x>686</x> + <y>229</y> + </hint> + </hints> + </connection> + <connection> + <sender>aGFSelectableCheckBox_3</sender> + <signal>toggled(bool)</signal> + <receiver>aGFHighSpinBox_3</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>340</x> + <y>229</y> + </hint> + <hint type="destinationlabel"> + <x>686</x> + <y>265</y> + </hint> + </hints> + </connection> + <connection> + <sender>aGFSelectableCheckBox_3</sender> + <signal>toggled(bool)</signal> + <receiver>aGFLowSpinBox_3</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>340</x> + <y>229</y> + </hint> + <hint type="destinationlabel"> + <x>686</x> + <y>229</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/desktop-widgets/css/tableviews.css b/desktop-widgets/css/tableviews.css new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/desktop-widgets/css/tableviews.css diff --git a/desktop-widgets/divecomponentselection.ui b/desktop-widgets/divecomponentselection.ui new file mode 100644 index 000000000..8262a5c2a --- /dev/null +++ b/desktop-widgets/divecomponentselection.ui @@ -0,0 +1,190 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>DiveComponentSelectionDialog</class> + <widget class="QDialog" name="DiveComponentSelectionDialog"> + <property name="windowModality"> + <enum>Qt::WindowModal</enum> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>401</width> + <height>317</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>Component selection</string> + </property> + <property name="windowIcon"> + <iconset> + <normalon>:/subsurface-icon</normalon> + </iconset> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="topMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>5</number> + </property> + <property name="bottomMargin"> + <number>5</number> + </property> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Which components would you like to copy</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <property name="spacing"> + <number>0</number> + </property> + <item row="0" column="0"> + <widget class="QCheckBox" name="divesite"> + <property name="text"> + <string>Dive site</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QCheckBox" name="suit"> + <property name="text"> + <string>Suit</string> + </property> + </widget> + </item> + <item row="5" column="0"> + <widget class="QCheckBox" name="visibility"> + <property name="text"> + <string>Visibility</string> + </property> + </widget> + </item> + <item row="6" column="0"> + <widget class="QCheckBox" name="notes"> + <property name="text"> + <string>Notes</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QCheckBox" name="tags"> + <property name="text"> + <string>Tags</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QCheckBox" name="weights"> + <property name="text"> + <string>Weights</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QCheckBox" name="cylinders"> + <property name="text"> + <string>Cylinders</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QCheckBox" name="divemaster"> + <property name="text"> + <string>Divemaster</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QCheckBox" name="buddy"> + <property name="text"> + <string>Buddy</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QCheckBox" name="rating"> + <property name="text"> + <string>Rating</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>DiveComponentSelectionDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>DiveComponentSelectionDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/desktop-widgets/divecomputermanagementdialog.cpp b/desktop-widgets/divecomputermanagementdialog.cpp new file mode 100644 index 000000000..fd9273ffb --- /dev/null +++ b/desktop-widgets/divecomputermanagementdialog.cpp @@ -0,0 +1,69 @@ +#include "divecomputermanagementdialog.h" +#include "mainwindow.h" +#include "helpers.h" +#include "divecomputermodel.h" +#include <QMessageBox> +#include <QShortcut> + +DiveComputerManagementDialog::DiveComputerManagementDialog(QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f), + model(0) +{ + ui.setupUi(this); + init(); + connect(ui.tableView, SIGNAL(clicked(QModelIndex)), this, SLOT(tryRemove(QModelIndex))); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); +} + +void DiveComputerManagementDialog::init() +{ + delete model; + model = new DiveComputerModel(dcList.dcMap); + ui.tableView->setModel(model); +} + +DiveComputerManagementDialog *DiveComputerManagementDialog::instance() +{ + static DiveComputerManagementDialog *self = new DiveComputerManagementDialog(MainWindow::instance()); + self->setAttribute(Qt::WA_QuitOnClose, false); + return self; +} + +void DiveComputerManagementDialog::update() +{ + model->update(); + ui.tableView->resizeColumnsToContents(); + ui.tableView->setColumnWidth(DiveComputerModel::REMOVE, 22); + layout()->activate(); +} + +void DiveComputerManagementDialog::tryRemove(const QModelIndex &index) +{ + if (index.column() != DiveComputerModel::REMOVE) + return; + + QMessageBox::StandardButton response = QMessageBox::question( + this, TITLE_OR_TEXT( + tr("Remove the selected dive computer?"), + tr("Are you sure that you want to \n remove the selected dive computer?")), + QMessageBox::Ok | QMessageBox::Cancel); + + if (response == QMessageBox::Ok) + model->remove(index); +} + +void DiveComputerManagementDialog::accept() +{ + model->keepWorkingList(); + hide(); + close(); +} + +void DiveComputerManagementDialog::reject() +{ + model->dropWorkingList(); + hide(); + close(); +} diff --git a/desktop-widgets/divecomputermanagementdialog.h b/desktop-widgets/divecomputermanagementdialog.h new file mode 100644 index 000000000..d065a0208 --- /dev/null +++ b/desktop-widgets/divecomputermanagementdialog.h @@ -0,0 +1,29 @@ +#ifndef DIVECOMPUTERMANAGEMENTDIALOG_H +#define DIVECOMPUTERMANAGEMENTDIALOG_H +#include <QDialog> +#include "ui_divecomputermanagementdialog.h" + +class QModelIndex; +class DiveComputerModel; + +class DiveComputerManagementDialog : public QDialog { + Q_OBJECT + +public: + static DiveComputerManagementDialog *instance(); + void update(); + void init(); + +public +slots: + void tryRemove(const QModelIndex &index); + void accept(); + void reject(); + +private: + explicit DiveComputerManagementDialog(QWidget *parent = 0, Qt::WindowFlags f = 0); + Ui::DiveComputerManagementDialog ui; + DiveComputerModel *model; +}; + +#endif // DIVECOMPUTERMANAGEMENTDIALOG_H diff --git a/desktop-widgets/divecomputermanagementdialog.ui b/desktop-widgets/divecomputermanagementdialog.ui new file mode 100644 index 000000000..0e5db2c41 --- /dev/null +++ b/desktop-widgets/divecomputermanagementdialog.ui @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>DiveComputerManagementDialog</class> + <widget class="QDialog" name="DiveComputerManagementDialog"> + <property name="windowModality"> + <enum>Qt::WindowModal</enum> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>560</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>Edit dive computer nicknames</string> + </property> + <property name="windowIcon"> + <iconset> + <normalon>:/subsurface-icon</normalon> + </iconset> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTableView" name="tableView"> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources> + <include location="../subsurface.qrc"/> + </resources> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>DiveComputerManagementDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>DiveComputerManagementDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/desktop-widgets/divelistview.cpp b/desktop-widgets/divelistview.cpp new file mode 100644 index 000000000..d2386ecf1 --- /dev/null +++ b/desktop-widgets/divelistview.cpp @@ -0,0 +1,1035 @@ +/* + * divelistview.cpp + * + * classes for the divelist of Subsurface + * + */ +#include "filtermodels.h" +#include "modeldelegates.h" +#include "mainwindow.h" +#include "divepicturewidget.h" +#include "display.h" +#include <unistd.h> +#include <QSettings> +#include <QKeyEvent> +#include <QFileDialog> +#include <QNetworkAccessManager> +#include <QNetworkReply> +#include <QStandardPaths> +#include <QMessageBox> +#include "qthelper.h" +#include "undocommands.h" +#include "divelistview.h" +#include "divepicturemodel.h" +#include "metrics.h" +#include "helpers.h" + +// # Date Rtg Dpth Dur Tmp Wght Suit Cyl Gas SAC OTU CNS Loc +static int defaultWidth[] = { 70, 140, 90, 50, 50, 50, 50, 70, 50, 50, 70, 50, 50, 500}; + +DiveListView::DiveListView(QWidget *parent) : QTreeView(parent), mouseClickSelection(false), sortColumn(0), + currentOrder(Qt::DescendingOrder), dontEmitDiveChangedSignal(false), selectionSaved(false) +{ + setItemDelegate(new DiveListDelegate(this)); + setUniformRowHeights(true); + setItemDelegateForColumn(DiveTripModel::RATING, new StarWidgetsDelegate(this)); + MultiFilterSortModel *model = MultiFilterSortModel::instance(); + model->setSortRole(DiveTripModel::SORT_ROLE); + model->setFilterKeyColumn(-1); // filter all columns + model->setFilterCaseSensitivity(Qt::CaseInsensitive); + setModel(model); + connect(model, SIGNAL(layoutChanged()), this, SLOT(fixMessyQtModelBehaviour())); + + setSortingEnabled(false); + setContextMenuPolicy(Qt::DefaultContextMenu); + setSelectionMode(ExtendedSelection); + header()->setContextMenuPolicy(Qt::ActionsContextMenu); + + const QFontMetrics metrics(defaultModelFont()); + int em = metrics.width('m'); + int zw = metrics.width('0'); + + // Fixes for the layout needed for mac +#ifdef Q_OS_MAC + int ht = metrics.height(); + header()->setMinimumHeight(ht + 4); +#endif + + // TODO FIXME we need this to get the header names + // can we find a smarter way? + DiveTripModel *tripModel = new DiveTripModel(this); + + // set the default width as a minimum between the hard-coded defaults, + // the header text width and the (assumed) content width, calculated + // based on type + for (int col = DiveTripModel::NR; col < DiveTripModel::COLUMNS; ++col) { + QString header_txt = tripModel->headerData(col, Qt::Horizontal, Qt::DisplayRole).toString(); + int width = metrics.width(header_txt); + int sw = 0; + switch (col) { + case DiveTripModel::NR: + case DiveTripModel::DURATION: + sw = 8*zw; + break; + case DiveTripModel::DATE: + sw = 14*em; + break; + case DiveTripModel::RATING: + sw = static_cast<StarWidgetsDelegate*>(itemDelegateForColumn(col))->starSize().width(); + break; + case DiveTripModel::SUIT: + case DiveTripModel::SAC: + sw = 7*em; + break; + case DiveTripModel::LOCATION: + sw = 50*em; + break; + default: + sw = 5*em; + } + if (sw > width) + width = sw; + width += zw; // small padding + if (width > defaultWidth[col]) + defaultWidth[col] = width; + } + delete tripModel; + + + header()->setStretchLastSection(true); + + installEventFilter(this); +} + +DiveListView::~DiveListView() +{ + QSettings settings; + settings.beginGroup("ListWidget"); + // don't set a width for the last column - location is supposed to be "the rest" + for (int i = DiveTripModel::NR; i < DiveTripModel::COLUMNS - 1; i++) { + if (isColumnHidden(i)) + continue; + // we used to hardcode them all to 100 - so that might still be in the settings + if (columnWidth(i) == 100 || columnWidth(i) == defaultWidth[i]) + settings.remove(QString("colwidth%1").arg(i)); + else + settings.setValue(QString("colwidth%1").arg(i), columnWidth(i)); + } + settings.remove(QString("colwidth%1").arg(DiveTripModel::COLUMNS - 1)); + settings.endGroup(); +} + +void DiveListView::setupUi() +{ + QSettings settings; + static bool firstRun = true; + if (firstRun) + backupExpandedRows(); + settings.beginGroup("ListWidget"); + /* if no width are set, use the calculated width for each column; + * for that to work we need to temporarily expand all rows */ + expandAll(); + for (int i = DiveTripModel::NR; i < DiveTripModel::COLUMNS; i++) { + if (isColumnHidden(i)) + continue; + QVariant width = settings.value(QString("colwidth%1").arg(i)); + if (width.isValid()) + setColumnWidth(i, width.toInt()); + else + setColumnWidth(i, defaultWidth[i]); + } + settings.endGroup(); + if (firstRun) + restoreExpandedRows(); + else + collapseAll(); + firstRun = false; + setColumnWidth(lastVisibleColumn(), 10); +} + +int DiveListView::lastVisibleColumn() +{ + int lastColumn = -1; + for (int i = DiveTripModel::NR; i < DiveTripModel::COLUMNS; i++) { + if (isColumnHidden(i)) + continue; + lastColumn = i; + } + return lastColumn; +} + +void DiveListView::backupExpandedRows() +{ + expandedRows.clear(); + for (int i = 0; i < model()->rowCount(); i++) + if (isExpanded(model()->index(i, 0))) + expandedRows.push_back(i); +} + +void DiveListView::restoreExpandedRows() +{ + setAnimated(false); + Q_FOREACH (const int &i, expandedRows) + setExpanded(model()->index(i, 0), true); + setAnimated(true); +} +void DiveListView::fixMessyQtModelBehaviour() +{ + QAbstractItemModel *m = model(); + for (int i = 0; i < model()->rowCount(); i++) + if (m->rowCount(m->index(i, 0)) != 0) + setFirstColumnSpanned(i, QModelIndex(), true); +} + +// this only remembers dives that were selected, not trips +void DiveListView::rememberSelection() +{ + selectedDives.clear(); + QItemSelection selection = selectionModel()->selection(); + Q_FOREACH (const QModelIndex &index, selection.indexes()) { + if (index.column() != 0) // We only care about the dives, so, let's stick to rows and discard columns. + continue; + struct dive *d = (struct dive *)index.data(DiveTripModel::DIVE_ROLE).value<void *>(); + if (d) { + selectedDives.insert(d->divetrip, get_divenr(d)); + } else { + struct dive_trip *t = (struct dive_trip *)index.data(DiveTripModel::TRIP_ROLE).value<void *>(); + if (t) + selectedDives.insert(t, -1); + } + } + selectionSaved = true; +} + +void DiveListView::restoreSelection() +{ + if (!selectionSaved) + return; + + selectionSaved = false; + dontEmitDiveChangedSignal = true; + unselectDives(); + dontEmitDiveChangedSignal = false; + Q_FOREACH (dive_trip_t *trip, selectedDives.keys()) { + QList<int> divesOnTrip = getDivesInTrip(trip); + QList<int> selectedDivesOnTrip = selectedDives.values(trip); + + // Only select trip if all of its dives were selected + if(selectedDivesOnTrip.contains(-1)) { + selectTrip(trip); + selectedDivesOnTrip.removeAll(-1); + } + selectDives(selectedDivesOnTrip); + } +} + +void DiveListView::selectTrip(dive_trip_t *trip) +{ + if (!trip) + return; + + QSortFilterProxyModel *m = qobject_cast<QSortFilterProxyModel *>(model()); + QModelIndexList match = m->match(m->index(0, 0), DiveTripModel::TRIP_ROLE, QVariant::fromValue<void *>(trip), 2, Qt::MatchRecursive); + QItemSelectionModel::SelectionFlags flags; + if (!match.count()) + return; + QModelIndex idx = match.first(); + flags = QItemSelectionModel::Select; + flags |= QItemSelectionModel::Rows; + selectionModel()->select(idx, flags); + expand(idx); +} + +// this is an odd one - when filtering the dive list the selection status of the trips +// is kept - but all other selections are lost. That's gets us into rather inconsistent state +// we call this function which clears the selection state of the trips as well, but does so +// without updating our internal "->selected" state. So once we called this function we can +// go back and select those dives that are still visible under the filter and everything +// works as expected +void DiveListView::clearTripSelection() +{ + // we want to make sure no trips are selected + disconnect(selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(selectionChanged(QItemSelection, QItemSelection))); + disconnect(selectionModel(), SIGNAL(currentChanged(QModelIndex, QModelIndex)), this, SLOT(currentChanged(QModelIndex, QModelIndex))); + + Q_FOREACH (const QModelIndex &index, selectionModel()->selectedRows()) { + dive_trip_t *trip = static_cast<dive_trip_t *>(index.data(DiveTripModel::TRIP_ROLE).value<void *>()); + if (!trip) + continue; + selectionModel()->select(index, QItemSelectionModel::Deselect); + } + + connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(selectionChanged(QItemSelection, QItemSelection))); + connect(selectionModel(), SIGNAL(currentChanged(QModelIndex, QModelIndex)), this, SLOT(currentChanged(QModelIndex, QModelIndex))); +} + +void DiveListView::unselectDives() +{ + // make sure we don't try to redraw the dives during the selection change + selected_dive = -1; + amount_selected = 0; + // clear the Qt selection + selectionModel()->clearSelection(); + // clearSelection should emit selectionChanged() but sometimes that + // appears not to happen + // since we are unselecting all dives there is no need to use deselect_dive() - that + // would only cause pointless churn + int i; + struct dive *dive; + for_each_dive (i, dive) { + dive->selected = false; + } +} + +QList<dive_trip_t *> DiveListView::selectedTrips() +{ + QList<dive_trip_t *> ret; + Q_FOREACH (const QModelIndex &index, selectionModel()->selectedRows()) { + dive_trip_t *trip = static_cast<dive_trip_t *>(index.data(DiveTripModel::TRIP_ROLE).value<void *>()); + if (!trip) + continue; + ret.push_back(trip); + } + return ret; +} + +void DiveListView::selectDive(int i, bool scrollto, bool toggle) +{ + if (i == -1) + return; + QSortFilterProxyModel *m = qobject_cast<QSortFilterProxyModel *>(model()); + QModelIndexList match = m->match(m->index(0, 0), DiveTripModel::DIVE_IDX, i, 2, Qt::MatchRecursive); + QItemSelectionModel::SelectionFlags flags; + if (match.isEmpty()) + return; + QModelIndex idx = match.first(); + flags = toggle ? QItemSelectionModel::Toggle : QItemSelectionModel::Select; + flags |= QItemSelectionModel::Rows; + selectionModel()->setCurrentIndex(idx, flags); + if (idx.parent().isValid()) { + setAnimated(false); + expand(idx.parent()); + if (scrollto) + scrollTo(idx.parent()); + setAnimated(true); + } + if (scrollto) + scrollTo(idx, PositionAtCenter); +} + +void DiveListView::selectDives(const QList<int> &newDiveSelection) +{ + int firstInList, newSelection; + struct dive *d; + + if (!newDiveSelection.count()) + return; + + dontEmitDiveChangedSignal = true; + // select the dives, highest index first - this way the oldest of the dives + // becomes the selected_dive that we scroll to + QList<int> sortedSelection = newDiveSelection; + qSort(sortedSelection.begin(), sortedSelection.end()); + newSelection = firstInList = sortedSelection.first(); + + while (!sortedSelection.isEmpty()) + selectDive(sortedSelection.takeLast()); + + while (selected_dive == -1) { + // that can happen if we restored a selection after edit + // and the only selected dive is no longer visible because of a filter + newSelection--; + if (newSelection < 0) + newSelection = dive_table.nr - 1; + if (newSelection == firstInList) + break; + if ((d = get_dive(newSelection)) != NULL && !d->hidden_by_filter) + selectDive(newSelection); + } + QSortFilterProxyModel *m = qobject_cast<QSortFilterProxyModel *>(model()); + QModelIndexList idxList = m->match(m->index(0, 0), DiveTripModel::DIVE_IDX, selected_dive, 2, Qt::MatchRecursive); + if (!idxList.isEmpty()) { + QModelIndex idx = idxList.first(); + if (idx.parent().isValid()) + scrollTo(idx.parent()); + scrollTo(idx); + } + // now that everything is up to date, update the widgets + Q_EMIT currentDiveChanged(selected_dive); + dontEmitDiveChangedSignal = false; + return; +} + +bool DiveListView::eventFilter(QObject *, QEvent *event) +{ + if (event->type() != QEvent::KeyPress) + return false; + QKeyEvent *keyEv = static_cast<QKeyEvent *>(event); + if (keyEv->key() == Qt::Key_Delete) { + contextMenuIndex = currentIndex(); + deleteDive(); + } + if (keyEv->key() != Qt::Key_Escape) + return false; + return true; +} + +// NOTE! This loses trip selection, because while we remember the +// dives, we don't remember the trips (see the "currentSelectedDives" +// list). I haven't figured out how to look up the trip from the +// index. TRIP_ROLE vs DIVE_ROLE? +void DiveListView::headerClicked(int i) +{ + DiveTripModel::Layout newLayout = i == (int)DiveTripModel::NR ? DiveTripModel::TREE : DiveTripModel::LIST; + rememberSelection(); + unselectDives(); + /* No layout change? Just re-sort, and scroll to first selection, making sure all selections are expanded */ + if (currentLayout == newLayout) { + currentOrder = (currentOrder == Qt::DescendingOrder) ? Qt::AscendingOrder : Qt::DescendingOrder; + sortByColumn(i, currentOrder); + } else { + // clear the model, repopulate with new indexes. + if (currentLayout == DiveTripModel::TREE) { + backupExpandedRows(); + } + reload(newLayout, false); + currentOrder = Qt::DescendingOrder; + sortByColumn(i, currentOrder); + if (newLayout == DiveTripModel::TREE) { + restoreExpandedRows(); + } + } + restoreSelection(); + // remember the new sort column + sortColumn = i; +} + +void DiveListView::reload(DiveTripModel::Layout layout, bool forceSort) +{ + // we want to run setupUi() once we actually are displaying something + // in the widget + static bool first = true; + if (first && dive_table.nr > 0) { + setupUi(); + first = false; + } + if (layout == DiveTripModel::CURRENT) + layout = currentLayout; + else + currentLayout = layout; + + header()->setSectionsClickable(true); + connect(header(), SIGNAL(sectionPressed(int)), this, SLOT(headerClicked(int)), Qt::UniqueConnection); + + QSortFilterProxyModel *m = qobject_cast<QSortFilterProxyModel *>(model()); + QAbstractItemModel *oldModel = m->sourceModel(); + if (oldModel) { + oldModel->deleteLater(); + } + DiveTripModel *tripModel = new DiveTripModel(this); + tripModel->setLayout(layout); + + m->setSourceModel(tripModel); + + if (!forceSort) + return; + + sortByColumn(sortColumn, currentOrder); + if (amount_selected && current_dive != NULL) { + selectDive(selected_dive, true); + } else { + QModelIndex firstDiveOrTrip = m->index(0, 0); + if (firstDiveOrTrip.isValid()) { + if (m->index(0, 0, firstDiveOrTrip).isValid()) + setCurrentIndex(m->index(0, 0, firstDiveOrTrip)); + else + setCurrentIndex(firstDiveOrTrip); + } + } + if (selectedIndexes().count()) { + QModelIndex curr = selectedIndexes().first(); + curr = curr.parent().isValid() ? curr.parent() : curr; + if (!isExpanded(curr)) { + setAnimated(false); + expand(curr); + scrollTo(curr); + setAnimated(true); + } + } + if (currentLayout == DiveTripModel::TREE) { + fixMessyQtModelBehaviour(); + } +} + +void DiveListView::reloadHeaderActions() +{ + // Populate the context menu of the headers that will show + // the menu to show / hide columns. + if (!header()->actions().size()) { + QSettings s; + s.beginGroup("DiveListColumnState"); + for (int i = 0; i < model()->columnCount(); i++) { + QString title = QString("%1").arg(model()->headerData(i, Qt::Horizontal).toString()); + QString settingName = QString("showColumn%1").arg(i); + QAction *a = new QAction(title, header()); + bool showHeaderFirstRun = !(i == DiveTripModel::MAXCNS || + i == DiveTripModel::GAS || + i == DiveTripModel::OTU || + i == DiveTripModel::TEMPERATURE || + i == DiveTripModel::TOTALWEIGHT || + i == DiveTripModel::SUIT || + i == DiveTripModel::CYLINDER || + i == DiveTripModel::SAC); + bool shown = s.value(settingName, showHeaderFirstRun).toBool(); + a->setCheckable(true); + a->setChecked(shown); + a->setProperty("index", i); + a->setProperty("settingName", settingName); + connect(a, SIGNAL(triggered(bool)), this, SLOT(toggleColumnVisibilityByIndex())); + header()->addAction(a); + setColumnHidden(i, !shown); + } + s.endGroup(); + } else { + for (int i = 0; i < model()->columnCount(); i++) { + QString title = QString("%1").arg(model()->headerData(i, Qt::Horizontal).toString()); + header()->actions()[i]->setText(title); + } + } +} + +void DiveListView::toggleColumnVisibilityByIndex() +{ + QAction *action = qobject_cast<QAction *>(sender()); + if (!action) + return; + + QSettings s; + s.beginGroup("DiveListColumnState"); + s.setValue(action->property("settingName").toString(), action->isChecked()); + s.endGroup(); + s.sync(); + setColumnHidden(action->property("index").toInt(), !action->isChecked()); + setColumnWidth(lastVisibleColumn(), 10); +} + +void DiveListView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) +{ + if (!isVisible()) + return; + if (!current.isValid()) + return; + scrollTo(current); +} + +void DiveListView::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) +{ + QItemSelection newSelected = selected.size() ? selected : selectionModel()->selection(); + QItemSelection newDeselected = deselected; + + disconnect(selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(selectionChanged(QItemSelection, QItemSelection))); + disconnect(selectionModel(), SIGNAL(currentChanged(QModelIndex, QModelIndex)), this, SLOT(currentChanged(QModelIndex, QModelIndex))); + + Q_FOREACH (const QModelIndex &index, newDeselected.indexes()) { + if (index.column() != 0) + continue; + const QAbstractItemModel *model = index.model(); + struct dive *dive = (struct dive *)model->data(index, DiveTripModel::DIVE_ROLE).value<void *>(); + if (!dive) // it's a trip! + deselect_dives_in_trip((dive_trip_t *)model->data(index, DiveTripModel::TRIP_ROLE).value<void *>()); + else + deselect_dive(get_divenr(dive)); + } + Q_FOREACH (const QModelIndex &index, newSelected.indexes()) { + if (index.column() != 0) + continue; + + const QAbstractItemModel *model = index.model(); + struct dive *dive = (struct dive *)model->data(index, DiveTripModel::DIVE_ROLE).value<void *>(); + if (!dive) { // it's a trip! + if (model->rowCount(index)) { + QItemSelection selection; + select_dives_in_trip((dive_trip_t *)model->data(index, DiveTripModel::TRIP_ROLE).value<void *>()); + selection.select(index.child(0, 0), index.child(model->rowCount(index) - 1, 0)); + selectionModel()->select(selection, QItemSelectionModel::Select | QItemSelectionModel::Rows); + selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select | QItemSelectionModel::NoUpdate); + if (!isExpanded(index)) + expand(index); + } + } else { + select_dive(get_divenr(dive)); + } + } + QTreeView::selectionChanged(selectionModel()->selection(), newDeselected); + connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(selectionChanged(QItemSelection, QItemSelection))); + connect(selectionModel(), SIGNAL(currentChanged(QModelIndex, QModelIndex)), this, SLOT(currentChanged(QModelIndex, QModelIndex))); + if (!dontEmitDiveChangedSignal) + Q_EMIT currentDiveChanged(selected_dive); +} + +enum asked_user {NOTYET, MERGE, DONTMERGE}; + +static bool can_merge(const struct dive *a, const struct dive *b, enum asked_user *have_asked) +{ + if (!a || !b) + return false; + if (a->when > b->when) + return false; + /* Don't merge dives if there's more than half an hour between them */ + if (dive_endtime(a) + 30 * 60 < b->when) { + if (*have_asked == NOTYET) { + if (QMessageBox::warning(MainWindow::instance(), + MainWindow::instance()->tr("Warning"), + MainWindow::instance()->tr("Trying to merge dives with %1min interval in between").arg( + (b->when - dive_endtime(a)) / 60), + QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Cancel) { + *have_asked = DONTMERGE; + return false; + } else { + *have_asked = MERGE; + return true; + } + } else { + return *have_asked == MERGE ? true : false; + } + } + return true; +} + +void DiveListView::mergeDives() +{ + int i; + struct dive *dive, *maindive = NULL; + enum asked_user have_asked = NOTYET; + + for_each_dive (i, dive) { + if (dive->selected) { + if (!can_merge(maindive, dive, &have_asked)) { + maindive = dive; + } else { + maindive = merge_two_dives(maindive, dive); + i--; // otherwise we skip a dive in the freshly changed list + } + } + } + MainWindow::instance()->refreshProfile(); + MainWindow::instance()->refreshDisplay(); +} + +void DiveListView::splitDives() +{ + int i; + struct dive *dive; + + for_each_dive (i, dive) { + if (dive->selected) + split_dive(dive); + } + MainWindow::instance()->refreshProfile(); + MainWindow::instance()->refreshDisplay(); +} + +void DiveListView::renumberDives() +{ + RenumberDialog::instance()->renumberOnlySelected(); + RenumberDialog::instance()->show(); +} + +void DiveListView::merge_trip(const QModelIndex &a, int offset) +{ + int i = a.row() + offset; + QModelIndex b = a.sibling(i, 0); + + dive_trip_t *trip_a = (dive_trip_t *)a.data(DiveTripModel::TRIP_ROLE).value<void *>(); + dive_trip_t *trip_b = (dive_trip_t *)b.data(DiveTripModel::TRIP_ROLE).value<void *>(); + if (trip_a == trip_b || !trip_a || !trip_b) + return; + combine_trips(trip_a, trip_b); + rememberSelection(); + reload(currentLayout, false); + fixMessyQtModelBehaviour(); + restoreSelection(); + mark_divelist_changed(true); + //TODO: emit a signal to signalize that the divelist changed? +} + +void DiveListView::mergeTripAbove() +{ + merge_trip(contextMenuIndex, -1); +} + +void DiveListView::mergeTripBelow() +{ + merge_trip(contextMenuIndex, +1); +} + +void DiveListView::removeFromTrip() +{ + //TODO: move this to C-code. + int i; + struct dive *d; + QMap<struct dive*, dive_trip*> divesToRemove; + for_each_dive (i, d) { + if (d->selected) + divesToRemove.insert(d, d->divetrip); + } + UndoRemoveDivesFromTrip *undoCommand = new UndoRemoveDivesFromTrip(divesToRemove); + MainWindow::instance()->undoStack->push(undoCommand); + + rememberSelection(); + reload(currentLayout, false); + fixMessyQtModelBehaviour(); + restoreSelection(); + mark_divelist_changed(true); +} + +void DiveListView::newTripAbove() +{ + struct dive *d = (struct dive *)contextMenuIndex.data(DiveTripModel::DIVE_ROLE).value<void *>(); + if (!d) // shouldn't happen as we only are setting up this action if this is a dive + return; + //TODO: port to c-code. + dive_trip_t *trip; + int idx; + rememberSelection(); + trip = create_and_hookup_trip_from_dive(d); + for_each_dive (idx, d) { + if (d->selected) + add_dive_to_trip(d, trip); + } + trip->expanded = 1; + reload(currentLayout, false); + fixMessyQtModelBehaviour(); + mark_divelist_changed(true); + restoreSelection(); +} + +void DiveListView::addToTripBelow() +{ + addToTrip(1); +} + +void DiveListView::addToTripAbove() +{ + addToTrip(-1); +} + +void DiveListView::addToTrip(int delta) +{ + // if there is a trip above / below, then it's a sibling at the same + // level as this dive. So let's take a look + struct dive *d = (struct dive *)contextMenuIndex.data(DiveTripModel::DIVE_ROLE).value<void *>(); + QModelIndex t = contextMenuIndex.sibling(contextMenuIndex.row() + delta, 0); + dive_trip_t *trip = (dive_trip_t *)t.data(DiveTripModel::TRIP_ROLE).value<void *>(); + + if (!trip || !d) + // no dive, no trip? get me out of here + return; + + rememberSelection(); + + add_dive_to_trip(d, trip); + if (d->selected) { // there are possibly other selected dives that we should add + int idx; + for_each_dive (idx, d) { + if (d->selected) + add_dive_to_trip(d, trip); + } + } + trip->expanded = 1; + mark_divelist_changed(true); + + reload(currentLayout, false); + restoreSelection(); + fixMessyQtModelBehaviour(); +} + +void DiveListView::markDiveInvalid() +{ + int i; + struct dive *d = (struct dive *)contextMenuIndex.data(DiveTripModel::DIVE_ROLE).value<void *>(); + if (!d) + return; + for_each_dive (i, d) { + if (!d->selected) + continue; + //TODO: this should be done in the future + // now mark the dive invalid... how do we do THAT? + // d->invalid = true; + } + if (amount_selected == 0) { + MainWindow::instance()->cleanUpEmpty(); + } + mark_divelist_changed(true); + MainWindow::instance()->refreshDisplay(); + if (prefs.display_invalid_dives == false) { + clearSelection(); + // select top dive that isn't marked invalid + rememberSelection(); + } + fixMessyQtModelBehaviour(); +} + +void DiveListView::deleteDive() +{ + struct dive *d = (struct dive *)contextMenuIndex.data(DiveTripModel::DIVE_ROLE).value<void *>(); + if (!d) + return; + + int i; + int lastDiveNr = -1; + QList<struct dive*> deletedDives; //a list of all deleted dives to be stored in the undo command + for_each_dive (i, d) { + if (!d->selected) + continue; + deletedDives.append(d); + lastDiveNr = i; + } + // the actual dive deletion is happening in the redo command that is implicitly triggered + UndoDeleteDive *undoEntry = new UndoDeleteDive(deletedDives); + MainWindow::instance()->undoStack->push(undoEntry); + if (amount_selected == 0) { + MainWindow::instance()->cleanUpEmpty(); + } + mark_divelist_changed(true); + MainWindow::instance()->refreshDisplay(); + if (lastDiveNr != -1) { + clearSelection(); + selectDive(lastDiveNr); + rememberSelection(); + } + fixMessyQtModelBehaviour(); +} + +void DiveListView::testSlot() +{ + struct dive *d = (struct dive *)contextMenuIndex.data(DiveTripModel::DIVE_ROLE).value<void *>(); + if (d) { + qDebug("testSlot called on dive #%d", d->number); + } else { + QModelIndex child = contextMenuIndex.child(0, 0); + d = (struct dive *)child.data(DiveTripModel::DIVE_ROLE).value<void *>(); + if (d) + qDebug("testSlot called on trip including dive #%d", d->number); + else + qDebug("testSlot called on trip with no dive"); + } +} + +void DiveListView::contextMenuEvent(QContextMenuEvent *event) +{ + QAction *collapseAction = NULL; + // let's remember where we are + contextMenuIndex = indexAt(event->pos()); + struct dive *d = (struct dive *)contextMenuIndex.data(DiveTripModel::DIVE_ROLE).value<void *>(); + dive_trip_t *trip = (dive_trip_t *)contextMenuIndex.data(DiveTripModel::TRIP_ROLE).value<void *>(); + QMenu popup(this); + if (currentLayout == DiveTripModel::TREE) { + // verify if there is a node that`s not expanded. + bool needs_expand = false; + bool needs_collapse = false; + uint expanded_nodes = 0; + for(int i = 0, end = model()->rowCount(); i < end; i++) { + QModelIndex idx = model()->index(i, 0); + if (idx.data(DiveTripModel::DIVE_ROLE).value<void *>()) + continue; + + if (!isExpanded(idx)) { + needs_expand = true; + } else { + needs_collapse = true; + expanded_nodes ++; + } + } + if (needs_expand) + popup.addAction(tr("Expand all"), this, SLOT(expandAll())); + if (needs_collapse) + popup.addAction(tr("Collapse all"), this, SLOT(collapseAll())); + + // verify if there`s a need for collapse others + if (expanded_nodes > 1) + collapseAction = popup.addAction(tr("Collapse others"), this, SLOT(collapseAll())); + + + if (d) { + popup.addAction(tr("Remove dive(s) from trip"), this, SLOT(removeFromTrip())); + popup.addAction(tr("Create new trip above"), this, SLOT(newTripAbove())); + if (!d->divetrip) { + struct dive *top = d; + struct dive *bottom = d; + if (d->selected) { + if (currentOrder == Qt::AscendingOrder) { + top = first_selected_dive(); + bottom = last_selected_dive(); + } else { + top = last_selected_dive(); + bottom = first_selected_dive(); + } + } + if (is_trip_before_after(top, (currentOrder == Qt::AscendingOrder))) + popup.addAction(tr("Add dive(s) to trip immediately above"), this, SLOT(addToTripAbove())); + if (is_trip_before_after(bottom, (currentOrder == Qt::DescendingOrder))) + popup.addAction(tr("Add dive(s) to trip immediately below"), this, SLOT(addToTripBelow())); + } + } + if (trip) { + popup.addAction(tr("Merge trip with trip above"), this, SLOT(mergeTripAbove())); + popup.addAction(tr("Merge trip with trip below"), this, SLOT(mergeTripBelow())); + } + } + if (d) { + popup.addAction(tr("Delete dive(s)"), this, SLOT(deleteDive())); +#if 0 + popup.addAction(tr("Mark dive(s) invalid", this, SLOT(markDiveInvalid()))); +#endif + } + if (amount_selected > 1 && consecutive_selected()) + popup.addAction(tr("Merge selected dives"), this, SLOT(mergeDives())); + if (amount_selected >= 1) { + popup.addAction(tr("Renumber dive(s)"), this, SLOT(renumberDives())); + popup.addAction(tr("Shift dive times"), this, SLOT(shiftTimes())); + popup.addAction(tr("Split selected dives"), this, SLOT(splitDives())); + popup.addAction(tr("Load image(s) from file(s)"), this, SLOT(loadImages())); + popup.addAction(tr("Load image(s) from web"), this, SLOT(loadWebImages())); + } + + // "collapse all" really closes all trips, + // "collapse" keeps the trip with the selected dive open + QAction *actionTaken = popup.exec(event->globalPos()); + if (actionTaken == collapseAction && collapseAction) { + this->setAnimated(false); + selectDive(selected_dive, true); + scrollTo(selectedIndexes().first()); + this->setAnimated(true); + } + event->accept(); +} + + +void DiveListView::shiftTimes() +{ + ShiftTimesDialog::instance()->show(); +} + +void DiveListView::loadImages() +{ + QStringList fileNames = QFileDialog::getOpenFileNames(this, tr("Open image files"), lastUsedImageDir(), tr("Image files (*.jpg *.jpeg *.pnm *.tif *.tiff)")); + if (fileNames.isEmpty()) + return; + updateLastUsedImageDir(QFileInfo(fileNames[0]).dir().path()); + matchImagesToDives(fileNames); +} + +void DiveListView::matchImagesToDives(QStringList fileNames) +{ + ShiftImageTimesDialog shiftDialog(this, fileNames); + shiftDialog.setOffset(lastImageTimeOffset()); + if (!shiftDialog.exec()) + return; + updateLastImageTimeOffset(shiftDialog.amount()); + + Q_FOREACH (const QString &fileName, fileNames) { + int j = 0; + struct dive *dive; + for_each_dive (j, dive) { + if (!dive->selected) + continue; + dive_create_picture(dive, copy_string(fileName.toUtf8().data()), shiftDialog.amount(), shiftDialog.matchAll()); + } + } + + mark_divelist_changed(true); + copy_dive(current_dive, &displayed_dive); + DivePictureModel::instance()->updateDivePictures(); +} + +void DiveListView::loadWebImages() +{ + URLDialog urlDialog(this); + if (!urlDialog.exec()) + return; + loadImageFromURL(QUrl::fromUserInput(urlDialog.url())); + +} + +void DiveListView::loadImageFromURL(QUrl url) +{ + if (url.isValid()) { + QEventLoop loop; + QNetworkRequest request(url); + QNetworkReply *reply = manager.get(request); + while (reply->isRunning()) { + loop.processEvents(); + sleep(1); + } + QByteArray imageData = reply->readAll(); + + QImage image = QImage(); + image.loadFromData(imageData); + if (image.isNull()) + // If this is not an image, maybe it's an html file and Miika can provide some xslr magic to extract images. + // In this case we would call the function recursively on the list of image source urls; + return; + + // Since we already downloaded the image we can cache it as well. + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(imageData); + QString path = QStandardPaths::standardLocations(QStandardPaths::CacheLocation).first(); + QDir dir(path); + if (!dir.exists()) + dir.mkpath(path); + QFile imageFile(path.append("/").append(hash.result().toHex())); + if (imageFile.open(QIODevice::WriteOnly)) { + QDataStream stream(&imageFile); + stream.writeRawData(imageData.data(), imageData.length()); + imageFile.waitForBytesWritten(-1); + imageFile.close(); + add_hash(imageFile.fileName(), hash.result()); + struct picture picture; + picture.hash = NULL; + picture.filename = strdup(url.toString().toUtf8().data()); + learnHash(&picture, hash.result()); + matchImagesToDives(QStringList(url.toString())); + } + } + + +} + + +QString DiveListView::lastUsedImageDir() +{ + QSettings settings; + QString lastImageDir = QDir::homePath(); + + settings.beginGroup("FileDialog"); + if (settings.contains("LastImageDir")) + if (QDir::setCurrent(settings.value("LastImageDir").toString())) + lastImageDir = settings.value("LastIamgeDir").toString(); + return lastImageDir; +} + +void DiveListView::updateLastUsedImageDir(const QString &dir) +{ + QSettings s; + s.beginGroup("FileDialog"); + s.setValue("LastImageDir", dir); +} + +int DiveListView::lastImageTimeOffset() +{ + QSettings settings; + int offset = 0; + + settings.beginGroup("MainWindow"); + if (settings.contains("LastImageTimeOffset")) + offset = settings.value("LastImageTimeOffset").toInt(); + return offset; +} + +void DiveListView::updateLastImageTimeOffset(const int offset) +{ + QSettings s; + s.beginGroup("MainWindow"); + s.setValue("LastImageTimeOffset", offset); +} diff --git a/desktop-widgets/divelistview.h b/desktop-widgets/divelistview.h new file mode 100644 index 000000000..aaec37af5 --- /dev/null +++ b/desktop-widgets/divelistview.h @@ -0,0 +1,89 @@ +/* + * divelistview.h + * + * header file for the dive list of Subsurface + * + */ +#ifndef DIVELISTVIEW_H +#define DIVELISTVIEW_H + +/*! A view subclass for use with dives + Note: calling this a list view might be misleading? +*/ + +#include <QTreeView> +#include <QLineEdit> +#include <QNetworkAccessManager> +#include "divetripmodel.h" + +class DiveListView : public QTreeView { + Q_OBJECT +public: + DiveListView(QWidget *parent = 0); + ~DiveListView(); + void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); + void currentChanged(const QModelIndex ¤t, const QModelIndex &previous); + void reload(DiveTripModel::Layout layout, bool forceSort = true); + bool eventFilter(QObject *, QEvent *); + void unselectDives(); + void clearTripSelection(); + void selectDive(int dive_table_idx, bool scrollto = false, bool toggle = false); + void selectDives(const QList<int> &newDiveSelection); + void rememberSelection(); + void restoreSelection(); + void contextMenuEvent(QContextMenuEvent *event); + QList<dive_trip_t *> selectedTrips(); +public +slots: + void toggleColumnVisibilityByIndex(); + void reloadHeaderActions(); + void headerClicked(int); + void removeFromTrip(); + void deleteDive(); + void markDiveInvalid(); + void testSlot(); + void fixMessyQtModelBehaviour(); + void mergeTripAbove(); + void mergeTripBelow(); + void newTripAbove(); + void addToTripAbove(); + void addToTripBelow(); + void mergeDives(); + void splitDives(); + void renumberDives(); + void shiftTimes(); + void loadImages(); + void loadWebImages(); + static QString lastUsedImageDir(); + +signals: + void currentDiveChanged(int divenr); + +private: + bool mouseClickSelection; + QList<int> expandedRows; + int sortColumn; + Qt::SortOrder currentOrder; + DiveTripModel::Layout currentLayout; + QModelIndex contextMenuIndex; + bool dontEmitDiveChangedSignal; + bool selectionSaved; + + /* if dive_trip_t is null, there's no problem. */ + QMultiHash<dive_trip_t *, int> selectedDives; + void merge_trip(const QModelIndex &a, const int offset); + void setupUi(); + void backupExpandedRows(); + void restoreExpandedRows(); + int lastVisibleColumn(); + void selectTrip(dive_trip_t *trip); + void updateLastUsedImageDir(const QString &s); + void updateLastImageTimeOffset(int offset); + int lastImageTimeOffset(); + void addToTrip(int delta); + void matchImagesToDives(QStringList fileNames); + void loadImageFromURL(QUrl url); + QNetworkAccessManager manager; +}; + +#endif // DIVELISTVIEW_H diff --git a/desktop-widgets/divelogexportdialog.cpp b/desktop-widgets/divelogexportdialog.cpp new file mode 100644 index 000000000..7a406b982 --- /dev/null +++ b/desktop-widgets/divelogexportdialog.cpp @@ -0,0 +1,240 @@ +#include <QFileDialog> +#include <QShortcut> +#include <QSettings> +#include <QtConcurrent> + +#include "divelogexportdialog.h" +#include "divelogexportlogic.h" +#include "diveshareexportdialog.h" +#include "ui_divelogexportdialog.h" +#include "subsurfacewebservices.h" +#include "worldmap-save.h" +#include "save-html.h" +#include "mainwindow.h" + +#define GET_UNIT(name, field, f, t) \ + v = settings.value(QString(name)); \ + if (v.isValid()) \ + field = (v.toInt() == 0) ? (t) : (f); \ + else \ + field = default_prefs.units.field + +DiveLogExportDialog::DiveLogExportDialog(QWidget *parent) : QDialog(parent), + ui(new Ui::DiveLogExportDialog) +{ + ui->setupUi(this); + showExplanation(); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), MainWindow::instance(), SLOT(close())); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + + /* the names are not the actual values exported to the json files,The font-family property should hold several + font names as a "fallback" system, to ensure maximum compatibility between browsers/operating systems */ + ui->fontSelection->addItem("Arial", "Arial, Helvetica, sans-serif"); + ui->fontSelection->addItem("Impact", "Impact, Charcoal, sans-serif"); + ui->fontSelection->addItem("Georgia", "Georgia, serif"); + ui->fontSelection->addItem("Courier", "Courier, monospace"); + ui->fontSelection->addItem("Verdana", "Verdana, Geneva, sans-serif"); + + QSettings settings; + settings.beginGroup("HTML"); + if (settings.contains("fontSelection")) { + ui->fontSelection->setCurrentIndex(settings.value("fontSelection").toInt()); + } + if (settings.contains("fontSizeSelection")) { + ui->fontSizeSelection->setCurrentIndex(settings.value("fontSizeSelection").toInt()); + } + if (settings.contains("themeSelection")) { + ui->themeSelection->setCurrentIndex(settings.value("themeSelection").toInt()); + } + if (settings.contains("subsurfaceNumbers")) { + ui->exportSubsurfaceNumber->setChecked(settings.value("subsurfaceNumbers").toBool()); + } + if (settings.contains("yearlyStatistics")) { + ui->exportStatistics->setChecked(settings.value("yearlyStatistics").toBool()); + } + if (settings.contains("listOnly")) { + ui->exportListOnly->setChecked(settings.value("listOnly").toBool()); + } + if (settings.contains("exportPhotos")) { + ui->exportPhotos->setChecked(settings.value("exportPhotos").toBool()); + } + settings.endGroup(); +} + +DiveLogExportDialog::~DiveLogExportDialog() +{ + delete ui; +} + +void DiveLogExportDialog::showExplanation() +{ + if (ui->exportUDDF->isChecked()) { + ui->description->setText(tr("Generic format that is used for data exchange between a variety of diving related programs.")); + } else if (ui->exportCSV->isChecked()) { + ui->description->setText(tr("Comma separated values describing the dive profile.")); + } else if (ui->exportCSVDetails->isChecked()) { + ui->description->setText(tr("Comma separated values of the dive information. This includes most of the dive details but no profile information.")); + } else if (ui->exportDivelogs->isChecked()) { + ui->description->setText(tr("Send the dive data to divelogs.de website.")); + } else if (ui->exportDiveshare->isChecked()) { + ui->description->setText(tr("Send the dive data to dive-share.appspot.com website")); + } else if (ui->exportWorldMap->isChecked()) { + ui->description->setText(tr("HTML export of the dive locations, visualized on a world map.")); + } else if (ui->exportSubsurfaceXML->isChecked()) { + ui->description->setText(tr("Subsurface native XML format.")); + } else if (ui->exportImageDepths->isChecked()) { + ui->description->setText(tr("Write depths of images to file.")); + } +} + +void DiveLogExportDialog::exportHtmlInit(const QString &filename) +{ + struct htmlExportSetting hes; + hes.themeFile = (ui->themeSelection->currentText() == tr("Light")) ? "light.css" : "sand.css"; + hes.exportPhotos = ui->exportPhotos->isChecked(); + hes.selectedOnly = ui->exportSelectedDives->isChecked(); + hes.listOnly = ui->exportListOnly->isChecked(); + hes.fontFamily = ui->fontSelection->itemData(ui->fontSelection->currentIndex()).toString(); + hes.fontSize = ui->fontSizeSelection->currentText(); + hes.themeSelection = ui->themeSelection->currentIndex(); + hes.subsurfaceNumbers = ui->exportSubsurfaceNumber->isChecked(); + hes.yearlyStatistics = ui->exportStatistics->isChecked(); + + exportHtmlInitLogic(filename, hes); +} + +void DiveLogExportDialog::exportHTMLsettings(const QString &filename) +{ + QSettings settings; + settings.beginGroup("HTML"); + settings.setValue("fontSelection", ui->fontSelection->currentIndex()); + settings.setValue("fontSizeSelection", ui->fontSizeSelection->currentIndex()); + settings.setValue("themeSelection", ui->themeSelection->currentIndex()); + settings.setValue("subsurfaceNumbers", ui->exportSubsurfaceNumber->isChecked()); + settings.setValue("yearlyStatistics", ui->exportStatistics->isChecked()); + settings.setValue("listOnly", ui->exportListOnly->isChecked()); + settings.setValue("exportPhotos", ui->exportPhotos->isChecked()); + settings.endGroup(); + +} + + +void DiveLogExportDialog::on_exportGroup_buttonClicked(QAbstractButton *button) +{ + showExplanation(); +} + +void DiveLogExportDialog::on_buttonBox_accepted() +{ + QString filename; + QString stylesheet; + QSettings settings; + QString lastDir = QDir::homePath(); + + settings.beginGroup("FileDialog"); + if (settings.contains("LastDir")) { + if (QDir::setCurrent(settings.value("LastDir").toString())) { + lastDir = settings.value("LastDir").toString(); + } + } + settings.endGroup(); + + switch (ui->tabWidget->currentIndex()) { + case 0: + if (ui->exportUDDF->isChecked()) { + stylesheet = "uddf-export.xslt"; + filename = QFileDialog::getSaveFileName(this, tr("Export UDDF file as"), lastDir, + tr("UDDF files (*.uddf *.UDDF)")); + } else if (ui->exportCSV->isChecked()) { + stylesheet = "xml2csv.xslt"; + filename = QFileDialog::getSaveFileName(this, tr("Export CSV file as"), lastDir, + tr("CSV files (*.csv *.CSV)")); + } else if (ui->exportCSVDetails->isChecked()) { + stylesheet = "xml2manualcsv.xslt"; + filename = QFileDialog::getSaveFileName(this, tr("Export CSV file as"), lastDir, + tr("CSV files (*.csv *.CSV)")); + } else if (ui->exportDivelogs->isChecked()) { + DivelogsDeWebServices::instance()->prepareDivesForUpload(ui->exportSelected->isChecked()); + } else if (ui->exportDiveshare->isChecked()) { + DiveShareExportDialog::instance()->prepareDivesForUpload(ui->exportSelected->isChecked()); + } else if (ui->exportWorldMap->isChecked()) { + filename = QFileDialog::getSaveFileName(this, tr("Export world map"), lastDir, + tr("HTML files (*.html)")); + if (!filename.isNull() && !filename.isEmpty()) + export_worldmap_HTML(filename.toUtf8().data(), ui->exportSelected->isChecked()); + } else if (ui->exportSubsurfaceXML->isChecked()) { + filename = QFileDialog::getSaveFileName(this, tr("Export Subsurface XML"), lastDir, + tr("XML files (*.xml *.ssrf)")); + if (!filename.isNull() && !filename.isEmpty()) { + if (!filename.contains('.')) + filename.append(".ssrf"); + QByteArray bt = QFile::encodeName(filename); + save_dives_logic(bt.data(), ui->exportSelected->isChecked()); + } + } else if (ui->exportImageDepths->isChecked()) { + filename = QFileDialog::getSaveFileName(this, tr("Save image depths"), lastDir); + if (!filename.isNull() && !filename.isEmpty()) + export_depths(filename.toUtf8().data(), ui->exportSelected->isChecked()); + } + break; + case 1: + filename = QFileDialog::getSaveFileName(this, tr("Export HTML files as"), lastDir, + tr("HTML files (*.html)")); + if (!filename.isNull() && !filename.isEmpty()) + exportHtmlInit(filename); + break; + } + + if (!filename.isNull() && !filename.isEmpty()) { + // remember the last export path + QFileInfo fileInfo(filename); + settings.beginGroup("FileDialog"); + settings.setValue("LastDir", fileInfo.dir().path()); + settings.endGroup(); + // the non XSLT exports are called directly above, the XSLT based ons are called here + if (!stylesheet.isEmpty()) { + future = QtConcurrent::run(export_dives_xslt, filename.toUtf8(), ui->exportSelected->isChecked(), ui->CSVUnits_2->currentIndex(), stylesheet.toUtf8()); + MainWindow::instance()->getNotificationWidget()->showNotification(tr("Please wait, exporting..."), KMessageWidget::Information); + MainWindow::instance()->getNotificationWidget()->setFuture(future); + } + } +} + +void DiveLogExportDialog::export_depths(const char *filename, const bool selected_only) +{ + FILE *f; + struct dive *dive; + depth_t depth; + int i; + const char *unit = NULL; + + struct membuffer buf = { 0 }; + + for_each_dive (i, dive) { + if (selected_only && !dive->selected) + continue; + + FOR_EACH_PICTURE (dive) { + int n = dive->dc.samples; + struct sample *s = dive->dc.sample; + depth.mm = 0; + while (--n >= 0 && (int32_t)s->time.seconds <= picture->offset.seconds) { + depth.mm = s->depth.mm; + s++; + } + put_format(&buf, "%s\t%.1f", picture->filename, get_depth_units(depth.mm, NULL, &unit)); + put_format(&buf, "%s\n", unit); + } + } + + f = subsurface_fopen(filename, "w+"); + if (!f) { + report_error(tr("Can't open file %s").toUtf8().data(), filename); + } else { + flush_buffer(&buf, f); /*check for writing errors? */ + fclose(f); + } + free_buffer(&buf); +} diff --git a/desktop-widgets/divelogexportdialog.h b/desktop-widgets/divelogexportdialog.h new file mode 100644 index 000000000..a5b5cc770 --- /dev/null +++ b/desktop-widgets/divelogexportdialog.h @@ -0,0 +1,39 @@ +#ifndef DIVELOGEXPORTDIALOG_H +#define DIVELOGEXPORTDIALOG_H + +#include <QDialog> +#include <QTextStream> +#include <QFuture> +#include "helpers.h" +#include "statistics.h" + +class QAbstractButton; + +namespace Ui { + class DiveLogExportDialog; +} + +void exportHTMLstatisticsTotal(QTextStream &out, stats_t *total_stats); + +class DiveLogExportDialog : public QDialog { + Q_OBJECT + +public: + explicit DiveLogExportDialog(QWidget *parent = 0); + ~DiveLogExportDialog(); + +private +slots: + void on_buttonBox_accepted(); + void on_exportGroup_buttonClicked(QAbstractButton *); + +private: + QFuture<int> future; + Ui::DiveLogExportDialog *ui; + void showExplanation(); + void exportHtmlInit(const QString &filename); + void exportHTMLsettings(const QString &filename); + void export_depths(const char *filename, const bool selected_only); +}; + +#endif // DIVELOGEXPORTDIALOG_H diff --git a/desktop-widgets/divelogexportdialog.ui b/desktop-widgets/divelogexportdialog.ui new file mode 100644 index 000000000..02c8cf38b --- /dev/null +++ b/desktop-widgets/divelogexportdialog.ui @@ -0,0 +1,606 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>DiveLogExportDialog</class> + <widget class="QDialog" name="DiveLogExportDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>507</width> + <height>548</height> + </rect> + </property> + <property name="windowTitle"> + <string>Export dive log files</string> + </property> + <property name="windowIcon"> + <iconset> + <normalon>:/subsurface-icon</normalon> + </iconset> + </property> + <layout class="QGridLayout" name="gridLayout"> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="topMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>5</number> + </property> + <property name="bottomMargin"> + <number>5</number> + </property> + <item row="0" column="0"> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <property name="documentMode"> + <bool>true</bool> + </property> + <widget class="QWidget" name="General_tab"> + <attribute name="title"> + <string>General export</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_2"> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="topMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>5</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item row="2" column="0" colspan="2"> + <widget class="QLabel" name="description"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>100</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>100</height> + </size> + </property> + <property name="text"> + <string/> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="3" column="0"> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item row="0" column="0"> + <widget class="QGroupBox" name="exportFormat"> + <property name="title"> + <string>Export format</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QRadioButton" name="exportSubsurfaceXML"> + <property name="maximumSize"> + <size> + <width>171</width> + <height>16777215</height> + </size> + </property> + <property name="text"> + <string>Subsurface &XML</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + <attribute name="buttonGroup"> + <string notr="true">exportGroup</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="exportUDDF"> + <property name="maximumSize"> + <size> + <width>110</width> + <height>16777215</height> + </size> + </property> + <property name="text"> + <string>UDDF</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <attribute name="buttonGroup"> + <string notr="true">exportGroup</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="exportDivelogs"> + <property name="text"> + <string>di&velogs.de</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">exportGroup</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="exportDiveshare"> + <property name="text"> + <string>DiveShare</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">exportGroup</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="exportCSV"> + <property name="text"> + <string>CSV dive profile</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">exportGroup</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="exportCSVDetails"> + <property name="text"> + <string>CSV dive details</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">exportGroup</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="exportWorldMap"> + <property name="text"> + <string>Worldmap</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">exportGroup</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="exportImageDepths"> + <property name="text"> + <string>I&mage depths</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">exportGroup</string> + </attribute> + </widget> + </item> + </layout> + </widget> + </item> + <item row="0" column="1"> + <widget class="QWidget" name="widget" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QGroupBox" name="exportSelection"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>100</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>16777215</height> + </size> + </property> + <property name="title"> + <string>Selection</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QRadioButton" name="exportSelected"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Selected dives</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="exportAll"> + <property name="text"> + <string>All dives</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="title"> + <string>CSV units</string> + </property> + <widget class="QComboBox" name="CSVUnits_2"> + <property name="geometry"> + <rect> + <x>30</x> + <y>30</y> + <width>102</width> + <height>27</height> + </rect> + </property> + <item> + <property name="text"> + <string>Metric</string> + </property> + </item> + <item> + <property name="text"> + <string>Imperial</string> + </property> + </item> + </widget> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="HTML_tab"> + <attribute name="title"> + <string>HTML</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QGroupBox" name="GeneralOptions"> + <property name="title"> + <string>General settings</string> + </property> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="0" column="0"> + <widget class="QCheckBox" name="exportSubsurfaceNumber"> + <property name="text"> + <string>Subsurface numbers</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QRadioButton" name="exportSelectedDives"> + <property name="minimumSize"> + <size> + <width>117</width> + <height>0</height> + </size> + </property> + <property name="text"> + <string>Selected dives</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup</string> + </attribute> + </widget> + </item> + <item row="1" column="0"> + <widget class="QCheckBox" name="exportStatistics"> + <property name="text"> + <string>Export yearly statistics</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QRadioButton" name="exportAllDives"> + <property name="minimumSize"> + <size> + <width>117</width> + <height>0</height> + </size> + </property> + <property name="text"> + <string>All di&ves</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup</string> + </attribute> + </widget> + </item> + <item row="3" column="0"> + <widget class="QCheckBox" name="exportListOnly"> + <property name="text"> + <string>Export list only</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QCheckBox" name="exportPhotos"> + <property name="text"> + <string>Export photos</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="advanceOptions"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="title"> + <string>Style options</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <layout class="QFormLayout" name="formLayout"> + <property name="fieldGrowthPolicy"> + <enum>QFormLayout::AllNonFixedFieldsGrow</enum> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="fontLabel"> + <property name="text"> + <string>Font</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="fontSelection"/> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="fontSizeLabel"> + <property name="text"> + <string>Font size</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QComboBox" name="fontSizeSelection"> + <property name="currentIndex"> + <number>3</number> + </property> + <item> + <property name="text"> + <string>8</string> + </property> + </item> + <item> + <property name="text"> + <string>10</string> + </property> + </item> + <item> + <property name="text"> + <string>12</string> + </property> + </item> + <item> + <property name="text"> + <string>14</string> + </property> + </item> + <item> + <property name="text"> + <string>16</string> + </property> + </item> + <item> + <property name="text"> + <string>18</string> + </property> + </item> + <item> + <property name="text"> + <string>20</string> + </property> + </item> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="themeLabel"> + <property name="text"> + <string>Theme</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QComboBox" name="themeSelection"> + <property name="currentIndex"> + <number>0</number> + </property> + <item> + <property name="text"> + <string>Light</string> + </property> + </item> + <item> + <property name="text"> + <string>Sand</string> + </property> + </item> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + <item row="1" column="0"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>DiveLogExportDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>DiveLogExportDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>exportCSV</sender> + <signal>toggled(bool)</signal> + <receiver>groupBox</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>20</x> + <y>20</y> + </hint> + <hint type="destinationlabel"> + <x>20</x> + <y>20</y> + </hint> + </hints> + </connection> + <connection> + <sender>exportCSVDetails</sender> + <signal>toggled(bool)</signal> + <receiver>groupBox</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>20</x> + <y>20</y> + </hint> + <hint type="destinationlabel"> + <x>20</x> + <y>20</y> + </hint> + </hints> + </connection> + </connections> + <buttongroups> + <buttongroup name="buttonGroup"/> + <buttongroup name="exportGroup"/> + </buttongroups> +</ui> diff --git a/desktop-widgets/divelogimportdialog.cpp b/desktop-widgets/divelogimportdialog.cpp new file mode 100644 index 000000000..025d181d1 --- /dev/null +++ b/desktop-widgets/divelogimportdialog.cpp @@ -0,0 +1,861 @@ +#include "divelogimportdialog.h" +#include "mainwindow.h" +#include "color.h" +#include "ui_divelogimportdialog.h" +#include <QShortcut> +#include <QDrag> +#include <QMimeData> + +static QString subsurface_mimedata = "subsurface/csvcolumns"; +static QString subsurface_index = "subsurface/csvindex"; + +const DiveLogImportDialog::CSVAppConfig DiveLogImportDialog::CSVApps[CSVAPPS] = { + // time, depth, temperature, po2, sensor1, sensor2, sensor3, cns, ndl, tts, stopdepth, pressure, setpoint + // indices are 0 based, -1 means the column doesn't exist + { "Manual import", }, + { "APD Log Viewer - DC1", 0, 1, 15, 6, 3, 4, 5, 17, -1, -1, 18, -1, 2, "Tab" }, + { "APD Log Viewer - DC2", 0, 1, 15, 6, 7, 8, 9, 17, -1, -1, 18, -1, 2, "Tab" }, + { "XP5", 0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, "Tab" }, + { "SensusCSV", 9, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, "," }, + { "Seabear CSV", 0, 1, 5, -1, -1, -1, -1, -1, 2, 3, 4, 6, -1, ";" }, + { "SubsurfaceCSV", -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, "Tab" }, + { NULL, } +}; + +static enum { + MANUAL, + APD, + APD2, + XP5, + SENSUS, + SEABEAR, + SUBSURFACE +} known; + +ColumnNameProvider::ColumnNameProvider(QObject *parent) : QAbstractListModel(parent) +{ + columnNames << tr("Dive #") << tr("Date") << tr("Time") << tr("Duration") << tr("Location") << tr("GPS") << tr("Weight") << tr("Cyl. size") << tr("Start pressure") << + tr("End pressure") << tr("Max. depth") << tr("Avg. depth") << tr("Divemaster") << tr("Buddy") << tr("Suit") << tr("Notes") << tr("Tags") << tr("Air temp.") << tr("Water temp.") << + tr("Oâ‚‚") << tr("He") << tr("Sample time") << tr("Sample depth") << tr("Sample temperature") << tr("Sample pOâ‚‚") << tr("Sample CNS") << tr("Sample NDL") << + tr("Sample TTS") << tr("Sample stopdepth") << tr("Sample pressure") << + tr("Sample sensor1 pOâ‚‚") << tr("Sample sensor2 pOâ‚‚") << tr("Sample sensor3 pOâ‚‚") << + tr("Sample setpoint"); +} + +bool ColumnNameProvider::insertRows(int row, int count, const QModelIndex &parent) +{ + beginInsertRows(QModelIndex(), row, row); + columnNames.append(QString()); + endInsertRows(); + return true; +} + +bool ColumnNameProvider::removeRows(int row, int count, const QModelIndex &parent) +{ + beginRemoveRows(QModelIndex(), row, row); + columnNames.removeAt(row); + endRemoveRows(); + return true; +} + +bool ColumnNameProvider::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (role == Qt::EditRole) { + columnNames[index.row()] = value.toString(); + } + dataChanged(index, index); + return true; +} + +QVariant ColumnNameProvider::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + if (role != Qt::DisplayRole) + return QVariant(); + + return QVariant(columnNames[index.row()]); +} + +int ColumnNameProvider::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return columnNames.count(); +} + +int ColumnNameProvider::mymatch(QString value) const +{ + QString searchString = value.toLower(); + searchString.replace("\"", "").replace(" ", "").replace(".", "").replace("\n",""); + for (int i = 0; i < columnNames.count(); i++) { + QString name = columnNames.at(i).toLower(); + name.replace("\"", "").replace(" ", "").replace(".", "").replace("\n",""); + if (searchString == name.toLower()) + return i; + } + return -1; +} + + + +ColumnNameView::ColumnNameView(QWidget *parent) +{ + setAcceptDrops(true); + setDragEnabled(true); +} + +void ColumnNameView::mousePressEvent(QMouseEvent *press) +{ + QModelIndex atClick = indexAt(press->pos()); + if (!atClick.isValid()) + return; + + QRect indexRect = visualRect(atClick); + QPixmap pix(indexRect.width(), indexRect.height()); + pix.fill(QColor(0,0,0,0)); + render(&pix, QPoint(0, 0),QRegion(indexRect)); + + QDrag *drag = new QDrag(this); + QMimeData *mimeData = new QMimeData; + mimeData->setData(subsurface_mimedata, atClick.data().toByteArray()); + model()->removeRow(atClick.row()); + drag->setPixmap(pix); + drag->setMimeData(mimeData); + if (drag->exec() == Qt::IgnoreAction){ + model()->insertRow(model()->rowCount()); + QModelIndex idx = model()->index(model()->rowCount()-1, 0); + model()->setData(idx, mimeData->data(subsurface_mimedata)); + } +} + +void ColumnNameView::dragLeaveEvent(QDragLeaveEvent *leave) +{ + Q_UNUSED(leave); +} + +void ColumnNameView::dragEnterEvent(QDragEnterEvent *event) +{ + event->acceptProposedAction(); +} + +void ColumnNameView::dragMoveEvent(QDragMoveEvent *event) +{ + QModelIndex curr = indexAt(event->pos()); + if (!curr.isValid() || curr.row() != 0) + return; + event->acceptProposedAction(); +} + +void ColumnNameView::dropEvent(QDropEvent *event) +{ + const QMimeData *mimeData = event->mimeData(); + if (mimeData->data(subsurface_mimedata).count()) { + if (event->source() != this) { + event->acceptProposedAction(); + QVariant value = QString(mimeData->data(subsurface_mimedata)); + model()->insertRow(model()->rowCount()); + model()->setData(model()->index(model()->rowCount()-1, 0), value); + } + } +} + +ColumnDropCSVView::ColumnDropCSVView(QWidget *parent) +{ + setAcceptDrops(true); +} + +void ColumnDropCSVView::dragLeaveEvent(QDragLeaveEvent *leave) +{ + Q_UNUSED(leave); +} + +void ColumnDropCSVView::dragEnterEvent(QDragEnterEvent *event) +{ + event->acceptProposedAction(); +} + +void ColumnDropCSVView::dragMoveEvent(QDragMoveEvent *event) +{ + QModelIndex curr = indexAt(event->pos()); + if (!curr.isValid() || curr.row() != 0) + return; + event->acceptProposedAction(); +} + +void ColumnDropCSVView::dropEvent(QDropEvent *event) +{ + QModelIndex curr = indexAt(event->pos()); + if (!curr.isValid() || curr.row() != 0) + return; + + const QMimeData *mimeData = event->mimeData(); + if (!mimeData->data(subsurface_mimedata).count()) + return; + + if (event->source() == this ) { + int value_old = mimeData->data(subsurface_index).toInt(); + int value_new = curr.column(); + ColumnNameResult *m = qobject_cast<ColumnNameResult*>(model()); + m->swapValues(value_old, value_new); + event->acceptProposedAction(); + return; + } + + if (curr.data().toString().isEmpty()) { + QVariant value = QString(mimeData->data(subsurface_mimedata)); + model()->setData(curr, value); + event->acceptProposedAction(); + } +} + +ColumnNameResult::ColumnNameResult(QObject *parent) : QAbstractTableModel(parent) +{ + +} + +void ColumnNameResult::swapValues(int firstIndex, int secondIndex) { + QString one = columnNames[firstIndex]; + QString two = columnNames[secondIndex]; + setData(index(0, firstIndex), QVariant(two), Qt::EditRole); + setData(index(0, secondIndex), QVariant(one), Qt::EditRole); +} + +bool ColumnNameResult::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid() || index.row() != 0) { + return false; + } + if (role == Qt::EditRole) { + columnNames[index.column()] = value.toString(); + dataChanged(index, index); + } + return true; +} + +QVariant ColumnNameResult::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + if (role == Qt::BackgroundColorRole) + if (index.row() == 0) + return QVariant(AIR_BLUE_TRANS); + + if (role != Qt::DisplayRole) + return QVariant(); + + if (index.row() == 0) { + return (columnNames[index.column()]); + } + // make sure the element exists before returning it - this might get called before the + // model is correctly set up again (e.g., when changing separators) + if (columnValues.count() > index.row() - 1 && columnValues[index.row() - 1].count() > index.column()) + return QVariant(columnValues[index.row() - 1][index.column()]); + else + return QVariant(); +} + +int ColumnNameResult::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return columnValues.count() + 1; // +1 == the header. +} + +int ColumnNameResult::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return columnNames.count(); +} + +QStringList ColumnNameResult::result() const +{ + return columnNames; +} + +void ColumnNameResult::setColumnValues(QList<QStringList> columns) +{ + if (rowCount() != 1) { + beginRemoveRows(QModelIndex(), 1, rowCount()-1); + columnValues.clear(); + endRemoveRows(); + } + if (columnCount() != 0) { + beginRemoveColumns(QModelIndex(), 0, columnCount()-1); + columnNames.clear(); + endRemoveColumns(); + } + + QStringList first = columns.first(); + beginInsertColumns(QModelIndex(), 0, first.count()-1); + for(int i = 0; i < first.count(); i++) + columnNames.append(QString()); + + endInsertColumns(); + + beginInsertRows(QModelIndex(), 0, columns.count()-1); + columnValues = columns; + endInsertRows(); +} + +void ColumnDropCSVView::mousePressEvent(QMouseEvent *press) +{ + QModelIndex atClick = indexAt(press->pos()); + if (!atClick.isValid() || atClick.row()) + return; + + QRect indexRect = visualRect(atClick); + QPixmap pix(indexRect.width(), indexRect.height()); + pix.fill(QColor(0,0,0,0)); + render(&pix, QPoint(0, 0),QRegion(indexRect)); + + QDrag *drag = new QDrag(this); + QMimeData *mimeData = new QMimeData; + mimeData->setData(subsurface_mimedata, atClick.data().toByteArray()); + mimeData->setData(subsurface_index, QString::number(atClick.column()).toLocal8Bit()); + drag->setPixmap(pix); + drag->setMimeData(mimeData); + if (drag->exec() != Qt::IgnoreAction){ + QObject *target = drag->target(); + if (target->objectName() == "qt_scrollarea_viewport") + target = target->parent(); + if (target != drag->source()) + model()->setData(atClick, QString()); + } +} + +DiveLogImportDialog::DiveLogImportDialog(QStringList fn, QWidget *parent) : QDialog(parent), + selector(true), + ui(new Ui::DiveLogImportDialog) +{ + ui->setupUi(this); + fileNames = fn; + column = 0; + delta = "0"; + hw = ""; + + /* Add indexes of XSLTs requiring special handling to the list */ + specialCSV << SENSUS; + specialCSV << SUBSURFACE; + + for (int i = 0; !CSVApps[i].name.isNull(); ++i) + ui->knownImports->addItem(CSVApps[i].name); + + ui->CSVSeparator->addItems( QStringList() << tr("Tab") << "," << ";"); + + loadFileContents(-1, INITIAL); + + /* manually import CSV file */ + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); + + connect(ui->CSVSeparator, SIGNAL(currentIndexChanged(int)), this, SLOT(loadFileContentsSeperatorSelected(int))); + connect(ui->knownImports, SIGNAL(currentIndexChanged(int)), this, SLOT(loadFileContentsKnownTypesSelected(int))); +} + +DiveLogImportDialog::~DiveLogImportDialog() +{ + delete ui; +} + +void DiveLogImportDialog::loadFileContentsSeperatorSelected(int value) +{ + loadFileContents(value, SEPARATOR); +} + +void DiveLogImportDialog::loadFileContentsKnownTypesSelected(int value) +{ + loadFileContents(value, KNOWNTYPES); +} + +void DiveLogImportDialog::loadFileContents(int value, whatChanged triggeredBy) +{ + QFile f(fileNames.first()); + QList<QStringList> fileColumns; + QStringList currColumns; + QStringList headers; + bool matchedSome = false; + bool seabear = false; + bool xp5 = false; + bool apd = false; + + // reset everything + ColumnNameProvider *provider = new ColumnNameProvider(this); + ui->avaliableColumns->setModel(provider); + ui->avaliableColumns->setItemDelegate(new TagDragDelegate(ui->avaliableColumns)); + resultModel = new ColumnNameResult(this); + ui->tableView->setModel(resultModel); + + f.open(QFile::ReadOnly); + QString firstLine = f.readLine(); + if (firstLine.contains("SEABEAR")) { + seabear = true; + + /* + * Parse header - currently only interested in sample + * interval and hardware version. If we have old format + * the interval value is missing from the header. + */ + + while ((firstLine = f.readLine().trimmed()).length() > 0 && !f.atEnd()) { + if (firstLine.contains("//Hardware Version: ")) { + hw = firstLine.replace(QString::fromLatin1("//Hardware Version: "), QString::fromLatin1("\"Seabear ")).trimmed().append("\""); + break; + } + } + + /* + * Note that we scan over the "Log interval" on purpose + */ + + while ((firstLine = f.readLine().trimmed()).length() > 0 && !f.atEnd()) { + if (firstLine.contains("//Log interval: ")) + delta = firstLine.remove(QString::fromLatin1("//Log interval: ")).trimmed().remove(QString::fromLatin1(" s")); + } + + /* + * Parse CSV fields + */ + + firstLine = f.readLine().trimmed(); + + currColumns = firstLine.split(';'); + Q_FOREACH (QString columnText, currColumns) { + if (columnText == "Time") { + headers.append("Sample time"); + } else if (columnText == "Depth") { + headers.append("Sample depth"); + } else if (columnText == "Temperature") { + headers.append("Sample temperature"); + } else if (columnText == "NDT") { + headers.append("Sample NDL"); + } else if (columnText == "TTS") { + headers.append("Sample TTS"); + } else if (columnText == "pO2_1") { + headers.append("Sample sensor1 pOâ‚‚"); + } else if (columnText == "pO2_2") { + headers.append("Sample sensor2 pOâ‚‚"); + } else if (columnText == "pO2_3") { + headers.append("Sample sensor3 pOâ‚‚"); + } else if (columnText == "Ceiling") { + headers.append("Sample ceiling"); + } else if (columnText == "Tank pressure") { + headers.append("Sample pressure"); + } else { + // We do not know about this value + qDebug() << "Seabear import found an un-handled field: " << columnText; + headers.append(""); + } + } + + firstLine = headers.join(";"); + blockSignals(true); + ui->knownImports->setCurrentText("Seabear CSV"); + blockSignals(false); + } else if (firstLine.contains("Tauchgangs-Nr.:")) { + xp5 = true; + //"Abgelaufene Tauchzeit (Std:Min.)\tTiefe\tStickstoff Balkenanzeige\tSauerstoff Balkenanzeige\tAufstiegsgeschwindigkeit\tRestluftzeit\tRestliche Tauchzeit\tDekompressionszeit (Std:Min)\tDekostopp-Tiefe\tTemperatur\tPO2\tPressluftflasche\tLesen des Druckes\tStatus der Verbindung\tTauchstatus"; + firstLine = "Sample time\tSample depth\t\t\t\t\t\t\t\tSample temperature\t"; + blockSignals(true); + ui->knownImports->setCurrentText("XP5"); + blockSignals(false); + } + + // Special handling for APD Log Viewer + if ((triggeredBy == KNOWNTYPES && (value == APD || value == APD2)) || (triggeredBy == INITIAL && fileNames.first().endsWith(".apd", Qt::CaseInsensitive))) { + apd=true; + firstLine = "Sample time\tSample depth\tSample setpoint\tSample sensor1 pOâ‚‚\tSample sensor2 pOâ‚‚\tSample sensor3 pOâ‚‚\tSample pOâ‚‚\t\t\t\t\t\t\t\t\tSample temperature\t\tSample CNS\tSample stopdepth"; + blockSignals(true); + ui->CSVSeparator->setCurrentText(tr("Tab")); + if (triggeredBy == INITIAL && fileNames.first().contains(".apd", Qt::CaseInsensitive)) + ui->knownImports->setCurrentText("APD Log Viewer - DC1"); + blockSignals(false); + } + + QString separator = ui->CSVSeparator->currentText() == tr("Tab") ? "\t" : ui->CSVSeparator->currentText(); + currColumns = firstLine.split(separator); + if (triggeredBy == INITIAL) { + // guess the separator + int tabs = firstLine.count('\t'); + int commas = firstLine.count(','); + int semis = firstLine.count(';'); + if (tabs > commas && tabs > semis) + separator = "\t"; + else if (commas > tabs && commas > semis) + separator = ","; + else if (semis > tabs && semis > commas) + separator = ";"; + if (ui->CSVSeparator->currentText() != separator) { + blockSignals(true); + ui->CSVSeparator->setCurrentText(separator); + blockSignals(false); + currColumns = firstLine.split(separator); + } + } + if (triggeredBy == INITIAL || (triggeredBy == KNOWNTYPES && value == MANUAL) || triggeredBy == SEPARATOR) { + // now try and guess the columns + Q_FOREACH (QString columnText, currColumns) { + /* + * We have to skip the conversion of 2 to â‚‚ for APD Log + * viewer as that would mess up the sensor numbering. We + * also know that the column headers do not need this + * conversion. + */ + if (apd == false) { + columnText.replace("\"", ""); + columnText.replace("number", "#", Qt::CaseInsensitive); + columnText.replace("2", "â‚‚", Qt::CaseInsensitive); + columnText.replace("cylinder", "cyl.", Qt::CaseInsensitive); + } + int idx = provider->mymatch(columnText.trimmed()); + if (idx >= 0) { + QString foundHeading = provider->data(provider->index(idx, 0), Qt::DisplayRole).toString(); + provider->removeRow(idx); + headers.append(foundHeading); + matchedSome = true; + } else { + headers.append(""); + } + } + if (matchedSome) { + ui->dragInstructions->setText(tr("Some column headers were pre-populated; please drag and drop the headers so they match the column they are in.")); + if (triggeredBy != KNOWNTYPES && !seabear && !xp5 && !apd) { + blockSignals(true); + ui->knownImports->setCurrentIndex(0); // <- that's "Manual import" + blockSignals(false); + } + } + } + if (triggeredBy == KNOWNTYPES && value != MANUAL) { + // an actual known type + if (value == SUBSURFACE) { + /* + * Subsurface CSV file needs separator detection + * as we used to default to comma but switched + * to tab. + */ + int tabs = firstLine.count('\t'); + int commas = firstLine.count(','); + if (tabs > commas) + separator = "Tab"; + else + separator = ","; + } else { + separator = CSVApps[value].separator; + } + + if (ui->CSVSeparator->currentText() != separator || separator == "Tab") { + ui->CSVSeparator->blockSignals(true); + ui->CSVSeparator->setCurrentText(separator); + ui->CSVSeparator->blockSignals(false); + if (separator == "Tab") + separator = "\t"; + currColumns = firstLine.split(separator); + } + // now set up time, depth, temperature, po2, cns, ndl, tts, stopdepth, pressure, setpoint + for (int i = 0; i < currColumns.count(); i++) + headers.append(""); + if (CSVApps[value].time > -1 && CSVApps[value].time < currColumns.count()) + headers.replace(CSVApps[value].time, tr("Sample time")); + if (CSVApps[value].depth > -1 && CSVApps[value].depth < currColumns.count()) + headers.replace(CSVApps[value].depth, tr("Sample depth")); + if (CSVApps[value].temperature > -1 && CSVApps[value].temperature < currColumns.count()) + headers.replace(CSVApps[value].temperature, tr("Sample temperature")); + if (CSVApps[value].po2 > -1 && CSVApps[value].po2 < currColumns.count()) + headers.replace(CSVApps[value].po2, tr("Sample pOâ‚‚")); + if (CSVApps[value].sensor1 > -1 && CSVApps[value].sensor1 < currColumns.count()) + headers.replace(CSVApps[value].sensor1, tr("Sample sensor1 pOâ‚‚")); + if (CSVApps[value].sensor2 > -1 && CSVApps[value].sensor2 < currColumns.count()) + headers.replace(CSVApps[value].sensor2, tr("Sample sensor2 pOâ‚‚")); + if (CSVApps[value].sensor3 > -1 && CSVApps[value].sensor3 < currColumns.count()) + headers.replace(CSVApps[value].sensor3, tr("Sample sensor3 pOâ‚‚")); + if (CSVApps[value].cns > -1 && CSVApps[value].cns < currColumns.count()) + headers.replace(CSVApps[value].cns, tr("Sample CNS")); + if (CSVApps[value].ndl > -1 && CSVApps[value].ndl < currColumns.count()) + headers.replace(CSVApps[value].ndl, tr("Sample NDL")); + if (CSVApps[value].tts > -1 && CSVApps[value].tts < currColumns.count()) + headers.replace(CSVApps[value].tts, tr("Sample TTS")); + if (CSVApps[value].stopdepth > -1 && CSVApps[value].stopdepth < currColumns.count()) + headers.replace(CSVApps[value].stopdepth, tr("Sample stopdepth")); + if (CSVApps[value].pressure > -1 && CSVApps[value].pressure < currColumns.count()) + headers.replace(CSVApps[value].pressure, tr("Sample pressure")); + if (CSVApps[value].setpoint > -1 && CSVApps[value].setpoint < currColumns.count()) + headers.replace(CSVApps[value].setpoint, tr("Sample setpoint")); + + /* Show the Subsurface CSV column headers */ + if (value == SUBSURFACE && currColumns.count() >= 23) { + headers.replace(0, tr("Dive #")); + headers.replace(1, tr("Date")); + headers.replace(2, tr("Time")); + headers.replace(3, tr("Duration")); + headers.replace(4, tr("Max. depth")); + headers.replace(5, tr("Avg. depth")); + headers.replace(6, tr("Air temp.")); + headers.replace(7, tr("Water temp.")); + headers.replace(8, tr("Cyl. size")); + headers.replace(9, tr("Start pressure")); + headers.replace(10, tr("End pressure")); + headers.replace(11, tr("Oâ‚‚")); + headers.replace(12, tr("He")); + headers.replace(13, tr("Location")); + headers.replace(14, tr("GPS")); + headers.replace(15, tr("Divemaster")); + headers.replace(16, tr("Buddy")); + headers.replace(17, tr("Suit")); + headers.replace(18, tr("Rating")); + headers.replace(19, tr("Visibility")); + headers.replace(20, tr("Notes")); + headers.replace(21, tr("Weight")); + headers.replace(22, tr("Tags")); + + blockSignals(true); + ui->CSVSeparator->setCurrentText(separator); + ui->DateFormat->setCurrentText("yyyy-mm-dd"); + ui->DurationFormat->setCurrentText("Minutes:seconds"); + blockSignals(false); + } + } + + f.reset(); + int rows = 0; + + /* Skipping the header of Seabear and XP5 CSV files. */ + if (seabear || xp5) { + /* + * First set of data on Seabear CSV file is metadata + * that is separated by an empty line (windows line + * termination might be encountered. + */ + while (strlen(f.readLine()) > 3 && !f.atEnd()); + /* + * Next we have description of the fields and two dummy + * lines. Separated again with an empty line from the + * actual data. + */ + while (strlen(f.readLine()) > 3 && !f.atEnd()); + } + + while (rows < 10 && !f.atEnd()) { + QString currLine = f.readLine().trimmed(); + currColumns = currLine.split(separator); + fileColumns.append(currColumns); + rows += 1; + } + resultModel->setColumnValues(fileColumns); + for (int i = 0; i < headers.count(); i++) + if (!headers.at(i).isEmpty()) + resultModel->setData(resultModel->index(0, i),headers.at(i),Qt::EditRole); +} + +char *intdup(int index) +{ + char tmpbuf[21]; + + snprintf(tmpbuf, sizeof(tmpbuf) - 2, "%d", index); + tmpbuf[20] = 0; + return strdup(tmpbuf); +} + +int DiveLogImportDialog::setup_csv_params(QStringList r, char **params, int pnr) +{ + params[pnr++] = strdup("timeField"); + params[pnr++] = intdup(r.indexOf(tr("Sample time"))); + params[pnr++] = strdup("depthField"); + params[pnr++] = intdup(r.indexOf(tr("Sample depth"))); + params[pnr++] = strdup("tempField"); + params[pnr++] = intdup(r.indexOf(tr("Sample temperature"))); + params[pnr++] = strdup("po2Field"); + params[pnr++] = intdup(r.indexOf(tr("Sample pOâ‚‚"))); + params[pnr++] = strdup("o2sensor1Field"); + params[pnr++] = intdup(r.indexOf(tr("Sample sensor1 pOâ‚‚"))); + params[pnr++] = strdup("o2sensor2Field"); + params[pnr++] = intdup(r.indexOf(tr("Sample sensor2 pOâ‚‚"))); + params[pnr++] = strdup("o2sensor3Field"); + params[pnr++] = intdup(r.indexOf(tr("Sample sensor3 pOâ‚‚"))); + params[pnr++] = strdup("cnsField"); + params[pnr++] = intdup(r.indexOf(tr("Sample CNS"))); + params[pnr++] = strdup("ndlField"); + params[pnr++] = intdup(r.indexOf(tr("Sample NDL"))); + params[pnr++] = strdup("ttsField"); + params[pnr++] = intdup(r.indexOf(tr("Sample TTS"))); + params[pnr++] = strdup("stopdepthField"); + params[pnr++] = intdup(r.indexOf(tr("Sample stopdepth"))); + params[pnr++] = strdup("pressureField"); + params[pnr++] = intdup(r.indexOf(tr("Sample pressure"))); + params[pnr++] = strdup("setpointFiend"); + params[pnr++] = intdup(r.indexOf(tr("Sample setpoint"))); + params[pnr++] = strdup("separatorIndex"); + params[pnr++] = intdup(ui->CSVSeparator->currentIndex()); + params[pnr++] = strdup("units"); + params[pnr++] = intdup(ui->CSVUnits->currentIndex()); + if (hw.length()) { + params[pnr++] = strdup("hw"); + params[pnr++] = strdup(hw.toUtf8().data()); + } else if (ui->knownImports->currentText().length() > 0) { + params[pnr++] = strdup("hw"); + params[pnr++] = strdup(ui->knownImports->currentText().prepend("\"").append("\"").toUtf8().data()); + } + params[pnr++] = NULL; + + return pnr; +} + +void DiveLogImportDialog::on_buttonBox_accepted() +{ + QStringList r = resultModel->result(); + if (ui->knownImports->currentText() != "Manual import") { + for (int i = 0; i < fileNames.size(); ++i) { + if (ui->knownImports->currentText() == "Seabear CSV") { + char *params[40]; + int pnr = 0; + + params[pnr++] = strdup("timeField"); + params[pnr++] = intdup(r.indexOf(tr("Sample time"))); + params[pnr++] = strdup("depthField"); + params[pnr++] = intdup(r.indexOf(tr("Sample depth"))); + params[pnr++] = strdup("tempField"); + params[pnr++] = intdup(r.indexOf(tr("Sample temperature"))); + params[pnr++] = strdup("po2Field"); + params[pnr++] = intdup(r.indexOf(tr("Sample pOâ‚‚"))); + params[pnr++] = strdup("o2sensor1Field"); + params[pnr++] = intdup(r.indexOf(tr("Sample sensor1 pOâ‚‚"))); + params[pnr++] = strdup("o2sensor2Field"); + params[pnr++] = intdup(r.indexOf(tr("Sample sensor2 pOâ‚‚"))); + params[pnr++] = strdup("o2sensor3Field"); + params[pnr++] = intdup(r.indexOf(tr("Sample sensor3 pOâ‚‚"))); + params[pnr++] = strdup("cnsField"); + params[pnr++] = intdup(r.indexOf(tr("Sample CNS"))); + params[pnr++] = strdup("ndlField"); + params[pnr++] = intdup(r.indexOf(tr("Sample NDL"))); + params[pnr++] = strdup("ttsField"); + params[pnr++] = intdup(r.indexOf(tr("Sample TTS"))); + params[pnr++] = strdup("stopdepthField"); + params[pnr++] = intdup(r.indexOf(tr("Sample stopdepth"))); + params[pnr++] = strdup("pressureField"); + params[pnr++] = intdup(r.indexOf(tr("Sample pressure"))); + params[pnr++] = strdup("setpointFiend"); + params[pnr++] = intdup(-1); + params[pnr++] = strdup("separatorIndex"); + params[pnr++] = intdup(ui->CSVSeparator->currentIndex()); + params[pnr++] = strdup("units"); + params[pnr++] = intdup(ui->CSVUnits->currentIndex()); + params[pnr++] = strdup("delta"); + params[pnr++] = strdup(delta.toUtf8().data()); + if (hw.length()) { + params[pnr++] = strdup("hw"); + params[pnr++] = strdup(hw.toUtf8().data()); + } + params[pnr++] = NULL; + + if (parse_seabear_csv_file(fileNames[i].toUtf8().data(), + params, pnr - 1, "csv") < 0) { + return; + } + // Seabear CSV stores NDL and TTS in Minutes, not seconds + struct dive *dive = dive_table.dives[dive_table.nr - 1]; + for(int s_nr = 0 ; s_nr <= dive->dc.samples ; s_nr++) { + struct sample *sample = dive->dc.sample + s_nr; + sample->ndl.seconds *= 60; + sample->tts.seconds *= 60; + } + } else { + char *params[37]; + int pnr = 0; + + pnr = setup_csv_params(r, params, pnr); + parse_csv_file(fileNames[i].toUtf8().data(), params, pnr - 1, + specialCSV.contains(ui->knownImports->currentIndex()) ? CSVApps[ui->knownImports->currentIndex()].name.toUtf8().data() : "csv"); + } + } + } else { + for (int i = 0; i < fileNames.size(); ++i) { + if (r.indexOf(tr("Sample time")) < 0) { + char *params[55]; + int pnr = 0; + params[pnr++] = strdup("numberField"); + params[pnr++] = intdup(r.indexOf(tr("Dive #"))); + params[pnr++] = strdup("dateField"); + params[pnr++] = intdup(r.indexOf(tr("Date"))); + params[pnr++] = strdup("timeField"); + params[pnr++] = intdup(r.indexOf(tr("Time"))); + params[pnr++] = strdup("durationField"); + params[pnr++] = intdup(r.indexOf(tr("Duration"))); + params[pnr++] = strdup("locationField"); + params[pnr++] = intdup(r.indexOf(tr("Location"))); + params[pnr++] = strdup("gpsField"); + params[pnr++] = intdup(r.indexOf(tr("GPS"))); + params[pnr++] = strdup("maxDepthField"); + params[pnr++] = intdup(r.indexOf(tr("Max. depth"))); + params[pnr++] = strdup("meanDepthField"); + params[pnr++] = intdup(r.indexOf(tr("Avg. depth"))); + params[pnr++] = strdup("divemasterField"); + params[pnr++] = intdup(r.indexOf(tr("Divemaster"))); + params[pnr++] = strdup("buddyField"); + params[pnr++] = intdup(r.indexOf(tr("Buddy"))); + params[pnr++] = strdup("suitField"); + params[pnr++] = intdup(r.indexOf(tr("Suit"))); + params[pnr++] = strdup("notesField"); + params[pnr++] = intdup(r.indexOf(tr("Notes"))); + params[pnr++] = strdup("weightField"); + params[pnr++] = intdup(r.indexOf(tr("Weight"))); + params[pnr++] = strdup("tagsField"); + params[pnr++] = intdup(r.indexOf(tr("Tags"))); + params[pnr++] = strdup("separatorIndex"); + params[pnr++] = intdup(ui->CSVSeparator->currentIndex()); + params[pnr++] = strdup("units"); + params[pnr++] = intdup(ui->CSVUnits->currentIndex()); + params[pnr++] = strdup("datefmt"); + params[pnr++] = intdup(ui->DateFormat->currentIndex()); + params[pnr++] = strdup("durationfmt"); + params[pnr++] = intdup(ui->DurationFormat->currentIndex()); + params[pnr++] = strdup("cylindersizeField"); + params[pnr++] = intdup(r.indexOf(tr("Cyl. size"))); + params[pnr++] = strdup("startpressureField"); + params[pnr++] = intdup(r.indexOf(tr("Start pressure"))); + params[pnr++] = strdup("endpressureField"); + params[pnr++] = intdup(r.indexOf(tr("End pressure"))); + params[pnr++] = strdup("o2Field"); + params[pnr++] = intdup(r.indexOf(tr("Oâ‚‚"))); + params[pnr++] = strdup("heField"); + params[pnr++] = intdup(r.indexOf(tr("He"))); + params[pnr++] = strdup("airtempField"); + params[pnr++] = intdup(r.indexOf(tr("Air temp."))); + params[pnr++] = strdup("watertempField"); + params[pnr++] = intdup(r.indexOf(tr("Water temp."))); + params[pnr++] = NULL; + + parse_manual_file(fileNames[i].toUtf8().data(), params, pnr - 1); + } else { + char *params[37]; + int pnr = 0; + + pnr = setup_csv_params(r, params, pnr); + parse_csv_file(fileNames[i].toUtf8().data(), params, pnr - 1, + specialCSV.contains(ui->knownImports->currentIndex()) ? CSVApps[ui->knownImports->currentIndex()].name.toUtf8().data() : "csv"); + } + } + } + + process_dives(true, false); + MainWindow::instance()->refreshDisplay(); +} + +TagDragDelegate::TagDragDelegate(QObject *parent) : QStyledItemDelegate(parent) +{ +} + +QSize TagDragDelegate::sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const +{ + QSize originalSize = QStyledItemDelegate::sizeHint(option, index); + return originalSize + QSize(5,5); +} + +void TagDragDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const +{ + painter->save(); + painter->setRenderHints(QPainter::Antialiasing); + painter->setBrush(QBrush(AIR_BLUE_TRANS)); + painter->drawRoundedRect(option.rect.adjusted(2,2,-2,-2), 5, 5); + painter->restore(); + QStyledItemDelegate::paint(painter, option, index); +} diff --git a/desktop-widgets/divelogimportdialog.h b/desktop-widgets/divelogimportdialog.h new file mode 100644 index 000000000..2d12c7cac --- /dev/null +++ b/desktop-widgets/divelogimportdialog.h @@ -0,0 +1,131 @@ +#ifndef DIVELOGIMPORTDIALOG_H +#define DIVELOGIMPORTDIALOG_H + +#include <QDialog> +#include <QAbstractListModel> +#include <QListView> +#include <QDragLeaveEvent> +#include <QTableView> +#include <QAbstractTableModel> +#include <QStyledItemDelegate> + +#include "subsurface-core/dive.h" +#include "subsurface-core/divelist.h" + +namespace Ui { + class DiveLogImportDialog; +} + +class ColumnNameProvider : public QAbstractListModel { + Q_OBJECT +public: + ColumnNameProvider(QObject *parent); + bool insertRows(int row, int count, const QModelIndex &parent); + bool removeRows(int row, int count, const QModelIndex &parent); + bool setData(const QModelIndex &index, const QVariant &value, int role); + QVariant data(const QModelIndex &index, int role) const; + int rowCount(const QModelIndex &parent) const; + int mymatch(QString value) const; +private: + QStringList columnNames; +}; + +class ColumnNameResult : public QAbstractTableModel { + Q_OBJECT +public: + ColumnNameResult(QObject *parent); + bool setData(const QModelIndex &index, const QVariant &value, int role); + QVariant data(const QModelIndex &index, int role) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + void setColumnValues(QList<QStringList> columns); + QStringList result() const; + void swapValues(int firstIndex, int secondIndex); +private: + QList<QStringList> columnValues; + QStringList columnNames; +}; + +class ColumnNameView : public QListView { + Q_OBJECT +public: + ColumnNameView(QWidget *parent); +protected: + void mousePressEvent(QMouseEvent *press); + void dragLeaveEvent(QDragLeaveEvent *leave); + void dragEnterEvent(QDragEnterEvent *event); + void dragMoveEvent(QDragMoveEvent *event); + void dropEvent(QDropEvent *event); +private: +}; + +class ColumnDropCSVView : public QTableView { + Q_OBJECT +public: + ColumnDropCSVView(QWidget *parent); +protected: + void mousePressEvent(QMouseEvent *press); + void dragLeaveEvent(QDragLeaveEvent *leave); + void dragEnterEvent(QDragEnterEvent *event); + void dragMoveEvent(QDragMoveEvent *event); + void dropEvent(QDropEvent *event); +private: + QStringList columns; +}; + +class DiveLogImportDialog : public QDialog { + Q_OBJECT + +public: + explicit DiveLogImportDialog(QStringList fn, QWidget *parent = 0); + ~DiveLogImportDialog(); + enum whatChanged { INITIAL, SEPARATOR, KNOWNTYPES }; +private +slots: + void on_buttonBox_accepted(); + void loadFileContentsSeperatorSelected(int value); + void loadFileContentsKnownTypesSelected(int value); + void loadFileContents(int value, enum whatChanged triggeredBy); + int setup_csv_params(QStringList r, char **params, int pnr); + +private: + bool selector; + QStringList fileNames; + Ui::DiveLogImportDialog *ui; + QList<int> specialCSV; + int column; + ColumnNameResult *resultModel; + QString delta; + QString hw; + + struct CSVAppConfig { + QString name; + int time; + int depth; + int temperature; + int po2; + int sensor1; + int sensor2; + int sensor3; + int cns; + int ndl; + int tts; + int stopdepth; + int pressure; + int setpoint; + QString separator; + }; + +#define CSVAPPS 8 + static const CSVAppConfig CSVApps[CSVAPPS]; +}; + +class TagDragDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + TagDragDelegate(QObject *parent); + QSize sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const; + void paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const; +}; + +#endif // DIVELOGIMPORTDIALOG_H diff --git a/desktop-widgets/divelogimportdialog.ui b/desktop-widgets/divelogimportdialog.ui new file mode 100644 index 000000000..6d154b7c6 --- /dev/null +++ b/desktop-widgets/divelogimportdialog.ui @@ -0,0 +1,249 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>DiveLogImportDialog</class> + <widget class="QDialog" name="DiveLogImportDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>614</width> + <height>434</height> + </rect> + </property> + <property name="windowTitle"> + <string>Import dive log file</string> + </property> + <property name="windowIcon"> + <iconset> + <normalon>:/subsurface-icon</normalon> + </iconset> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="topMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>5</number> + </property> + <property name="bottomMargin"> + <number>5</number> + </property> + <item> + <widget class="QWidget" name="horizontalWidget" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QWidget" name="verticalWidget" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="spacing"> + <number>5</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>2</number> + </property> + <item> + <widget class="QComboBox" name="knownImports"> + <property name="currentIndex"> + <number>-1</number> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="CSVSeparator"/> + </item> + <item> + <widget class="QComboBox" name="DateFormat"> + <item> + <property name="text"> + <string>dd.mm.yyyy</string> + </property> + </item> + <item> + <property name="text"> + <string>mm/dd/yyyy</string> + </property> + </item> + <item> + <property name="text"> + <string>yyyy-mm-dd</string> + </property> + </item> + </widget> + </item> + <item> + <widget class="QComboBox" name="DurationFormat"> + <item> + <property name="text"> + <string>Seconds</string> + </property> + </item> + <item> + <property name="text"> + <string>Minutes</string> + </property> + </item> + <item> + <property name="text"> + <string>Minutes:seconds</string> + </property> + </item> + </widget> + </item> + <item> + <widget class="QComboBox" name="CSVUnits"> + <item> + <property name="text"> + <string>Metric</string> + </property> + </item> + <item> + <property name="text"> + <string>Imperial</string> + </property> + </item> + </widget> + </item> + </layout> + </item> + <item> + <widget class="ColumnNameView" name="avaliableColumns"> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>100</height> + </size> + </property> + <property name="viewMode"> + <enum>QListView::IconMode</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="dragInstructions"> + <property name="text"> + <string>Drag the tags above to each corresponding column below</string> + </property> + </widget> + </item> + <item> + <widget class="ColumnDropCSVView" name="tableView"> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>16777215</height> + </size> + </property> + <attribute name="horizontalHeaderVisible"> + <bool>false</bool> + </attribute> + <attribute name="horizontalHeaderHighlightSections"> + <bool>false</bool> + </attribute> + <attribute name="verticalHeaderVisible"> + <bool>false</bool> + </attribute> + <attribute name="verticalHeaderHighlightSections"> + <bool>false</bool> + </attribute> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>ColumnNameView</class> + <extends>QListView</extends> + <header>divelogimportdialog.h</header> + </customwidget> + <customwidget> + <class>ColumnDropCSVView</class> + <extends>QTableView</extends> + <header>divelogimportdialog.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>DiveLogImportDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>334</x> + <y>467</y> + </hint> + <hint type="destinationlabel"> + <x>215</x> + <y>164</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>DiveLogImportDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>334</x> + <y>467</y> + </hint> + <hint type="destinationlabel"> + <x>215</x> + <y>164</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/desktop-widgets/divepicturewidget.cpp b/desktop-widgets/divepicturewidget.cpp new file mode 100644 index 000000000..bed3d3bd1 --- /dev/null +++ b/desktop-widgets/divepicturewidget.cpp @@ -0,0 +1,100 @@ +#include "divepicturewidget.h" +#include "divepicturemodel.h" +#include "metrics.h" +#include "dive.h" +#include "divelist.h" +#include <unistd.h> +#include <QtConcurrentMap> +#include <QtConcurrentRun> +#include <QFuture> +#include <QDir> +#include <QCryptographicHash> +#include <QNetworkAccessManager> +#include <QNetworkReply> +#include <mainwindow.h> +#include <qthelper.h> +#include <QStandardPaths> + +void loadPicture(struct picture *picture) +{ + ImageDownloader download(picture); + download.load(); +} + +SHashedImage::SHashedImage(struct picture *picture) : QImage() +{ + QUrl url = QUrl::fromUserInput(QString(picture->filename)); + if(url.isLocalFile()) + load(url.toLocalFile()); + if (isNull()) { + // Hash lookup. + load(fileFromHash(picture->hash)); + if (!isNull()) { + QtConcurrent::run(updateHash, picture); + } else { + QtConcurrent::run(loadPicture, picture); + } + } else { + QByteArray hash = hashFile(url.toLocalFile()); + free(picture->hash); + picture->hash = strdup(hash.toHex().data()); + } +} + +ImageDownloader::ImageDownloader(struct picture *pic) +{ + picture = pic; +} + +void ImageDownloader::load(){ + QUrl url = QUrl::fromUserInput(QString(picture->filename)); + if (url.isValid()) { + QEventLoop loop; + QNetworkRequest request(url); + connect(&manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(saveImage(QNetworkReply *))); + QNetworkReply *reply = manager.get(request); + while (reply->isRunning()) { + loop.processEvents(); + sleep(1); + } + } + +} + +void ImageDownloader::saveImage(QNetworkReply *reply) +{ + QByteArray imageData = reply->readAll(); + QImage image = QImage(); + image.loadFromData(imageData); + if (image.isNull()) + return; + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(imageData); + QString path = QStandardPaths::standardLocations(QStandardPaths::CacheLocation).first(); + QDir dir(path); + if (!dir.exists()) + dir.mkpath(path); + QFile imageFile(path.append("/").append(hash.result().toHex())); + if (imageFile.open(QIODevice::WriteOnly)) { + QDataStream stream(&imageFile); + stream.writeRawData(imageData.data(), imageData.length()); + imageFile.waitForBytesWritten(-1); + imageFile.close(); + add_hash(imageFile.fileName(), hash.result()); + learnHash(picture, hash.result()); + DivePictureModel::instance()->updateDivePictures(); + } + reply->manager()->deleteLater(); + reply->deleteLater(); +} + +DivePictureWidget::DivePictureWidget(QWidget *parent) : QListView(parent) +{ + connect(this, SIGNAL(doubleClicked(const QModelIndex &)), this, SLOT(doubleClicked(const QModelIndex &))); +} + +void DivePictureWidget::doubleClicked(const QModelIndex &index) +{ + QString filePath = model()->data(index, Qt::DisplayPropertyRole).toString(); + emit photoDoubleClicked(localFilePath(filePath)); +} diff --git a/desktop-widgets/divepicturewidget.h b/desktop-widgets/divepicturewidget.h new file mode 100644 index 000000000..54f5bb826 --- /dev/null +++ b/desktop-widgets/divepicturewidget.h @@ -0,0 +1,36 @@ +#ifndef DIVEPICTUREWIDGET_H +#define DIVEPICTUREWIDGET_H + +#include <QAbstractTableModel> +#include <QListView> +#include <QThread> +#include <QFuture> +#include <QNetworkReply> + +class ImageDownloader : public QObject { + Q_OBJECT; +public: + ImageDownloader(struct picture *picture); + void load(); +private: + struct picture *picture; + QNetworkAccessManager manager; +private slots: + void saveImage(QNetworkReply *reply); +}; + +class DivePictureWidget : public QListView { + Q_OBJECT +public: + DivePictureWidget(QWidget *parent); +signals: + void photoDoubleClicked(const QString filePath); +private +slots: + void doubleClicked(const QModelIndex &index); +}; + +class DivePictureThumbnailThread : public QThread { +}; + +#endif diff --git a/desktop-widgets/diveplanner.cpp b/desktop-widgets/diveplanner.cpp new file mode 100644 index 000000000..b4413d11a --- /dev/null +++ b/desktop-widgets/diveplanner.cpp @@ -0,0 +1,513 @@ +#include "diveplanner.h" +#include "modeldelegates.h" +#include "mainwindow.h" +#include "planner.h" +#include "helpers.h" +#include "cylindermodel.h" +#include "models.h" +#include "profile/profilewidget2.h" +#include "diveplannermodel.h" + +#include <QGraphicsSceneMouseEvent> +#include <QMessageBox> +#include <QSettings> +#include <QShortcut> + +#define TIME_INITIAL_MAX 30 + +#define MAX_DEPTH M_OR_FT(150, 450) +#define MIN_DEPTH M_OR_FT(20, 60) + +#define UNIT_FACTOR ((prefs.units.length == units::METERS) ? 1000.0 / 60.0 : feet_to_mm(1.0) / 60.0) + +static DivePlannerPointsModel* plannerModel = DivePlannerPointsModel::instance(); + +DiveHandler::DiveHandler() : QGraphicsEllipseItem() +{ + setRect(-5, -5, 10, 10); + setFlags(ItemIgnoresTransformations | ItemIsSelectable | ItemIsMovable | ItemSendsGeometryChanges); + setBrush(Qt::white); + setZValue(2); + t.start(); +} + +int DiveHandler::parentIndex() +{ + ProfileWidget2 *view = qobject_cast<ProfileWidget2 *>(scene()->views().first()); + return view->handles.indexOf(this); +} + +void DiveHandler::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) +{ + QMenu m; + // Don't have a gas selection for the last point + QModelIndex index = plannerModel->index(parentIndex(), DivePlannerPointsModel::GAS); + if (index.sibling(index.row() + 1, index.column()).isValid()) { + GasSelectionModel *model = GasSelectionModel::instance(); + model->repopulate(); + int rowCount = model->rowCount(); + for (int i = 0; i < rowCount; i++) { + QAction *action = new QAction(&m); + action->setText(model->data(model->index(i, 0), Qt::DisplayRole).toString()); + connect(action, SIGNAL(triggered(bool)), this, SLOT(changeGas())); + m.addAction(action); + } + } + // don't allow removing the last point + if (plannerModel->rowCount() > 1) { + m.addSeparator(); + m.addAction(QObject::tr("Remove this point"), this, SLOT(selfRemove())); + m.exec(event->screenPos()); + } +} + +void DiveHandler::selfRemove() +{ + setSelected(true); + ProfileWidget2 *view = qobject_cast<ProfileWidget2 *>(scene()->views().first()); + view->keyDeleteAction(); +} + +void DiveHandler::changeGas() +{ + QAction *action = qobject_cast<QAction *>(sender()); + QModelIndex index = plannerModel->index(parentIndex(), DivePlannerPointsModel::GAS); + plannerModel->gaschange(index.sibling(index.row() + 1, index.column()), action->text()); +} + +void DiveHandler::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +{ + if (t.elapsed() < 40) + return; + t.start(); + + ProfileWidget2 *view = qobject_cast<ProfileWidget2*>(scene()->views().first()); + if(view->isPointOutOfBoundaries(event->scenePos())) + return; + + QGraphicsEllipseItem::mouseMoveEvent(event); + emit moved(); +} + +void DiveHandler::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + QGraphicsItem::mousePressEvent(event); + emit clicked(); +} + +void DiveHandler::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + QGraphicsItem::mouseReleaseEvent(event); + emit released(); +} + +DivePlannerWidget::DivePlannerWidget(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f) +{ + ui.setupUi(this); + ui.dateEdit->setDisplayFormat(getDateFormat()); + ui.tableWidget->setTitle(tr("Dive planner points")); + ui.tableWidget->setModel(plannerModel); + plannerModel->setRecalc(true); + ui.tableWidget->view()->setItemDelegateForColumn(DivePlannerPointsModel::GAS, new AirTypesDelegate(this)); + ui.cylinderTableWidget->setTitle(tr("Available gases")); + ui.cylinderTableWidget->setModel(CylindersModel::instance()); + QTableView *view = ui.cylinderTableWidget->view(); + view->setColumnHidden(CylindersModel::START, true); + view->setColumnHidden(CylindersModel::END, true); + view->setColumnHidden(CylindersModel::DEPTH, false); + view->setItemDelegateForColumn(CylindersModel::TYPE, new TankInfoDelegate(this)); + connect(ui.cylinderTableWidget, SIGNAL(addButtonClicked()), plannerModel, SLOT(addCylinder_clicked())); + connect(ui.tableWidget, SIGNAL(addButtonClicked()), plannerModel, SLOT(addStop())); + + connect(CylindersModel::instance(), SIGNAL(dataChanged(QModelIndex, QModelIndex)), + GasSelectionModel::instance(), SLOT(repopulate())); + connect(CylindersModel::instance(), SIGNAL(rowsInserted(QModelIndex, int, int)), + GasSelectionModel::instance(), SLOT(repopulate())); + connect(CylindersModel::instance(), SIGNAL(rowsRemoved(QModelIndex, int, int)), + GasSelectionModel::instance(), SLOT(repopulate())); + connect(CylindersModel::instance(), SIGNAL(dataChanged(QModelIndex, QModelIndex)), + plannerModel, SIGNAL(cylinderModelEdited())); + connect(CylindersModel::instance(), SIGNAL(rowsInserted(QModelIndex, int, int)), + plannerModel, SIGNAL(cylinderModelEdited())); + connect(CylindersModel::instance(), SIGNAL(rowsRemoved(QModelIndex, int, int)), + plannerModel, SIGNAL(cylinderModelEdited())); + connect(plannerModel, SIGNAL(calculatedPlanNotes()), MainWindow::instance(), SLOT(setPlanNotes())); + + + ui.tableWidget->setBtnToolTip(tr("Add dive data point")); + connect(ui.startTime, SIGNAL(timeChanged(QTime)), plannerModel, SLOT(setStartTime(QTime))); + connect(ui.dateEdit, SIGNAL(dateChanged(QDate)), plannerModel, SLOT(setStartDate(QDate))); + connect(ui.ATMPressure, SIGNAL(valueChanged(int)), this, SLOT(atmPressureChanged(int))); + connect(ui.atmHeight, SIGNAL(valueChanged(int)), this, SLOT(heightChanged(int))); + connect(ui.salinity, SIGNAL(valueChanged(double)), this, SLOT(salinityChanged(double))); + connect(plannerModel, SIGNAL(startTimeChanged(QDateTime)), this, SLOT(setupStartTime(QDateTime))); + + // Creating (and canceling) the plan + replanButton = ui.buttonBox->addButton(tr("Save new"), QDialogButtonBox::ActionRole); + connect(replanButton, SIGNAL(clicked()), plannerModel, SLOT(saveDuplicatePlan())); + connect(ui.buttonBox, SIGNAL(accepted()), plannerModel, SLOT(savePlan())); + connect(ui.buttonBox, SIGNAL(rejected()), plannerModel, SLOT(cancelPlan())); + QShortcut *closeKey = new QShortcut(QKeySequence(Qt::Key_Escape), this); + connect(closeKey, SIGNAL(activated()), plannerModel, SLOT(cancelPlan())); + + // This makes shure the spinbox gets a setMinimum(0) on it so we can't have negative time or depth. + ui.tableWidget->view()->setItemDelegateForColumn(DivePlannerPointsModel::DEPTH, new SpinBoxDelegate(0, INT_MAX, 1, this)); + ui.tableWidget->view()->setItemDelegateForColumn(DivePlannerPointsModel::RUNTIME, new SpinBoxDelegate(0, INT_MAX, 1, this)); + ui.tableWidget->view()->setItemDelegateForColumn(DivePlannerPointsModel::DURATION, new SpinBoxDelegate(0, INT_MAX, 1, this)); + ui.tableWidget->view()->setItemDelegateForColumn(DivePlannerPointsModel::CCSETPOINT, new DoubleSpinBoxDelegate(0, 2, 0.1, this)); + + /* set defaults. */ + ui.ATMPressure->setValue(1013); + ui.atmHeight->setValue(0); + + setMinimumWidth(0); + setMinimumHeight(0); +} + +void DivePlannerWidget::setReplanButton(bool replan) +{ + replanButton->setVisible(replan); +} + +void DivePlannerWidget::setupStartTime(QDateTime startTime) +{ + ui.startTime->setTime(startTime.time()); + ui.dateEdit->setDate(startTime.date()); +} + +void DivePlannerWidget::settingsChanged() +{ + // Adopt units + if (get_units()->length == units::FEET) { + ui.atmHeight->setSuffix("ft"); + } else { + ui.atmHeight->setSuffix(("m")); + } + ui.atmHeight->blockSignals(true); + ui.atmHeight->setValue((int) get_depth_units((int) (log(1013.0 / plannerModel->getSurfacePressure()) * 7800000), NULL,NULL)); + ui.atmHeight->blockSignals(false); +} + +void DivePlannerWidget::atmPressureChanged(const int pressure) +{ + plannerModel->setSurfacePressure(pressure); + ui.atmHeight->blockSignals(true); + ui.atmHeight->setValue((int) get_depth_units((int) (log(1013.0 / pressure) * 7800000), NULL,NULL)); + ui.atmHeight->blockSignals(false); +} + +void DivePlannerWidget::heightChanged(const int height) +{ + int pressure = (int) (1013.0 * exp(- (double) units_to_depth((double) height) / 7800000.0)); + ui.ATMPressure->blockSignals(true); + ui.ATMPressure->setValue(pressure); + ui.ATMPressure->blockSignals(false); + plannerModel->setSurfacePressure(pressure); +} + +void DivePlannerWidget::salinityChanged(const double salinity) +{ + /* Salinity is expressed in weight in grams per 10l */ + plannerModel->setSalinity(10000 * salinity); +} + +void PlannerSettingsWidget::bottomSacChanged(const double bottomSac) +{ + plannerModel->setBottomSac(bottomSac); +} + +void PlannerSettingsWidget::decoSacChanged(const double decosac) +{ + plannerModel->setDecoSac(decosac); +} + +void PlannerSettingsWidget::disableDecoElements(int mode) +{ + if (mode == RECREATIONAL) { + ui.gflow->setDisabled(false); + ui.gfhigh->setDisabled(false); + ui.lastStop->setDisabled(true); + ui.backgasBreaks->setDisabled(true); + ui.bottompo2->setDisabled(true); + ui.decopo2->setDisabled(true); + ui.reserve_gas->setDisabled(false); + ui.conservatism_lvl->setDisabled(true); + ui.switch_at_req_stop->setDisabled(true); + ui.min_switch_duration->setDisabled(true); + } + else if (mode == VPMB) { + ui.gflow->setDisabled(true); + ui.gfhigh->setDisabled(true); + ui.lastStop->setDisabled(false); + ui.backgasBreaks->setDisabled(false); + ui.bottompo2->setDisabled(false); + ui.decopo2->setDisabled(false); + ui.reserve_gas->setDisabled(true); + ui.conservatism_lvl->setDisabled(false); + ui.switch_at_req_stop->setDisabled(false); + ui.min_switch_duration->setDisabled(false); + } + else if (mode == BUEHLMANN) { + ui.gflow->setDisabled(false); + ui.gfhigh->setDisabled(false); + ui.lastStop->setDisabled(false); + ui.backgasBreaks->setDisabled(false); + ui.bottompo2->setDisabled(false); + ui.decopo2->setDisabled(false); + ui.reserve_gas->setDisabled(true); + ui.conservatism_lvl->setDisabled(true); + ui.switch_at_req_stop->setDisabled(false); + ui.min_switch_duration->setDisabled(false); + } +} + +void DivePlannerWidget::printDecoPlan() +{ + MainWindow::instance()->printPlan(); +} + +PlannerSettingsWidget::PlannerSettingsWidget(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f) +{ + ui.setupUi(this); + + QSettings s; + QStringList rebreather_modes; + s.beginGroup("Planner"); + prefs.last_stop = s.value("last_stop", prefs.last_stop).toBool(); + prefs.verbatim_plan = s.value("verbatim_plan", prefs.verbatim_plan).toBool(); + prefs.display_duration = s.value("display_duration", prefs.display_duration).toBool(); + prefs.display_runtime = s.value("display_runtime", prefs.display_runtime).toBool(); + prefs.display_transitions = s.value("display_transitions", prefs.display_transitions).toBool(); + prefs.deco_mode = deco_mode(s.value("deco_mode", prefs.deco_mode).toInt()); + prefs.safetystop = s.value("safetystop", prefs.safetystop).toBool(); + prefs.reserve_gas = s.value("reserve_gas", prefs.reserve_gas).toInt(); + prefs.ascrate75 = s.value("ascrate75", prefs.ascrate75).toInt(); + prefs.ascrate50 = s.value("ascrate50", prefs.ascrate50).toInt(); + prefs.ascratestops = s.value("ascratestops", prefs.ascratestops).toInt(); + prefs.ascratelast6m = s.value("ascratelast6m", prefs.ascratelast6m).toInt(); + prefs.descrate = s.value("descrate", prefs.descrate).toInt(); + prefs.bottompo2 = s.value("bottompo2", prefs.bottompo2).toInt(); + prefs.decopo2 = s.value("decopo2", prefs.decopo2).toInt(); + prefs.doo2breaks = s.value("doo2breaks", prefs.doo2breaks).toBool(); + prefs.switch_at_req_stop = s.value("switch_at_req_stop", prefs.switch_at_req_stop).toBool(); + prefs.min_switch_duration = s.value("min_switch_duration", prefs.min_switch_duration).toInt(); + prefs.drop_stone_mode = s.value("drop_stone_mode", prefs.drop_stone_mode).toBool(); + prefs.bottomsac = s.value("bottomsac", prefs.bottomsac).toInt(); + prefs.decosac = s.value("decosac", prefs.decosac).toInt(); + prefs.conservatism_level = s.value("conservatism", prefs.conservatism_level).toInt(); + plannerModel->getDiveplan().bottomsac = prefs.bottomsac; + plannerModel->getDiveplan().decosac = prefs.decosac; + s.endGroup(); + + updateUnitsUI(); + ui.lastStop->setChecked(prefs.last_stop); + ui.verbatim_plan->setChecked(prefs.verbatim_plan); + ui.display_duration->setChecked(prefs.display_duration); + ui.display_runtime->setChecked(prefs.display_runtime); + ui.display_transitions->setChecked(prefs.display_transitions); + ui.safetystop->setChecked(prefs.safetystop); + ui.reserve_gas->setValue(prefs.reserve_gas / 1000); + ui.bottompo2->setValue(prefs.bottompo2 / 1000.0); + ui.decopo2->setValue(prefs.decopo2 / 1000.0); + ui.backgasBreaks->setChecked(prefs.doo2breaks); + ui.drop_stone_mode->setChecked(prefs.drop_stone_mode); + ui.switch_at_req_stop->setChecked(prefs.switch_at_req_stop); + ui.min_switch_duration->setValue(prefs.min_switch_duration / 60); + ui.recreational_deco->setChecked(prefs.deco_mode == RECREATIONAL); + ui.buehlmann_deco->setChecked(prefs.deco_mode == BUEHLMANN); + ui.vpmb_deco->setChecked(prefs.deco_mode == VPMB); + ui.conservatism_lvl->setValue(prefs.conservatism_level); + disableDecoElements((int) prefs.deco_mode); + + // should be the same order as in dive_comp_type! + rebreather_modes << tr("Open circuit") << tr("CCR") << tr("pSCR"); + ui.rebreathermode->insertItems(0, rebreather_modes); + + modeMapper = new QSignalMapper(this); + connect(modeMapper, SIGNAL(mapped(int)) , plannerModel, SLOT(setDecoMode(int))); + modeMapper->setMapping(ui.recreational_deco, int(RECREATIONAL)); + modeMapper->setMapping(ui.buehlmann_deco, int(BUEHLMANN)); + modeMapper->setMapping(ui.vpmb_deco, int(VPMB)); + + connect(ui.recreational_deco, SIGNAL(clicked()), modeMapper, SLOT(map())); + connect(ui.buehlmann_deco, SIGNAL(clicked()), modeMapper, SLOT(map())); + connect(ui.vpmb_deco, SIGNAL(clicked()), modeMapper, SLOT(map())); + + connect(ui.lastStop, SIGNAL(toggled(bool)), plannerModel, SLOT(setLastStop6m(bool))); + connect(ui.verbatim_plan, SIGNAL(toggled(bool)), plannerModel, SLOT(setVerbatim(bool))); + connect(ui.display_duration, SIGNAL(toggled(bool)), plannerModel, SLOT(setDisplayDuration(bool))); + connect(ui.display_runtime, SIGNAL(toggled(bool)), plannerModel, SLOT(setDisplayRuntime(bool))); + connect(ui.display_transitions, SIGNAL(toggled(bool)), plannerModel, SLOT(setDisplayTransitions(bool))); + connect(ui.safetystop, SIGNAL(toggled(bool)), plannerModel, SLOT(setSafetyStop(bool))); + connect(ui.reserve_gas, SIGNAL(valueChanged(int)), plannerModel, SLOT(setReserveGas(int))); + connect(ui.ascRate75, SIGNAL(valueChanged(int)), this, SLOT(setAscRate75(int))); + connect(ui.ascRate75, SIGNAL(valueChanged(int)), plannerModel, SLOT(emitDataChanged())); + connect(ui.ascRate50, SIGNAL(valueChanged(int)), this, SLOT(setAscRate50(int))); + connect(ui.ascRate50, SIGNAL(valueChanged(int)), plannerModel, SLOT(emitDataChanged())); + connect(ui.ascRateStops, SIGNAL(valueChanged(int)), this, SLOT(setAscRateStops(int))); + connect(ui.ascRateStops, SIGNAL(valueChanged(int)), plannerModel, SLOT(emitDataChanged())); + connect(ui.ascRateLast6m, SIGNAL(valueChanged(int)), this, SLOT(setAscRateLast6m(int))); + connect(ui.ascRateLast6m, SIGNAL(valueChanged(int)), plannerModel, SLOT(emitDataChanged())); + connect(ui.descRate, SIGNAL(valueChanged(int)), this, SLOT(setDescRate(int))); + connect(ui.descRate, SIGNAL(valueChanged(int)), plannerModel, SLOT(emitDataChanged())); + connect(ui.bottompo2, SIGNAL(valueChanged(double)), this, SLOT(setBottomPo2(double))); + connect(ui.decopo2, SIGNAL(valueChanged(double)), this, SLOT(setDecoPo2(double))); + connect(ui.drop_stone_mode, SIGNAL(toggled(bool)), plannerModel, SLOT(setDropStoneMode(bool))); + connect(ui.bottomSAC, SIGNAL(valueChanged(double)), this, SLOT(bottomSacChanged(double))); + connect(ui.decoStopSAC, SIGNAL(valueChanged(double)), this, SLOT(decoSacChanged(double))); + connect(ui.gfhigh, SIGNAL(valueChanged(int)), plannerModel, SLOT(setGFHigh(int))); + connect(ui.gflow, SIGNAL(valueChanged(int)), plannerModel, SLOT(setGFLow(int))); + connect(ui.gfhigh, SIGNAL(editingFinished()), plannerModel, SLOT(triggerGFHigh())); + connect(ui.gflow, SIGNAL(editingFinished()), plannerModel, SLOT(triggerGFLow())); + connect(ui.conservatism_lvl, SIGNAL(valueChanged(int)), plannerModel, SLOT(setConservatism(int))); + connect(ui.backgasBreaks, SIGNAL(toggled(bool)), this, SLOT(setBackgasBreaks(bool))); + connect(ui.switch_at_req_stop, SIGNAL(toggled(bool)), plannerModel, SLOT(setSwitchAtReqStop(bool))); + connect(ui.min_switch_duration, SIGNAL(valueChanged(int)), plannerModel, SLOT(setMinSwitchDuration(int))); + connect(ui.rebreathermode, SIGNAL(currentIndexChanged(int)), plannerModel, SLOT(setRebreatherMode(int))); + connect(modeMapper, SIGNAL(mapped(int)), this, SLOT(disableDecoElements(int))); + + settingsChanged(); + ui.gflow->setValue(prefs.gflow); + ui.gfhigh->setValue(prefs.gfhigh); + + setMinimumWidth(0); + setMinimumHeight(0); +} + +void PlannerSettingsWidget::updateUnitsUI() +{ + ui.ascRate75->setValue(rint(prefs.ascrate75 / UNIT_FACTOR)); + ui.ascRate50->setValue(rint(prefs.ascrate50 / UNIT_FACTOR)); + ui.ascRateStops->setValue(rint(prefs.ascratestops / UNIT_FACTOR)); + ui.ascRateLast6m->setValue(rint(prefs.ascratelast6m / UNIT_FACTOR)); + ui.descRate->setValue(rint(prefs.descrate / UNIT_FACTOR)); +} + +PlannerSettingsWidget::~PlannerSettingsWidget() +{ + QSettings s; + s.beginGroup("Planner"); + s.setValue("last_stop", prefs.last_stop); + s.setValue("verbatim_plan", prefs.verbatim_plan); + s.setValue("display_duration", prefs.display_duration); + s.setValue("display_runtime", prefs.display_runtime); + s.setValue("display_transitions", prefs.display_transitions); + s.setValue("safetystop", prefs.safetystop); + s.setValue("reserve_gas", prefs.reserve_gas); + s.setValue("ascrate75", prefs.ascrate75); + s.setValue("ascrate50", prefs.ascrate50); + s.setValue("ascratestops", prefs.ascratestops); + s.setValue("ascratelast6m", prefs.ascratelast6m); + s.setValue("descrate", prefs.descrate); + s.setValue("bottompo2", prefs.bottompo2); + s.setValue("decopo2", prefs.decopo2); + s.setValue("doo2breaks", prefs.doo2breaks); + s.setValue("drop_stone_mode", prefs.drop_stone_mode); + s.setValue("switch_at_req_stop", prefs.switch_at_req_stop); + s.setValue("min_switch_duration", prefs.min_switch_duration); + s.setValue("bottomsac", prefs.bottomsac); + s.setValue("decosac", prefs.decosac); + s.setValue("deco_mode", int(prefs.deco_mode)); + s.setValue("conservatism", prefs.conservatism_level); + s.endGroup(); +} + +void PlannerSettingsWidget::settingsChanged() +{ + QString vs; + // don't recurse into setting the value from the ui when setting the ui from the value + ui.bottomSAC->blockSignals(true); + ui.decoStopSAC->blockSignals(true); + if (get_units()->length == units::FEET) { + vs.append(tr("ft/min")); + ui.lastStop->setText(tr("Last stop at 20ft")); + ui.asc50to6->setText(tr("50% avg. depth to 20ft")); + ui.asc6toSurf->setText(tr("20ft to surface")); + } else { + vs.append(tr("m/min")); + ui.lastStop->setText(tr("Last stop at 6m")); + ui.asc50to6->setText(tr("50% avg. depth to 6m")); + ui.asc6toSurf->setText(tr("6m to surface")); + } + if(get_units()->volume == units::CUFT) { + ui.bottomSAC->setSuffix(tr("cuft/min")); + ui.decoStopSAC->setSuffix(tr("cuft/min")); + ui.bottomSAC->setDecimals(2); + ui.bottomSAC->setSingleStep(0.1); + ui.decoStopSAC->setDecimals(2); + ui.decoStopSAC->setSingleStep(0.1); + ui.bottomSAC->setValue(ml_to_cuft(prefs.bottomsac)); + ui.decoStopSAC->setValue(ml_to_cuft(prefs.decosac)); + } else { + ui.bottomSAC->setSuffix(tr("â„“/min")); + ui.decoStopSAC->setSuffix(tr("â„“/min")); + ui.bottomSAC->setDecimals(0); + ui.bottomSAC->setSingleStep(1); + ui.decoStopSAC->setDecimals(0); + ui.decoStopSAC->setSingleStep(1); + ui.bottomSAC->setValue((double) prefs.bottomsac / 1000.0); + ui.decoStopSAC->setValue((double) prefs.decosac / 1000.0); + } + ui.bottomSAC->blockSignals(false); + ui.decoStopSAC->blockSignals(false); + updateUnitsUI(); + ui.ascRate75->setSuffix(vs); + ui.ascRate50->setSuffix(vs); + ui.ascRateStops->setSuffix(vs); + ui.ascRateLast6m->setSuffix(vs); + ui.descRate->setSuffix(vs); +} + +void PlannerSettingsWidget::atmPressureChanged(const QString &pressure) +{ +} + +void PlannerSettingsWidget::printDecoPlan() +{ +} + +void PlannerSettingsWidget::setAscRate75(int rate) +{ + prefs.ascrate75 = rate * UNIT_FACTOR; +} + +void PlannerSettingsWidget::setAscRate50(int rate) +{ + prefs.ascrate50 = rate * UNIT_FACTOR; +} + +void PlannerSettingsWidget::setAscRateStops(int rate) +{ + prefs.ascratestops = rate * UNIT_FACTOR; +} + +void PlannerSettingsWidget::setAscRateLast6m(int rate) +{ + prefs.ascratelast6m = rate * UNIT_FACTOR; +} + +void PlannerSettingsWidget::setDescRate(int rate) +{ + prefs.descrate = rate * UNIT_FACTOR; +} + +void PlannerSettingsWidget::setBottomPo2(double po2) +{ + prefs.bottompo2 = (int) (po2 * 1000.0); +} + +void PlannerSettingsWidget::setDecoPo2(double po2) +{ + prefs.decopo2 = (int) (po2 * 1000.0); +} + +void PlannerSettingsWidget::setBackgasBreaks(bool dobreaks) +{ + prefs.doo2breaks = dobreaks; + plannerModel->emitDataChanged(); +} + +PlannerDetails::PlannerDetails(QWidget *parent) : QWidget(parent) +{ + ui.setupUi(this); +} diff --git a/desktop-widgets/diveplanner.h b/desktop-widgets/diveplanner.h new file mode 100644 index 000000000..b2e03a97b --- /dev/null +++ b/desktop-widgets/diveplanner.h @@ -0,0 +1,104 @@ +#ifndef DIVEPLANNER_H +#define DIVEPLANNER_H + +#include <QGraphicsPathItem> +#include <QAbstractTableModel> +#include <QAbstractButton> +#include <QDateTime> +#include <QSignalMapper> + +#include "dive.h" + +class QListView; +class QModelIndex; +class DivePlannerPointsModel; + +class DiveHandler : public QObject, public QGraphicsEllipseItem { + Q_OBJECT +public: + DiveHandler(); + +protected: + void contextMenuEvent(QGraphicsSceneContextMenuEvent *event); + void mouseMoveEvent(QGraphicsSceneMouseEvent *event); + void mousePressEvent(QGraphicsSceneMouseEvent *event); + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event); +signals: + void moved(); + void clicked(); + void released(); +private: + int parentIndex(); +public +slots: + void selfRemove(); + void changeGas(); +private: + QTime t; +}; + +#include "ui_diveplanner.h" + +class DivePlannerWidget : public QWidget { + Q_OBJECT +public: + explicit DivePlannerWidget(QWidget *parent = 0, Qt::WindowFlags f = 0); + void setReplanButton(bool replan); +public +slots: + void setupStartTime(QDateTime startTime); + void settingsChanged(); + void atmPressureChanged(const int pressure); + void heightChanged(const int height); + void salinityChanged(const double salinity); + void printDecoPlan(); + +private: + Ui::DivePlanner ui; + QAbstractButton *replanButton; +}; + +#include "ui_plannerSettings.h" + +class PlannerSettingsWidget : public QWidget { + Q_OBJECT +public: + explicit PlannerSettingsWidget(QWidget *parent = 0, Qt::WindowFlags f = 0); + virtual ~PlannerSettingsWidget(); +public +slots: + void settingsChanged(); + void atmPressureChanged(const QString &pressure); + void bottomSacChanged(const double bottomSac); + void decoSacChanged(const double decosac); + void printDecoPlan(); + void setAscRate75(int rate); + void setAscRate50(int rate); + void setAscRateStops(int rate); + void setAscRateLast6m(int rate); + void setDescRate(int rate); + void setBottomPo2(double po2); + void setDecoPo2(double po2); + void setBackgasBreaks(bool dobreaks); + void disableDecoElements(int mode); + +private: + Ui::plannerSettingsWidget ui; + void updateUnitsUI(); + QSignalMapper *modeMapper; +}; + +#include "ui_plannerDetails.h" + +class PlannerDetails : public QWidget { + Q_OBJECT +public: + explicit PlannerDetails(QWidget *parent = 0); + QPushButton *printPlan() const { return ui.printPlan; } + QTextEdit *divePlanOutput() const { return ui.divePlanOutput; } + +private: + Ui::plannerDetails ui; +}; + +#endif // DIVEPLANNER_H diff --git a/desktop-widgets/diveplanner.ui b/desktop-widgets/diveplanner.ui new file mode 100644 index 000000000..adb44fad9 --- /dev/null +++ b/desktop-widgets/diveplanner.ui @@ -0,0 +1,257 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>DivePlanner</class> + <widget class="QWidget" name="DivePlanner"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>515</width> + <height>591</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="topMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>5</number> + </property> + <property name="bottomMargin"> + <number>5</number> + </property> + <property name="spacing"> + <number>0</number> + </property> + <item row="6" column="1"> + <widget class="QScrollArea" name="scrollArea"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Plain</enum> + </property> + <property name="widgetResizable"> + <bool>true</bool> + </property> + <widget class="QWidget" name="scrollAreaWidgetContents"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>505</width> + <height>581</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <property name="spacing"> + <number>2</number> + </property> + <item row="5" column="0" colspan="3"> + <widget class="TableView" name="tableWidget" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>50</height> + </size> + </property> + </widget> + </item> + <item row="4" column="0" colspan="3"> + <widget class="TableView" name="cylinderTableWidget" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>50</height> + </size> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Planned dive time</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QDateEdit" name="dateEdit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="calendarPopup"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QTimeEdit" name="startTime"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>Altitude</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>ATM pressure</string> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Salinity</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QSpinBox" name="ATMPressure"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="suffix"> + <string>mbar</string> + </property> + <property name="minimum"> + <number>689</number> + </property> + <property name="maximum"> + <number>1100</number> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QSpinBox" name="atmHeight"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="suffix"> + <string>m</string> + </property> + <property name="minimum"> + <number>-100</number> + </property> + <property name="maximum"> + <number>3000</number> + </property> + <property name="singleStep"> + <number>10</number> + </property> + </widget> + </item> + <item row="3" column="2"> + <widget class="QDoubleSpinBox" name="salinity"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="suffix"> + <string> kg/â„“</string> + </property> + <property name="minimum"> + <double>1.000000000000000</double> + </property> + <property name="maximum"> + <double>1.050000000000000</double> + </property> + <property name="singleStep"> + <double>0.010000000000000</double> + </property> + <property name="value"> + <double>1.030000000000000</double> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>TableView</class> + <extends>QWidget</extends> + <header>tableview.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>startTime</tabstop> + <tabstop>buttonBox</tabstop> + <tabstop>scrollArea</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/desktop-widgets/diveshareexportdialog.cpp b/desktop-widgets/diveshareexportdialog.cpp new file mode 100644 index 000000000..9fe6eefd6 --- /dev/null +++ b/desktop-widgets/diveshareexportdialog.cpp @@ -0,0 +1,141 @@ +#include "diveshareexportdialog.h" +#include "ui_diveshareexportdialog.h" +#include "mainwindow.h" +#include "save-html.h" +#include "subsurfacewebservices.h" +#include "helpers.h" + +#include <QDesktopServices> +#include <QSettings> + +DiveShareExportDialog::DiveShareExportDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::DiveShareExportDialog), + reply(NULL), + exportSelected(false) +{ + ui->setupUi(this); +} + +DiveShareExportDialog::~DiveShareExportDialog() +{ + delete ui; +} + +void DiveShareExportDialog::UIDFromBrowser() +{ + QDesktopServices::openUrl(QUrl(DIVESHARE_BASE_URI "/secret")); +} + +DiveShareExportDialog *DiveShareExportDialog::instance() +{ + static DiveShareExportDialog *self = new DiveShareExportDialog(MainWindow::instance()); + self->setAttribute(Qt::WA_QuitOnClose, false); + self->ui->txtResult->setHtml(""); + self->ui->buttonBox->setStandardButtons(QDialogButtonBox::Cancel); + return self; +} + +void DiveShareExportDialog::prepareDivesForUpload(bool selected) +{ + exportSelected = selected; + ui->frameConfigure->setVisible(true); + ui->frameResults->setVisible(false); + + QSettings settings; + if (settings.contains("diveshareExport/uid")) + ui->txtUID->setText(settings.value("diveshareExport/uid").toString()); + + if (settings.contains("diveshareExport/private")) + ui->chkPrivate->setChecked(settings.value("diveshareExport/private").toBool()); + + show(); +} + +static QByteArray generate_html_list(const QByteArray &data) +{ + QList<QByteArray> dives = data.split('\n'); + QByteArray html; + html.append("<html><body><table>"); + for (int i = 0; i < dives.length(); i++ ) { + html.append("<tr>"); + QList<QByteArray> dive_details = dives[i].split(','); + if (dive_details.length() < 3) + continue; + + QByteArray dive_id = dive_details[0]; + QByteArray dive_delete = dive_details[1]; + + html.append("<td>"); + html.append("<a href=\"" DIVESHARE_BASE_URI "/dive/" + dive_id + "\">"); + + //Title gets separated too, this puts it back together + const char *sep = ""; + for (int t = 2; t < dive_details.length(); t++) { + html.append(sep); + html.append(dive_details[t]); + sep = ","; + } + + html.append("</a>"); + html.append("</td>"); + html.append("<td>"); + html.append("<a href=\"" DIVESHARE_BASE_URI "/delete/dive/" + dive_delete + "\">Delete dive</a>"); + html.append("</td>" ); + + html.append("</tr>"); + } + + html.append("</table></body></html>"); + return html; +} + +void DiveShareExportDialog::finishedSlot() +{ + ui->progressBar->setVisible(false); + if (reply->error() != 0) { + ui->buttonBox->setStandardButtons(QDialogButtonBox::Cancel); + ui->txtResult->setText(reply->errorString()); + } else { + ui->buttonBox->setStandardButtons(QDialogButtonBox::Ok); + ui->txtResult->setHtml(generate_html_list(reply->readAll())); + } + + reply->deleteLater(); +} + +void DiveShareExportDialog::doUpload() +{ + //Store current settings + QSettings settings; + settings.setValue("diveshareExport/uid", ui->txtUID->text()); + settings.setValue("diveshareExport/private", ui->chkPrivate->isChecked()); + + //Change UI into results mode + ui->frameConfigure->setVisible(false); + ui->frameResults->setVisible(true); + ui->progressBar->setVisible(true); + ui->progressBar->setRange(0, 0); + + //generate json + struct membuffer buf = { 0 }; + export_list(&buf, NULL, exportSelected, false); + QByteArray json_data(buf.buffer, buf.len); + free_buffer(&buf); + + //Request to server + QNetworkRequest request; + + if (ui->chkPrivate->isChecked()) + request.setUrl(QUrl(DIVESHARE_BASE_URI "/upload?private=true")); + else + request.setUrl(QUrl(DIVESHARE_BASE_URI "/upload")); + + request.setRawHeader("User-Agent", getUserAgent().toUtf8()); + if (ui->txtUID->text().length() != 0) + request.setRawHeader("X-UID", ui->txtUID->text().toUtf8()); + + reply = WebServices::manager()->put(request, json_data); + + QObject::connect(reply, SIGNAL(finished()), this, SLOT(finishedSlot())); +} diff --git a/desktop-widgets/diveshareexportdialog.h b/desktop-widgets/diveshareexportdialog.h new file mode 100644 index 000000000..85dadf5f1 --- /dev/null +++ b/desktop-widgets/diveshareexportdialog.h @@ -0,0 +1,34 @@ +#ifndef DIVESHAREEXPORTDIALOG_H +#define DIVESHAREEXPORTDIALOG_H + +#include <QDialog> +#include <QNetworkReply> +#include <QNetworkAccessManager> + +#define DIVESHARE_WEBSITE "dive-share.appspot.com" +#define DIVESHARE_BASE_URI "http://" DIVESHARE_WEBSITE + +namespace Ui { +class DiveShareExportDialog; +} + +class DiveShareExportDialog : public QDialog +{ + Q_OBJECT +public: + explicit DiveShareExportDialog(QWidget *parent = 0); + ~DiveShareExportDialog(); + static DiveShareExportDialog *instance(); + void prepareDivesForUpload(bool); +private: + Ui::DiveShareExportDialog *ui; + bool exportSelected; + QNetworkReply *reply; +private +slots: + void UIDFromBrowser(); + void doUpload(); + void finishedSlot(); +}; + +#endif // DIVESHAREEXPORTDIALOG_H diff --git a/desktop-widgets/diveshareexportdialog.ui b/desktop-widgets/diveshareexportdialog.ui new file mode 100644 index 000000000..2235740c8 --- /dev/null +++ b/desktop-widgets/diveshareexportdialog.ui @@ -0,0 +1,291 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>DiveShareExportDialog</class> + <widget class="QDialog" name="DiveShareExportDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>593</width> + <height>420</height> + </rect> + </property> + <property name="windowTitle"> + <string>Dialog</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="topMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>5</number> + </property> + <property name="bottomMargin"> + <number>5</number> + </property> + <item> + <widget class="QFrame" name="frameConfigure"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Plain</enum> + </property> + <property name="lineWidth"> + <number>0</number> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>2</number> + </property> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>User ID</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="txtUID"> + <property name="toolTip"> + <string/> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="cmdClear"> + <property name="text"> + <string>⌫</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="getUIDbutton"> + <property name="text"> + <string>Get user ID</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string><html><head/><body><p><span style=" font-size:20pt; font-weight:600; color:#ff8000;">âš </span> Not using a UserID means that you will need to manually keep bookmarks to your dives, to find them again.</p></body></html></string> + </property> + <property name="textFormat"> + <enum>Qt::AutoText</enum> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="chkPrivate"> + <property name="toolTip"> + <string>Private dives will not appear in "related dives" lists, and will only be accessible if their URL is known.</string> + </property> + <property name="text"> + <string>Keep dives private</string> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="doUploadButton"> + <property name="text"> + <string>Upload dive data</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + <property name="default"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QFrame" name="frameResults"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <property name="spacing"> + <number>2</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QTextBrowser" name="txtResult"> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="html"> + <string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Oxygen-Sans'; font-size:7pt; font-weight:600; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html></string> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QProgressBar" name="progressBar"> + <property name="value"> + <number>24</number> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>68</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>DiveShareExportDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>97</x> + <y>299</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>252</y> + </hint> + </hints> + </connection> + <connection> + <sender>getUIDbutton</sender> + <signal>clicked()</signal> + <receiver>DiveShareExportDialog</receiver> + <slot>UIDFromBrowser()</slot> + <hints> + <hint type="sourcelabel"> + <x>223</x> + <y>29</y> + </hint> + <hint type="destinationlabel"> + <x>159</x> + <y>215</y> + </hint> + </hints> + </connection> + <connection> + <sender>doUploadButton</sender> + <signal>clicked()</signal> + <receiver>DiveShareExportDialog</receiver> + <slot>doUpload()</slot> + <hints> + <hint type="sourcelabel"> + <x>223</x> + <y>120</y> + </hint> + <hint type="destinationlabel"> + <x>159</x> + <y>215</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>DiveShareExportDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>159</x> + <y>288</y> + </hint> + <hint type="destinationlabel"> + <x>159</x> + <y>154</y> + </hint> + </hints> + </connection> + </connections> + <slots> + <slot>getUID()</slot> + <slot>doUpload()</slot> + </slots> +</ui> diff --git a/desktop-widgets/downloadfromdivecomputer.cpp b/desktop-widgets/downloadfromdivecomputer.cpp new file mode 100644 index 000000000..4c8fa6b4a --- /dev/null +++ b/desktop-widgets/downloadfromdivecomputer.cpp @@ -0,0 +1,727 @@ +#include "downloadfromdivecomputer.h" +#include "helpers.h" +#include "mainwindow.h" +#include "divelistview.h" +#include "display.h" +#include "uemis.h" +#include "models.h" + +#include <QTimer> +#include <QFileDialog> +#include <QMessageBox> +#include <QShortcut> + +struct product { + const char *product; + dc_descriptor_t *descriptor; + struct product *next; +}; + +struct vendor { + const char *vendor; + struct product *productlist; + struct vendor *next; +}; + +struct mydescriptor { + const char *vendor; + const char *product; + dc_family_t type; + unsigned int model; +}; + +namespace DownloadFromDcGlobal { + const char *err_string; +}; + +struct dive_table downloadTable; + +DownloadFromDCWidget::DownloadFromDCWidget(QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f), + thread(0), + downloading(false), + previousLast(0), + vendorModel(0), + productModel(0), + timer(new QTimer(this)), + dumpWarningShown(false), + ostcFirmwareCheck(0), + currentState(INITIAL) +{ + clear_table(&downloadTable); + ui.setupUi(this); + ui.progressBar->hide(); + ui.progressBar->setMinimum(0); + ui.progressBar->setMaximum(100); + diveImportedModel = new DiveImportedModel(this); + ui.downloadedView->setModel(diveImportedModel); + ui.downloadedView->setSelectionBehavior(QAbstractItemView::SelectRows); + ui.downloadedView->setSelectionMode(QAbstractItemView::SingleSelection); + int startingWidth = defaultModelFont().pointSize(); + ui.downloadedView->setColumnWidth(0, startingWidth * 20); + ui.downloadedView->setColumnWidth(1, startingWidth * 10); + ui.downloadedView->setColumnWidth(2, startingWidth * 10); + connect(ui.downloadedView, SIGNAL(clicked(QModelIndex)), diveImportedModel, SLOT(changeSelected(QModelIndex))); + + progress_bar_text = ""; + + fill_computer_list(); + + ui.chooseDumpFile->setEnabled(ui.dumpToFile->isChecked()); + connect(ui.chooseDumpFile, SIGNAL(clicked()), this, SLOT(pickDumpFile())); + connect(ui.dumpToFile, SIGNAL(stateChanged(int)), this, SLOT(checkDumpFile(int))); + ui.chooseLogFile->setEnabled(ui.logToFile->isChecked()); + connect(ui.chooseLogFile, SIGNAL(clicked()), this, SLOT(pickLogFile())); + connect(ui.logToFile, SIGNAL(stateChanged(int)), this, SLOT(checkLogFile(int))); + ui.selectAllButton->setEnabled(false); + ui.unselectAllButton->setEnabled(false); + connect(ui.selectAllButton, SIGNAL(clicked()), diveImportedModel, SLOT(selectAll())); + connect(ui.unselectAllButton, SIGNAL(clicked()), diveImportedModel, SLOT(selectNone())); + vendorModel = new QStringListModel(vendorList); + ui.vendor->setModel(vendorModel); + if (default_dive_computer_vendor) { + ui.vendor->setCurrentIndex(ui.vendor->findText(default_dive_computer_vendor)); + productModel = new QStringListModel(productList[default_dive_computer_vendor]); + ui.product->setModel(productModel); + if (default_dive_computer_product) + ui.product->setCurrentIndex(ui.product->findText(default_dive_computer_product)); + } + if (default_dive_computer_device) + ui.device->setEditText(default_dive_computer_device); + + timer->setInterval(200); + connect(timer, SIGNAL(timeout()), this, SLOT(updateProgressBar())); + updateState(INITIAL); + memset(&data, 0, sizeof(data)); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); + ui.ok->setEnabled(false); + ui.downloadCancelRetryButton->setEnabled(true); + ui.downloadCancelRetryButton->setText(tr("Download")); + +#if defined(BT_SUPPORT) && defined(SSRF_CUSTOM_SERIAL) + ui.bluetoothMode->setText(tr("Choose Bluetooth download mode")); + ui.bluetoothMode->setChecked(default_dive_computer_download_mode == DC_TRANSPORT_BLUETOOTH); + btDeviceSelectionDialog = 0; + ui.chooseBluetoothDevice->setEnabled(ui.bluetoothMode->isChecked()); + connect(ui.bluetoothMode, SIGNAL(stateChanged(int)), this, SLOT(enableBluetoothMode(int))); + connect(ui.chooseBluetoothDevice, SIGNAL(clicked()), this, SLOT(selectRemoteBluetoothDevice())); +#else + ui.bluetoothMode->hide(); + ui.chooseBluetoothDevice->hide(); +#endif +} + +void DownloadFromDCWidget::updateProgressBar() +{ + if (*progress_bar_text != '\0') { + ui.progressBar->setFormat(progress_bar_text); + } else { + ui.progressBar->setFormat("%p%"); + } + ui.progressBar->setValue(progress_bar_fraction * 100); +} + +void DownloadFromDCWidget::updateState(states state) +{ + if (state == currentState) + return; + + if (state == INITIAL) { + fill_device_list(DC_TYPE_OTHER); + ui.progressBar->hide(); + markChildrenAsEnabled(); + timer->stop(); + } + + // tries to cancel an on going download + else if (currentState == DOWNLOADING && state == CANCELLING) { + import_thread_cancelled = true; + ui.downloadCancelRetryButton->setEnabled(false); + } + + // user pressed cancel but the application isn't doing anything. + // means close the window + else if ((currentState == INITIAL || currentState == DONE || currentState == ERROR) && state == CANCELLING) { + timer->stop(); + reject(); + } + + // the cancelation process is finished + else if (currentState == CANCELLING && state == DONE) { + timer->stop(); + ui.progressBar->setValue(0); + ui.progressBar->hide(); + markChildrenAsEnabled(); + } + + // DOWNLOAD is finally done, but we don't know if there was an error as libdivecomputer doesn't pass + // that information on to us. + // If we find an error, offer to retry, otherwise continue the interaction to pick the dives the user wants + else if (currentState == DOWNLOADING && state == DONE) { + timer->stop(); + if (QString(progress_bar_text).contains("error", Qt::CaseInsensitive)) { + updateProgressBar(); + markChildrenAsEnabled(); + progress_bar_text = ""; + } else { + ui.progressBar->setValue(100); + markChildrenAsEnabled(); + } + } + + // DOWNLOAD is started. + else if (state == DOWNLOADING) { + timer->start(); + ui.progressBar->setValue(0); + updateProgressBar(); + ui.progressBar->show(); + markChildrenAsDisabled(); + } + + // got an error + else if (state == ERROR) { + QMessageBox::critical(this, TITLE_OR_TEXT(tr("Error"), this->thread->error), QMessageBox::Ok); + + markChildrenAsEnabled(); + ui.progressBar->hide(); + } + + // properly updating the widget state + currentState = state; +} + +void DownloadFromDCWidget::on_vendor_currentIndexChanged(const QString &vendor) +{ + int dcType = DC_TYPE_SERIAL; + QAbstractItemModel *currentModel = ui.product->model(); + if (!currentModel) + return; + + productModel = new QStringListModel(productList[vendor]); + ui.product->setModel(productModel); + + if (vendor == QString("Uemis")) + dcType = DC_TYPE_UEMIS; + fill_device_list(dcType); + + // Memleak - but deleting gives me a crash. + //currentModel->deleteLater(); +} + +void DownloadFromDCWidget::on_product_currentIndexChanged(const QString &product) +{ + // Set up the DC descriptor + dc_descriptor_t *descriptor = NULL; + descriptor = descriptorLookup[ui.vendor->currentText() + product]; + + // call dc_descriptor_get_transport to see if the dc_transport_t is DC_TRANSPORT_SERIAL + if (dc_descriptor_get_transport(descriptor) == DC_TRANSPORT_SERIAL) { + // if the dc_transport_t is DC_TRANSPORT_SERIAL, then enable the device node box. + ui.device->setEnabled(true); + } else { + // otherwise disable the device node box + ui.device->setEnabled(false); + } +} + +void DownloadFromDCWidget::fill_computer_list() +{ + dc_iterator_t *iterator = NULL; + dc_descriptor_t *descriptor = NULL; + struct mydescriptor *mydescriptor; + + QStringList computer; + dc_descriptor_iterator(&iterator); + while (dc_iterator_next(iterator, &descriptor) == DC_STATUS_SUCCESS) { + const char *vendor = dc_descriptor_get_vendor(descriptor); + const char *product = dc_descriptor_get_product(descriptor); + + if (!vendorList.contains(vendor)) + vendorList.append(vendor); + + if (!productList[vendor].contains(product)) + productList[vendor].push_back(product); + + descriptorLookup[QString(vendor) + QString(product)] = descriptor; + } + dc_iterator_free(iterator); + Q_FOREACH (QString vendor, vendorList) + qSort(productList[vendor]); + + /* and add the Uemis Zurich which we are handling internally + THIS IS A HACK as we magically have a data structure here that + happens to match a data structure that is internal to libdivecomputer; + this WILL BREAK if libdivecomputer changes the dc_descriptor struct... + eventually the UEMIS code needs to move into libdivecomputer, I guess */ + + mydescriptor = (struct mydescriptor *)malloc(sizeof(struct mydescriptor)); + mydescriptor->vendor = "Uemis"; + mydescriptor->product = "Zurich"; + mydescriptor->type = DC_FAMILY_NULL; + mydescriptor->model = 0; + + if (!vendorList.contains("Uemis")) + vendorList.append("Uemis"); + + if (!productList["Uemis"].contains("Zurich")) + productList["Uemis"].push_back("Zurich"); + + descriptorLookup["UemisZurich"] = (dc_descriptor_t *)mydescriptor; + + qSort(vendorList); +} + +void DownloadFromDCWidget::on_search_clicked() +{ + if (ui.vendor->currentText() == "Uemis") { + QString dirName = QFileDialog::getExistingDirectory(this, + tr("Find Uemis dive computer"), + QDir::homePath(), + QFileDialog::ShowDirsOnly); + if (ui.device->findText(dirName) == -1) + ui.device->addItem(dirName); + ui.device->setEditText(dirName); + } +} + +void DownloadFromDCWidget::on_downloadCancelRetryButton_clicked() +{ + if (currentState == DOWNLOADING) { + updateState(CANCELLING); + return; + } + if (currentState == DONE) { + // this means we are retrying - so we better clean out the partial + // list of downloaded dives from the last attempt + diveImportedModel->clearTable(); + clear_table(&downloadTable); + } + updateState(DOWNLOADING); + + // you cannot cancel the dialog, just the download + ui.cancel->setEnabled(false); + ui.downloadCancelRetryButton->setText("Cancel download"); + + // I don't really think that create/destroy the thread + // is really necessary. + if (thread) { + thread->deleteLater(); + } + + data.vendor = strdup(ui.vendor->currentText().toUtf8().data()); + data.product = strdup(ui.product->currentText().toUtf8().data()); +#if defined(BT_SUPPORT) + data.bluetooth_mode = ui.bluetoothMode->isChecked(); + if (data.bluetooth_mode && btDeviceSelectionDialog != NULL) { + // Get the selected device address + data.devname = strdup(btDeviceSelectionDialog->getSelectedDeviceAddress().toUtf8().data()); + } else + // this breaks an "else if" across lines... not happy... +#endif + if (same_string(data.vendor, "Uemis")) { + char *colon; + char *devname = strdup(ui.device->currentText().toUtf8().data()); + + if ((colon = strstr(devname, ":\\ (UEMISSDA)")) != NULL) { + *(colon + 2) = '\0'; + fprintf(stderr, "shortened devname to \"%s\"", data.devname); + } + data.devname = devname; + } else { + data.devname = strdup(ui.device->currentText().toUtf8().data()); + } + data.descriptor = descriptorLookup[ui.vendor->currentText() + ui.product->currentText()]; + data.force_download = ui.forceDownload->isChecked(); + data.create_new_trip = ui.createNewTrip->isChecked(); + data.trip = NULL; + data.deviceid = data.diveid = 0; + set_default_dive_computer(data.vendor, data.product); + set_default_dive_computer_device(data.devname); +#if defined(BT_SUPPORT) && defined(SSRF_CUSTOM_SERIAL) + set_default_dive_computer_download_mode(ui.bluetoothMode->isChecked() ? DC_TRANSPORT_BLUETOOTH : DC_TRANSPORT_SERIAL); +#endif + thread = new DownloadThread(this, &data); + + connect(thread, SIGNAL(finished()), + this, SLOT(onDownloadThreadFinished()), Qt::QueuedConnection); + + MainWindow *w = MainWindow::instance(); + connect(thread, SIGNAL(finished()), w, SLOT(refreshDisplay())); + + // before we start, remember where the dive_table ended + previousLast = dive_table.nr; + + thread->start(); + + QString product(ui.product->currentText()); + if (product == "OSTC 3" || product == "OSTC Sport") + ostcFirmwareCheck = new OstcFirmwareCheck(product); +} + +bool DownloadFromDCWidget::preferDownloaded() +{ + return ui.preferDownloaded->isChecked(); +} + +void DownloadFromDCWidget::checkLogFile(int state) +{ + ui.chooseLogFile->setEnabled(state == Qt::Checked); + data.libdc_log = (state == Qt::Checked); + if (state == Qt::Checked && logFile.isEmpty()) { + pickLogFile(); + } +} + +void DownloadFromDCWidget::pickLogFile() +{ + QString filename = existing_filename ?: prefs.default_filename; + QFileInfo fi(filename); + filename = fi.absolutePath().append(QDir::separator()).append("subsurface.log"); + logFile = QFileDialog::getSaveFileName(this, tr("Choose file for divecomputer download logfile"), + filename, tr("Log files (*.log)")); + if (!logFile.isEmpty()) { + free(logfile_name); + logfile_name = strdup(logFile.toUtf8().data()); + } +} + +void DownloadFromDCWidget::checkDumpFile(int state) +{ + ui.chooseDumpFile->setEnabled(state == Qt::Checked); + data.libdc_dump = (state == Qt::Checked); + if (state == Qt::Checked) { + if (dumpFile.isEmpty()) + pickDumpFile(); + if (!dumpWarningShown) { + QMessageBox::warning(this, tr("Warning"), + tr("Saving the libdivecomputer dump will NOT download dives to the dive list.")); + dumpWarningShown = true; + } + } +} + +void DownloadFromDCWidget::pickDumpFile() +{ + QString filename = existing_filename ?: prefs.default_filename; + QFileInfo fi(filename); + filename = fi.absolutePath().append(QDir::separator()).append("subsurface.bin"); + dumpFile = QFileDialog::getSaveFileName(this, tr("Choose file for divecomputer binary dump file"), + filename, tr("Dump files (*.bin)")); + if (!dumpFile.isEmpty()) { + free(dumpfile_name); + dumpfile_name = strdup(dumpFile.toUtf8().data()); + } +} + +void DownloadFromDCWidget::reject() +{ + // we don't want the download window being able to close + // while we're still downloading. + if (currentState != DOWNLOADING && currentState != CANCELLING) + QDialog::reject(); +} + +void DownloadFromDCWidget::onDownloadThreadFinished() +{ + if (currentState == DOWNLOADING) { + if (thread->error.isEmpty()) + updateState(DONE); + else + updateState(ERROR); + } else if (currentState == CANCELLING) { + updateState(DONE); + } + ui.downloadCancelRetryButton->setText(tr("Retry")); + ui.downloadCancelRetryButton->setEnabled(true); + // regardless, if we got dives, we should show them to the user + if (downloadTable.nr) { + diveImportedModel->setImportedDivesIndexes(0, downloadTable.nr - 1); + } + +} + +void DownloadFromDCWidget::on_cancel_clicked() +{ + if (currentState == DOWNLOADING || currentState == CANCELLING) + return; + + // now discard all the dives + clear_table(&downloadTable); + done(-1); +} + +void DownloadFromDCWidget::on_ok_clicked() +{ + struct dive *dive; + + if (currentState != DONE && currentState != ERROR) + return; + + // record all the dives in the 'real' dive_table + for (int i = 0; i < downloadTable.nr; i++) { + if (diveImportedModel->data(diveImportedModel->index(i, 0),Qt::CheckStateRole) == Qt::Checked) + record_dive(downloadTable.dives[i]); + downloadTable.dives[i] = NULL; + } + downloadTable.nr = 0; + + int uniqId, idx; + // remember the last downloaded dive (on most dive computers this will be the chronologically + // first new dive) and select it again after processing all the dives + MainWindow::instance()->dive_list()->unselectDives(); + + dive = get_dive(dive_table.nr - 1); + if (dive != NULL) { + uniqId = get_dive(dive_table.nr - 1)->id; + process_dives(true, preferDownloaded()); + // after process_dives does any merging or resorting needed, we need + // to recreate the model for the dive list so we can select the newest dive + MainWindow::instance()->recreateDiveList(); + idx = get_idx_by_uniq_id(uniqId); + // this shouldn't be necessary - but there are reports that somehow existing dives stay selected + // (but not visible as selected) + MainWindow::instance()->dive_list()->unselectDives(); + MainWindow::instance()->dive_list()->selectDive(idx, true); + } + + if (ostcFirmwareCheck && currentState == DONE) + ostcFirmwareCheck->checkLatest(this, &data); + accept(); +} + +void DownloadFromDCWidget::markChildrenAsDisabled() +{ + ui.device->setEnabled(false); + ui.vendor->setEnabled(false); + ui.product->setEnabled(false); + ui.forceDownload->setEnabled(false); + ui.createNewTrip->setEnabled(false); + ui.preferDownloaded->setEnabled(false); + ui.ok->setEnabled(false); + ui.search->setEnabled(false); + ui.logToFile->setEnabled(false); + ui.dumpToFile->setEnabled(false); + ui.chooseLogFile->setEnabled(false); + ui.chooseDumpFile->setEnabled(false); + ui.selectAllButton->setEnabled(false); + ui.unselectAllButton->setEnabled(false); + ui.bluetoothMode->setEnabled(false); + ui.chooseBluetoothDevice->setEnabled(false); +} + +void DownloadFromDCWidget::markChildrenAsEnabled() +{ + ui.device->setEnabled(true); + ui.vendor->setEnabled(true); + ui.product->setEnabled(true); + ui.forceDownload->setEnabled(true); + ui.createNewTrip->setEnabled(true); + ui.preferDownloaded->setEnabled(true); + ui.ok->setEnabled(true); + ui.cancel->setEnabled(true); + ui.search->setEnabled(true); + ui.logToFile->setEnabled(true); + ui.dumpToFile->setEnabled(true); + ui.chooseLogFile->setEnabled(true); + ui.chooseDumpFile->setEnabled(true); + ui.selectAllButton->setEnabled(true); + ui.unselectAllButton->setEnabled(true); +#if defined(BT_SUPPORT) + ui.bluetoothMode->setEnabled(true); + ui.chooseBluetoothDevice->setEnabled(true); +#endif +} + +#if defined(BT_SUPPORT) +void DownloadFromDCWidget::selectRemoteBluetoothDevice() +{ + if (!btDeviceSelectionDialog) { + btDeviceSelectionDialog = new BtDeviceSelectionDialog(this); + connect(btDeviceSelectionDialog, SIGNAL(finished(int)), + this, SLOT(bluetoothSelectionDialogIsFinished(int))); + } + + btDeviceSelectionDialog->show(); +} + +void DownloadFromDCWidget::bluetoothSelectionDialogIsFinished(int result) +{ + if (result == QDialog::Accepted) { + /* Make the selected Bluetooth device default */ + QString selectedDeviceName = btDeviceSelectionDialog->getSelectedDeviceName(); + + if (selectedDeviceName == NULL || selectedDeviceName.isEmpty()) { + ui.device->setCurrentText(btDeviceSelectionDialog->getSelectedDeviceAddress()); + } else { + ui.device->setCurrentText(selectedDeviceName); + } + } else if (result == QDialog::Rejected){ + /* Disable Bluetooth download mode */ + ui.bluetoothMode->setChecked(false); + } +} + +void DownloadFromDCWidget::enableBluetoothMode(int state) +{ + ui.chooseBluetoothDevice->setEnabled(state == Qt::Checked); + + if (state == Qt::Checked) + selectRemoteBluetoothDevice(); + else + ui.device->setCurrentIndex(-1); +} +#endif + +static void fillDeviceList(const char *name, void *data) +{ + QComboBox *comboBox = (QComboBox *)data; + comboBox->addItem(name); +} + +void DownloadFromDCWidget::fill_device_list(int dc_type) +{ + int deviceIndex; + ui.device->clear(); + deviceIndex = enumerate_devices(fillDeviceList, ui.device, dc_type); + if (deviceIndex >= 0) + ui.device->setCurrentIndex(deviceIndex); +} + +DownloadThread::DownloadThread(QObject *parent, device_data_t *data) : QThread(parent), + data(data) +{ +} + +static QString str_error(const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + const QString str = QString().vsprintf(fmt, args); + va_end(args); + + return str; +} + +void DownloadThread::run() +{ + const char *errorText; + import_thread_cancelled = false; + data->download_table = &downloadTable; + if (!strcmp(data->vendor, "Uemis")) + errorText = do_uemis_import(data); + else + errorText = do_libdivecomputer_import(data); + if (errorText) + error = str_error(errorText, data->devname, data->vendor, data->product); +} + +DiveImportedModel::DiveImportedModel(QObject *o) : QAbstractTableModel(o), + firstIndex(0), + lastIndex(-1), + checkStates(0) +{ +} + +int DiveImportedModel::columnCount(const QModelIndex &model) const +{ + return 3; +} + +int DiveImportedModel::rowCount(const QModelIndex &model) const +{ + return lastIndex - firstIndex + 1; +} + +QVariant DiveImportedModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Vertical) + return QVariant(); + if (role == Qt::DisplayRole) { + switch (section) { + case 0: + return QVariant(tr("Date/time")); + case 1: + return QVariant(tr("Duration")); + case 2: + return QVariant(tr("Depth")); + } + } + return QVariant(); +} + +QVariant DiveImportedModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() + firstIndex > lastIndex) + return QVariant(); + + struct dive *d = get_dive_from_table(index.row() + firstIndex, &downloadTable); + if (!d) + return QVariant(); + if (role == Qt::DisplayRole) { + switch (index.column()) { + case 0: + return QVariant(get_short_dive_date_string(d->when)); + case 1: + return QVariant(get_dive_duration_string(d->duration.seconds, tr("h:"), tr("min"))); + case 2: + return QVariant(get_depth_string(d->maxdepth.mm, true, false)); + } + } + if (role == Qt::CheckStateRole) { + if (index.column() == 0) + return checkStates[index.row()] ? Qt::Checked : Qt::Unchecked; + } + return QVariant(); +} + +void DiveImportedModel::changeSelected(QModelIndex clickedIndex) +{ + checkStates[clickedIndex.row()] = !checkStates[clickedIndex.row()]; + dataChanged(index(clickedIndex.row(), 0), index(clickedIndex.row(), 0), QVector<int>() << Qt::CheckStateRole); +} + +void DiveImportedModel::selectAll() +{ + memset(checkStates, true, lastIndex - firstIndex + 1); + dataChanged(index(0, 0), index(lastIndex - firstIndex, 0), QVector<int>() << Qt::CheckStateRole); +} + +void DiveImportedModel::selectNone() +{ + memset(checkStates, false, lastIndex - firstIndex + 1); + dataChanged(index(0, 0), index(lastIndex - firstIndex,0 ), QVector<int>() << Qt::CheckStateRole); +} + +Qt::ItemFlags DiveImportedModel::flags(const QModelIndex &index) const +{ + if (index.column() != 0) + return QAbstractTableModel::flags(index); + return QAbstractTableModel::flags(index) | Qt::ItemIsUserCheckable; +} + +void DiveImportedModel::clearTable() +{ + beginRemoveRows(QModelIndex(), 0, lastIndex - firstIndex); + lastIndex = -1; + firstIndex = 0; + endRemoveRows(); +} + +void DiveImportedModel::setImportedDivesIndexes(int first, int last) +{ + Q_ASSERT(last >= first); + beginRemoveRows(QModelIndex(), 0, lastIndex - firstIndex); + endRemoveRows(); + beginInsertRows(QModelIndex(), 0, last - first); + lastIndex = last; + firstIndex = first; + delete[] checkStates; + checkStates = new bool[last - first + 1]; + memset(checkStates, true, last - first + 1); + endInsertRows(); +} diff --git a/desktop-widgets/downloadfromdivecomputer.h b/desktop-widgets/downloadfromdivecomputer.h new file mode 100644 index 000000000..7acd49e95 --- /dev/null +++ b/desktop-widgets/downloadfromdivecomputer.h @@ -0,0 +1,125 @@ +#ifndef DOWNLOADFROMDIVECOMPUTER_H +#define DOWNLOADFROMDIVECOMPUTER_H + +#include <QDialog> +#include <QThread> +#include <QHash> +#include <QMap> +#include <QAbstractTableModel> + +#include "libdivecomputer.h" +#include "configuredivecomputerdialog.h" +#include "ui_downloadfromdivecomputer.h" + +#if defined(BT_SUPPORT) +#include "btdeviceselectiondialog.h" +#endif + +class QStringListModel; + +class DownloadThread : public QThread { + Q_OBJECT +public: + DownloadThread(QObject *parent, device_data_t *data); + virtual void run(); + + QString error; + +private: + device_data_t *data; +}; + +class DiveImportedModel : public QAbstractTableModel +{ + Q_OBJECT +public: + DiveImportedModel(QObject *o); + int columnCount(const QModelIndex& index = QModelIndex()) const; + int rowCount(const QModelIndex& index = QModelIndex()) const; + QVariant data(const QModelIndex& index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + void setImportedDivesIndexes(int first, int last); + Qt::ItemFlags flags(const QModelIndex &index) const; + void clearTable(); + +public +slots: + void changeSelected(QModelIndex clickedIndex); + void selectAll(); + void selectNone(); + +private: + int firstIndex; + int lastIndex; + bool *checkStates; +}; + +class DownloadFromDCWidget : public QDialog { + Q_OBJECT +public: + explicit DownloadFromDCWidget(QWidget *parent = 0, Qt::WindowFlags f = 0); + void reject(); + + enum states { + INITIAL, + DOWNLOADING, + CANCELLING, + ERROR, + DONE, + }; + +public +slots: + void on_downloadCancelRetryButton_clicked(); + void on_ok_clicked(); + void on_cancel_clicked(); + void on_search_clicked(); + void on_vendor_currentIndexChanged(const QString &vendor); + void on_product_currentIndexChanged(const QString &product); + + void onDownloadThreadFinished(); + void updateProgressBar(); + void checkLogFile(int state); + void checkDumpFile(int state); + void pickDumpFile(); + void pickLogFile(); +#if defined(BT_SUPPORT) + void enableBluetoothMode(int state); + void selectRemoteBluetoothDevice(); + void bluetoothSelectionDialogIsFinished(int result); +#endif +private: + void markChildrenAsDisabled(); + void markChildrenAsEnabled(); + + Ui::DownloadFromDiveComputer ui; + DownloadThread *thread; + bool downloading; + + QStringList vendorList; + QHash<QString, QStringList> productList; + QMap<QString, dc_descriptor_t *> descriptorLookup; + device_data_t data; + int previousLast; + + QStringListModel *vendorModel; + QStringListModel *productModel; + void fill_computer_list(); + void fill_device_list(int dc_type); + QString logFile; + QString dumpFile; + QTimer *timer; + bool dumpWarningShown; + OstcFirmwareCheck *ostcFirmwareCheck; + DiveImportedModel *diveImportedModel; +#if defined(BT_SUPPORT) + BtDeviceSelectionDialog *btDeviceSelectionDialog; +#endif + +public: + bool preferDownloaded(); + void updateState(states state); + states currentState; +}; + +#endif // DOWNLOADFROMDIVECOMPUTER_H diff --git a/desktop-widgets/downloadfromdivecomputer.ui b/desktop-widgets/downloadfromdivecomputer.ui new file mode 100644 index 000000000..b1f152034 --- /dev/null +++ b/desktop-widgets/downloadfromdivecomputer.ui @@ -0,0 +1,301 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>DownloadFromDiveComputer</class> + <widget class="QDialog" name="DownloadFromDiveComputer"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>747</width> + <height>535</height> + </rect> + </property> + <property name="windowTitle"> + <string>Download from dive computer</string> + </property> + <property name="windowIcon"> + <iconset> + <normalon>:/subsurface-icon</normalon> + </iconset> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <property name="spacing"> + <number>5</number> + </property> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="topMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>5</number> + </property> + <property name="bottomMargin"> + <number>5</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <layout class="QGridLayout" name="choicesLayout"> + <property name="horizontalSpacing"> + <number>0</number> + </property> + <property name="verticalSpacing"> + <number>5</number> + </property> + <item row="4" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Device or mount point</string> + </property> + </widget> + </item> + <item row="5" column="0"> + <widget class="QComboBox" name="device"> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QToolButton" name="search"> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item row="6" column="0"> + <widget class="QCheckBox" name="forceDownload"> + <property name="text"> + <string>Force download of all dives</string> + </property> + </widget> + </item> + <item row="7" column="0"> + <widget class="QCheckBox" name="preferDownloaded"> + <property name="text"> + <string>Always prefer downloaded dives</string> + </property> + </widget> + </item> + <item row="8" column="0"> + <widget class="QCheckBox" name="createNewTrip"> + <property name="text"> + <string>Download into new trip</string> + </property> + </widget> + </item> + <item row="9" column="0"> + <widget class="QCheckBox" name="logToFile"> + <property name="text"> + <string>Save libdivecomputer logfile</string> + </property> + </widget> + </item> + <item row="9" column="1"> + <widget class="QToolButton" name="chooseLogFile"> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item row="10" column="0"> + <widget class="QCheckBox" name="dumpToFile"> + <property name="text"> + <string>Save libdivecomputer dumpfile</string> + </property> + </widget> + </item> + <item row="10" column="1"> + <widget class="QToolButton" name="chooseDumpFile"> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item row="11" column="0"> + <widget class="QCheckBox" name="bluetoothMode"> + <property name="text"> + <string>Choose Bluetooth download mode</string> + </property> + </widget> + </item> + <item row="11" column="1"> + <widget class="QToolButton" name="chooseBluetoothDevice"> + <property name="toolTip"> + <string>Select a remote Bluetooth device.</string> + </property> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Vendor</string> + </property> + </widget> + </item> + <item row="1" column="0" colspan="2"> + <widget class="QComboBox" name="vendor"/> + </item> + <item row="2" column="0" colspan="2"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Dive computer</string> + </property> + </widget> + </item> + <item row="3" column="0" colspan="2"> + <widget class="QComboBox" name="product"/> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="downloadCancelRetryLayout"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="downloadCancelRetryButton"> + <property name="text"> + <string>Download</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QProgressBar" name="progressBar"> + <property name="value"> + <number>0</number> + </property> + </widget> + </item> + <item> + <spacer name="aboveOKCancelSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <layout class="QHBoxLayout" name="labelSelectAllNoneLayout"> + <property name="spacing"> + <number>5</number> + </property> + <item> + <widget class="QLabel" name="downloadedDivesLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Downloaded dives</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="selectAllButton"> + <property name="text"> + <string>Select all</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="unselectAllButton"> + <property name="text"> + <string>Unselect all</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QTableView" name="downloadedView"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="buttonBoxLayout"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="ok"> + <property name="text"> + <string>OK</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="cancel"> + <property name="text"> + <string>Cancel</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/desktop-widgets/filterwidget.ui b/desktop-widgets/filterwidget.ui new file mode 100644 index 000000000..1450d81b2 --- /dev/null +++ b/desktop-widgets/filterwidget.ui @@ -0,0 +1,140 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>FilterWidget2</class> + <widget class="QWidget" name="FilterWidget2"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>594</width> + <height>362</height> + </rect> + </property> + <property name="windowTitle"> + <string></string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="filterText"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QToolButton" name="clear"> + <property name="toolTip"> + <string>Reset filters</string> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/filter-reset</normaloff>:/filter-reset</iconset> + </property> + <property name="autoRaise"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="maximize"> + <property name="toolTip"> + <string>Show/hide filters</string> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/filter-hide</normaloff>:/filter-hide</iconset> + </property> + <property name="autoRaise"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="close"> + <property name="toolTip"> + <string>Close and reset filters</string> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/filter-close</normaloff>:/filter-close</iconset> + </property> + <property name="autoRaise"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QScrollArea" name="scrollArea"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="widgetResizable"> + <bool>true</bool> + </property> + <widget class="QWidget" name="scrollAreaWidgetContents"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>594</width> + <height>337</height> + </rect> + </property> + </widget> + </widget> + </item> + </layout> + </widget> + <resources> + <include location="../subsurface.qrc"/> + </resources> + <connections/> +</ui> diff --git a/desktop-widgets/globe.cpp b/desktop-widgets/globe.cpp new file mode 100644 index 000000000..135f195a1 --- /dev/null +++ b/desktop-widgets/globe.cpp @@ -0,0 +1,431 @@ +#include "globe.h" +#ifndef NO_MARBLE +#include "mainwindow.h" +#include "helpers.h" +#include "divelistview.h" +#include "maintab.h" +#include "display.h" + +#include <QTimer> +#include <QContextMenuEvent> +#include <QMouseEvent> + +#include <marble/AbstractFloatItem.h> +#include <marble/GeoDataPlacemark.h> +#include <marble/GeoDataDocument.h> +#include <marble/MarbleModel.h> +#include <marble/MarbleDirs.h> +#include <marble/MapThemeManager.h> +#include <marble/GeoDataStyle.h> +#include <marble/GeoDataIconStyle.h> +#include <marble/GeoDataTreeModel.h> + +#ifdef MARBLE_SUBSURFACE_BRANCH +#include <marble/MarbleDebug.h> +#endif + +GlobeGPS *GlobeGPS::instance() +{ + static GlobeGPS *self = new GlobeGPS(); + return self; +} + +GlobeGPS::GlobeGPS(QWidget *parent) : MarbleWidget(parent), + loadedDives(0), + messageWidget(new KMessageWidget(this)), + fixZoomTimer(new QTimer(this)), + needResetZoom(false), + editingDiveLocation(false), + doubleClick(false) +{ +#ifdef MARBLE_SUBSURFACE_BRANCH + // we need to make sure this gets called after the command line arguments have + // been processed but before we initialize the rest of Marble + Marble::MarbleDebug::setEnabled(verbose); +#endif + currentZoomLevel = -1; + // check if Google Sat Maps are installed + // if not, check if they are in a known location + MapThemeManager mtm; + QStringList list = mtm.mapThemeIds(); + QString subsurfaceDataPath; + QDir marble; + if (!list.contains("earth/googlesat/googlesat.dgml")) { + subsurfaceDataPath = getSubsurfaceDataPath("marbledata"); + if (subsurfaceDataPath.size()) { + MarbleDirs::setMarbleDataPath(subsurfaceDataPath); + } else { + subsurfaceDataPath = getSubsurfaceDataPath("data"); + if (subsurfaceDataPath.size()) + MarbleDirs::setMarbleDataPath(subsurfaceDataPath); + } + } + messageWidget->setCloseButtonVisible(false); + messageWidget->setHidden(true); + + setMapThemeId("earth/googlesat/googlesat.dgml"); + //setMapThemeId("earth/openstreetmap/openstreetmap.dgml"); + setProjection(Marble::Spherical); + + setAnimationsEnabled(true); + Q_FOREACH (AbstractFloatItem *i, floatItems()) { + i->setVisible(false); + } + + setShowClouds(false); + setShowBorders(false); + setShowPlaces(true); + setShowCrosshairs(false); + setShowGrid(false); + setShowOverviewMap(false); + setShowScaleBar(true); + setShowCompass(false); + connect(this, SIGNAL(mouseClickGeoPosition(qreal, qreal, GeoDataCoordinates::Unit)), + this, SLOT(mouseClicked(qreal, qreal, GeoDataCoordinates::Unit))); + + setMinimumHeight(0); + setMinimumWidth(0); + connect(fixZoomTimer, SIGNAL(timeout()), this, SLOT(fixZoom())); + fixZoomTimer->setSingleShot(true); + installEventFilter(this); +} + +bool GlobeGPS::eventFilter(QObject *obj, QEvent *ev) +{ + // sometimes Marble seems not to notice double clicks and consequently not call + // the right callback - so let's remember here if the last 'click' is a 'double' or not + enum QEvent::Type type = ev->type(); + if (type == QEvent::MouseButtonDblClick) + doubleClick = true; + else if (type == QEvent::MouseButtonPress) + doubleClick = false; + + // This disables Zooming when a double click occours on the scene. + if (type == QEvent::MouseButtonDblClick && !editingDiveLocation) + return true; + // This disables the Marble's Context Menu + // we need to move this to our 'contextMenuEvent' + // if we plan to do a different one in the future. + if (type == QEvent::ContextMenu) { + contextMenuEvent(static_cast<QContextMenuEvent *>(ev)); + return true; + } + if (type == QEvent::MouseButtonPress) { + QMouseEvent *e = static_cast<QMouseEvent *>(ev); + if (e->button() == Qt::RightButton) + return true; + } + return QObject::eventFilter(obj, ev); +} + +void GlobeGPS::contextMenuEvent(QContextMenuEvent *ev) +{ + QMenu m; + QAction *a = m.addAction(tr("Edit selected dive locations"), this, SLOT(prepareForGetDiveCoordinates())); + a->setData(QVariant::fromValue<void *>(&m)); + a->setEnabled(current_dive); + m.exec(ev->globalPos()); +} + +void GlobeGPS::mouseClicked(qreal lon, qreal lat, GeoDataCoordinates::Unit unit) +{ + if (doubleClick) { + // strangely sometimes we don't get the changeDiveGeoPosition callback + // and end up here instead + changeDiveGeoPosition(lon, lat, unit); + return; + } + // don't mess with the selection while the user is editing a dive + if (MainWindow::instance()->information()->isEditing() || messageWidget->isVisible()) + return; + + GeoDataCoordinates here(lon, lat, unit); + long lon_udeg = rint(1000000 * here.longitude(GeoDataCoordinates::Degree)); + long lat_udeg = rint(1000000 * here.latitude(GeoDataCoordinates::Degree)); + + // distance() is in km above the map. + // We're going to use that to decide how + // approximate the dives have to be. + // + // Totally arbitrarily I say that 1km + // distance means that we can resolve + // to about 100m. Which in turn is about + // 1000 udeg. + // + // Trigonometry is hard, but sin x == x + // for small x, so let's just do this as + // a linear thing. + long resolve = rint(distance() * 1000); + + int idx; + struct dive *dive; + bool clear = !(QApplication::keyboardModifiers() & Qt::ControlModifier); + QList<int> selectedDiveIds; + for_each_dive (idx, dive) { + long lat_diff, lon_diff; + struct dive_site *ds = get_dive_site_for_dive(dive); + if (!dive_site_has_gps_location(ds)) + continue; + lat_diff = labs(ds->latitude.udeg - lat_udeg); + lon_diff = labs(ds->longitude.udeg - lon_udeg); + if (lat_diff > 180000000) + lat_diff = 360000000 - lat_diff; + if (lon_diff > 180000000) + lon_diff = 180000000 - lon_diff; + if (lat_diff > resolve || lon_diff > resolve) + continue; + + selectedDiveIds.push_back(idx); + } + if (selectedDiveIds.empty()) + return; + if (clear) + MainWindow::instance()->dive_list()->unselectDives(); + MainWindow::instance()->dive_list()->selectDives(selectedDiveIds); +} + +void GlobeGPS::repopulateLabels() +{ + static GeoDataStyle otherSite, currentSite; + static GeoDataIconStyle darkFlag(QImage(":flagDark")), lightFlag(QImage(":flagLight")); + struct dive_site *ds; + int idx; + QMap<QString, GeoDataPlacemark *> locationMap; + if (loadedDives) { + model()->treeModel()->removeDocument(loadedDives); + delete loadedDives; + } + loadedDives = new GeoDataDocument; + otherSite.setIconStyle(darkFlag); + currentSite.setIconStyle(lightFlag); + + if (displayed_dive_site.uuid && dive_site_has_gps_location(&displayed_dive_site)) { + GeoDataPlacemark *place = new GeoDataPlacemark(displayed_dive_site.name); + place->setStyle(¤tSite); + place->setCoordinate(displayed_dive_site.longitude.udeg / 1000000.0, + displayed_dive_site.latitude.udeg / 1000000.0, 0, GeoDataCoordinates::Degree); + locationMap[QString(displayed_dive_site.name)] = place; + loadedDives->append(place); + } + for_each_dive_site(idx, ds) { + if (ds->uuid == displayed_dive_site.uuid) + continue; + if (dive_site_has_gps_location(ds)) { + GeoDataPlacemark *place = new GeoDataPlacemark(ds->name); + place->setStyle(&otherSite); + place->setCoordinate(ds->longitude.udeg / 1000000.0, ds->latitude.udeg / 1000000.0, 0, GeoDataCoordinates::Degree); + + // don't add dive locations twice, unless they are at least 50m apart + if (locationMap[QString(ds->name)]) { + GeoDataCoordinates existingLocation = locationMap[QString(ds->name)]->coordinate(); + GeoDataLineString segment = GeoDataLineString(); + segment.append(existingLocation); + GeoDataCoordinates newLocation = place->coordinate(); + segment.append(newLocation); + double dist = segment.length(6371); + // the dist is scaled to the radius given - so with 6371km as radius + // 50m turns into 0.05 as threashold + if (dist < 0.05) + continue; + } + locationMap[QString(ds->name)] = place; + loadedDives->append(place); + } + } + model()->treeModel()->addDocument(loadedDives); + + struct dive_site *center = displayed_dive_site.uuid != 0 ? + &displayed_dive_site : current_dive ? + get_dive_site_by_uuid(current_dive->dive_site_uuid) : NULL; + if(dive_site_has_gps_location(&displayed_dive_site) && center) + centerOn(displayed_dive_site.longitude.udeg / 1000000.0, displayed_dive_site.latitude.udeg / 1000000.0, true); +} + +void GlobeGPS::reload() +{ + editingDiveLocation = false; + messageWidget->hide(); + repopulateLabels(); +} + +void GlobeGPS::centerOnDiveSite(struct dive_site *ds) +{ + if (!dive_site_has_gps_location(ds)) { + // this is not intuitive and at times causes trouble - let's comment it out for now + // zoomOutForNoGPS(); + return; + } + qreal longitude = ds->longitude.udeg / 1000000.0; + qreal latitude = ds->latitude.udeg / 1000000.0; + + if(IS_FP_SAME(longitude, centerLongitude()) && IS_FP_SAME(latitude,centerLatitude())) { + return; + } + + // if no zoom is set up, set the zoom as seen from 3km above + // if we come back from a dive without GPS data, reset to the last zoom value + // otherwise check to make sure we aren't still running an animation and then remember + // the current zoom level + if (currentZoomLevel == -1) { + currentZoomLevel = zoomFromDistance(3.0); + centerOn(longitude, latitude); + fixZoom(true); + return; + } + if (!fixZoomTimer->isActive()) { + if (needResetZoom) { + needResetZoom = false; + fixZoom(); + } else if (zoom() >= 1200) { + currentZoomLevel = zoom(); + } + } + // From the marble source code, the maximum time of + // 'spin and fit' is 2000 miliseconds so wait a bit them zoom again. + fixZoomTimer->stop(); + if (zoom() < 1200 && IS_FP_SAME(centerLatitude(), latitude) && IS_FP_SAME(centerLongitude(), longitude)) { + // create a tiny movement + centerOn(longitude + 0.00001, latitude + 0.00001); + fixZoomTimer->start(300); + } else { + fixZoomTimer->start(2100); + } + centerOn(longitude, latitude, true); +} + +void GlobeGPS::fixZoom(bool now) +{ + setZoom(currentZoomLevel, now ? Marble::Instant : Marble::Linear); +} + +void GlobeGPS::zoomOutForNoGPS() +{ + // this is called if the dive has no GPS location. + // zoom out quite a bit to show the globe and remember that the next time + // we show a dive with GPS location we need to zoom in again + if (!needResetZoom) { + needResetZoom = true; + if (!fixZoomTimer->isActive() && zoom() >= 1500) { + currentZoomLevel = zoom(); + } + } + if (fixZoomTimer->isActive()) + fixZoomTimer->stop(); + // 1000 is supposed to make sure you see the whole globe + setZoom(1000, Marble::Linear); +} + +void GlobeGPS::endGetDiveCoordinates() +{ + messageWidget->animatedHide(); + editingDiveLocation = false; +} + +void GlobeGPS::prepareForGetDiveCoordinates() +{ + messageWidget->setMessageType(KMessageWidget::Warning); + messageWidget->setText(QObject::tr("Move the map and double-click to set the dive location")); + messageWidget->setWordWrap(true); + messageWidget->setCloseButtonVisible(false); + messageWidget->animatedShow(); + editingDiveLocation = true; + // this is not intuitive and at times causes trouble - let's comment it out for now + // if (!dive_has_gps_location(current_dive)) + // zoomOutForNoGPS(); +} + +void GlobeGPS::changeDiveGeoPosition(qreal lon, qreal lat, GeoDataCoordinates::Unit unit) +{ + if (!editingDiveLocation) + return; + + // convert to degrees if in radian. + if (unit == GeoDataCoordinates::Radian) { + lon = lon * 180 / M_PI; + lat = lat * 180 / M_PI; + } + centerOn(lon, lat, true); + + // change the location of the displayed_dive and put the UI in edit mode + displayed_dive_site.latitude.udeg = lrint(lat * 1000000.0); + displayed_dive_site.longitude.udeg = lrint(lon * 1000000.0); + emit coordinatesChanged(); + repopulateLabels(); +} + +void GlobeGPS::mousePressEvent(QMouseEvent *event) +{ + if (event->type() != QEvent::MouseButtonDblClick) + return; + + qreal lat, lon; + bool clickOnGlobe = geoCoordinates(event->pos().x(), event->pos().y(), lon, lat, GeoDataCoordinates::Degree); + + // there could be two scenarios that got us here; let's check if we are editing a dive + if (MainWindow::instance()->information()->isEditing() && clickOnGlobe) { + // + // FIXME + // TODO + // + // this needs to do this on the dive site screen + // MainWindow::instance()->information()->updateCoordinatesText(lat, lon); + repopulateLabels(); + } else if (clickOnGlobe) { + changeDiveGeoPosition(lon, lat, GeoDataCoordinates::Degree); + } +} + +void GlobeGPS::resizeEvent(QResizeEvent *event) +{ + int size = event->size().width(); + MarbleWidget::resizeEvent(event); + if (size > 600) + messageWidget->setGeometry((size - 600) / 2, 5, 600, 0); + else + messageWidget->setGeometry(5, 5, size - 10, 0); + messageWidget->setMaximumHeight(500); +} + +void GlobeGPS::centerOnIndex(const QModelIndex& idx) +{ + struct dive_site *ds = get_dive_site_by_uuid(idx.model()->index(idx.row(), 0).data().toInt()); + if (!ds || !dive_site_has_gps_location(ds)) + centerOnDiveSite(&displayed_dive_site); + else + centerOnDiveSite(ds); +} +#else + +GlobeGPS *GlobeGPS::instance() +{ + static GlobeGPS *self = new GlobeGPS(); + return self; +} + +GlobeGPS::GlobeGPS(QWidget *parent) +{ + setText("MARBLE DISABLED AT BUILD TIME"); +} +void GlobeGPS::repopulateLabels() +{ +} +void GlobeGPS::centerOnCurrentDive() +{ +} +bool GlobeGPS::eventFilter(QObject *obj, QEvent *ev) +{ + return QObject::eventFilter(obj, ev); +} +void GlobeGPS::prepareForGetDiveCoordinates() +{ +} +void GlobeGPS::endGetDiveCoordinates() +{ +} +void GlobeGPS::reload() +{ +} +void GlobeGPS::centerOnIndex(const QModelIndex& idx) +{ +} +#endif diff --git a/desktop-widgets/globe.h b/desktop-widgets/globe.h new file mode 100644 index 000000000..8cc1265e4 --- /dev/null +++ b/desktop-widgets/globe.h @@ -0,0 +1,84 @@ +#ifndef GLOBE_H +#define GLOBE_H + +#include <stdint.h> + +#ifndef NO_MARBLE +#include <marble/MarbleWidget.h> +#include <marble/GeoDataCoordinates.h> + +#include <QHash> + +namespace Marble{ + class GeoDataDocument; +} + +class KMessageWidget; +using namespace Marble; +struct dive; + +class GlobeGPS : public MarbleWidget { + Q_OBJECT +public: + using MarbleWidget::centerOn; + static GlobeGPS *instance(); + void reload(); + bool eventFilter(QObject *, QEvent *); + +protected: + /* reimp */ void resizeEvent(QResizeEvent *event); + /* reimp */ void mousePressEvent(QMouseEvent *event); + /* reimp */ void contextMenuEvent(QContextMenuEvent *); + +private: + GeoDataDocument *loadedDives; + KMessageWidget *messageWidget; + QTimer *fixZoomTimer; + int currentZoomLevel; + bool needResetZoom; + bool editingDiveLocation; + bool doubleClick; + GlobeGPS(QWidget *parent = 0); + +signals: + void coordinatesChanged(); + +public +slots: + void repopulateLabels(); + void changeDiveGeoPosition(qreal lon, qreal lat, GeoDataCoordinates::Unit); + void mouseClicked(qreal lon, qreal lat, GeoDataCoordinates::Unit); + void fixZoom(bool now = false); + void zoomOutForNoGPS(); + void prepareForGetDiveCoordinates(); + void endGetDiveCoordinates(); + void centerOnDiveSite(struct dive_site *ds); + void centerOnIndex(const QModelIndex& idx); +}; + +#else // NO_MARBLE +/* Dummy widget for when we don't have MarbleWidget */ +#include <QLabel> + +class GlobeGPS : public QLabel { + Q_OBJECT +public: + GlobeGPS(QWidget *parent = 0); + static GlobeGPS *instance(); + void reload(); + void repopulateLabels(); + void centerOnDiveSite(uint32_t uuid); + void centerOnIndex(const QModelIndex& idx); + void centerOnCurrentDive(); + bool eventFilter(QObject *, QEvent *); +public +slots: + void prepareForGetDiveCoordinates(); + void endGetDiveCoordinates(); +}; + +#endif // NO_MARBLE + +extern "C" double getDistance(int lat1, int lon1, int lat2, int lon2); + +#endif // GLOBE_H diff --git a/desktop-widgets/groupedlineedit.cpp b/desktop-widgets/groupedlineedit.cpp new file mode 100644 index 000000000..9ce5e175c --- /dev/null +++ b/desktop-widgets/groupedlineedit.cpp @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2013 Maximilian Güntner <maximilian.guentner@gmail.com> + * + * This file is subject to the terms and conditions of version 2 of + * the GNU General Public License. See the file gpl-2.0.txt in the main + * directory of this archive for more details. + * + * Original License: + * + * This file is part of the Nepomuk widgets collection + * Copyright (c) 2013 Denis Steckelmacher <steckdenis@yahoo.fr> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License version 2.1 as published by the Free Software Foundation, + * or any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. +*/ + +#include "groupedlineedit.h" + +#include <QScrollBar> +#include <QTextBlock> +#include <QPainter> +#include <QApplication> +#include <QStyle> +#include <QStyleOptionFocusRect> +#include <QDebug> + +struct GroupedLineEdit::Private { + struct Block { + int start; + int end; + QString text; + }; + QVector<Block> blocks; + QVector<QColor> colors; +}; + +GroupedLineEdit::GroupedLineEdit(QWidget *parent) : QPlainTextEdit(parent), + d(new Private) +{ + setWordWrapMode(QTextOption::NoWrap); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + document()->setMaximumBlockCount(1); +} + +GroupedLineEdit::~GroupedLineEdit() +{ + delete d; +} + +QString GroupedLineEdit::text() const +{ + // Remove the block crosses from the text + return toPlainText(); +} + +int GroupedLineEdit::cursorPosition() const +{ + return textCursor().positionInBlock(); +} + +void GroupedLineEdit::addBlock(int start, int end) +{ + Private::Block block; + block.start = start; + block.end = end; + block.text = text().mid(start, end - start + 1).remove(',').trimmed(); + if (block.text.isEmpty()) + return; + d->blocks.append(block); + viewport()->update(); +} + +void GroupedLineEdit::addColor(QColor color) +{ + d->colors.append(color); +} + +void GroupedLineEdit::removeAllColors() +{ + d->colors.clear(); +} + +QStringList GroupedLineEdit::getBlockStringList() +{ + QStringList retList; + foreach (const Private::Block &block, d->blocks) + retList.append(block.text); + return retList; +} + +void GroupedLineEdit::setCursorPosition(int position) +{ + QTextCursor c = textCursor(); + c.setPosition(position, QTextCursor::MoveAnchor); + setTextCursor(c); +} + +void GroupedLineEdit::setText(const QString &text) +{ + setPlainText(text); +} + +void GroupedLineEdit::clear() +{ + QPlainTextEdit::clear(); + removeAllBlocks(); +} + +void GroupedLineEdit::selectAll() +{ + QTextCursor c = textCursor(); + c.select(QTextCursor::LineUnderCursor); + setTextCursor(c); +} + +void GroupedLineEdit::removeAllBlocks() +{ + d->blocks.clear(); + viewport()->update(); +} + +QSize GroupedLineEdit::sizeHint() const +{ + QSize rs( + 40, + document()->findBlock(0).layout()->lineAt(0).height() + + document()->documentMargin() * 2 + + frameWidth() * 2); + return rs; +} + +QSize GroupedLineEdit::minimumSizeHint() const +{ + return sizeHint(); +} + +void GroupedLineEdit::keyPressEvent(QKeyEvent *e) +{ + switch (e->key()) { + case Qt::Key_Return: + case Qt::Key_Enter: + emit editingFinished(); + return; + } + QPlainTextEdit::keyPressEvent(e); +} + +void GroupedLineEdit::paintEvent(QPaintEvent *e) +{ + QTextLine line = document()->findBlock(0).layout()->lineAt(0); + QPainter painter(viewport()); + + painter.setRenderHint(QPainter::Antialiasing, true); + painter.fillRect(0, 0, viewport()->width(), viewport()->height(), palette().base()); + + QVectorIterator<QColor> i(d->colors); + i.toFront(); + foreach (const Private::Block &block, d->blocks) { + qreal start_x = line.cursorToX(block.start, QTextLine::Leading); + qreal end_x = line.cursorToX(block.end-1, QTextLine::Trailing); + + QPainterPath path; + QRectF rectangle( + start_x - 1.0 - double(horizontalScrollBar()->value()), + 1.0, + end_x - start_x + 2.0, + double(viewport()->height() - 2)); + if (!i.hasNext()) + i.toFront(); + path.addRoundedRect(rectangle, 5.0, 5.0); + painter.setPen(i.peekNext()); + if (palette().color(QPalette::Text).lightnessF() <= 0.3) + painter.setBrush(i.next().lighter()); + else if (palette().color(QPalette::Text).lightnessF() <= 0.6) + painter.setBrush(i.next()); + else + painter.setBrush(i.next().darker()); + painter.drawPath(path); + } + QPlainTextEdit::paintEvent(e); +} diff --git a/desktop-widgets/groupedlineedit.h b/desktop-widgets/groupedlineedit.h new file mode 100644 index 000000000..c9cd1a0e0 --- /dev/null +++ b/desktop-widgets/groupedlineedit.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2013 Maximilian Güntner <maximilian.guentner@gmail.com> + * + * This file is subject to the terms and conditions of version 2 of + * the GNU General Public License. See the file gpl-2.0.txt in the main + * directory of this archive for more details. + * + * Original License: + * + * This file is part of the Nepomuk widgets collection + * Copyright (c) 2013 Denis Steckelmacher <steckdenis@yahoo.fr> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License version 2.1 as published by the Free Software Foundation, + * or any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef GROUPEDLINEEDIT_H +#define GROUPEDLINEEDIT_H + +#include <QPlainTextEdit> +#include <QStringList> + +class GroupedLineEdit : public QPlainTextEdit { + Q_OBJECT + +public: + explicit GroupedLineEdit(QWidget *parent = 0); + virtual ~GroupedLineEdit(); + + QString text() const; + + int cursorPosition() const; + void setCursorPosition(int position); + void setText(const QString &text); + void clear(); + void selectAll(); + + void removeAllBlocks(); + void addBlock(int start, int end); + QStringList getBlockStringList(); + + void addColor(QColor color); + void removeAllColors(); + + virtual QSize sizeHint() const; + virtual QSize minimumSizeHint() const; + +signals: + void editingFinished(); + +protected: + virtual void paintEvent(QPaintEvent *e); + virtual void keyPressEvent(QKeyEvent *e); + +private: + struct Private; + Private *d; +}; +#endif // GROUPEDLINEEDIT_H diff --git a/desktop-widgets/kmessagewidget.cpp b/desktop-widgets/kmessagewidget.cpp new file mode 100644 index 000000000..2e506af2d --- /dev/null +++ b/desktop-widgets/kmessagewidget.cpp @@ -0,0 +1,480 @@ +/* This file is part of the KDE libraries + * + * Copyright (c) 2011 Aurélien Gâteau <agateau@kde.org> + * Copyright (c) 2014 Dominik Haumann <dhaumann@kde.org> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + */ +#include "kmessagewidget.h" + +#include <QAction> +#include <QEvent> +#include <QGridLayout> +#include <QHBoxLayout> +#include <QLabel> +#include <QPainter> +#include <QShowEvent> +#include <QTimeLine> +#include <QToolButton> +#include <QStyle> + +//--------------------------------------------------------------------- +// KMessageWidgetPrivate +//--------------------------------------------------------------------- +class KMessageWidgetPrivate +{ +public: + void init(KMessageWidget *); + + KMessageWidget *q; + QFrame *content; + QLabel *iconLabel; + QLabel *textLabel; + QToolButton *closeButton; + QTimeLine *timeLine; + QIcon icon; + + KMessageWidget::MessageType messageType; + bool wordWrap; + QList<QToolButton *> buttons; + QPixmap contentSnapShot; + + void createLayout(); + void updateSnapShot(); + void updateLayout(); + void slotTimeLineChanged(qreal); + void slotTimeLineFinished(); + + int bestContentHeight() const; +}; + +void KMessageWidgetPrivate::init(KMessageWidget *q_ptr) +{ + q = q_ptr; + + q->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); + + timeLine = new QTimeLine(500, q); + QObject::connect(timeLine, SIGNAL(valueChanged(qreal)), q, SLOT(slotTimeLineChanged(qreal))); + QObject::connect(timeLine, SIGNAL(finished()), q, SLOT(slotTimeLineFinished())); + + content = new QFrame(q); + content->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + wordWrap = false; + + iconLabel = new QLabel(content); + iconLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + iconLabel->hide(); + + textLabel = new QLabel(content); + textLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + textLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); + QObject::connect(textLabel, SIGNAL(linkActivated(QString)), q, SIGNAL(linkActivated(QString))); + QObject::connect(textLabel, SIGNAL(linkHovered(QString)), q, SIGNAL(linkHovered(QString))); + + QAction *closeAction = new QAction(q); + closeAction->setText(KMessageWidget::tr("&Close")); + closeAction->setToolTip(KMessageWidget::tr("Close message")); + closeAction->setIcon(q->style()->standardIcon(QStyle::SP_DialogCloseButton)); + + QObject::connect(closeAction, SIGNAL(triggered(bool)), q, SLOT(animatedHide())); + + closeButton = new QToolButton(content); + closeButton->setAutoRaise(true); + closeButton->setDefaultAction(closeAction); + + q->setMessageType(KMessageWidget::Information); +} + +void KMessageWidgetPrivate::createLayout() +{ + delete content->layout(); + + content->resize(q->size()); + + qDeleteAll(buttons); + buttons.clear(); + + Q_FOREACH (QAction *action, q->actions()) { + QToolButton *button = new QToolButton(content); + button->setDefaultAction(action); + button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + buttons.append(button); + } + + // AutoRaise reduces visual clutter, but we don't want to turn it on if + // there are other buttons, otherwise the close button will look different + // from the others. + closeButton->setAutoRaise(buttons.isEmpty()); + + if (wordWrap) { + QGridLayout *layout = new QGridLayout(content); + // Set alignment to make sure icon does not move down if text wraps + layout->addWidget(iconLabel, 0, 0, 1, 1, Qt::AlignHCenter | Qt::AlignTop); + layout->addWidget(textLabel, 0, 1); + + QHBoxLayout *buttonLayout = new QHBoxLayout; + buttonLayout->addStretch(); + Q_FOREACH (QToolButton *button, buttons) { + // For some reason, calling show() is necessary if wordwrap is true, + // otherwise the buttons do not show up. It is not needed if + // wordwrap is false. + button->show(); + buttonLayout->addWidget(button); + } + buttonLayout->addWidget(closeButton); + layout->addItem(buttonLayout, 1, 0, 1, 2); + } else { + QHBoxLayout *layout = new QHBoxLayout(content); + layout->addWidget(iconLabel); + layout->addWidget(textLabel); + + Q_FOREACH (QToolButton *button, buttons) { + layout->addWidget(button); + } + + layout->addWidget(closeButton); + }; + + if (q->isVisible()) { + q->setFixedHeight(content->sizeHint().height()); + } + q->updateGeometry(); +} + +void KMessageWidgetPrivate::updateLayout() +{ + if (content->layout()) { + createLayout(); + } +} + +void KMessageWidgetPrivate::updateSnapShot() +{ + // Attention: updateSnapShot calls QWidget::render(), which causes the whole + // window layouts to be activated. Calling this method from resizeEvent() + // can lead to infinite recursion, see: + // https://bugs.kde.org/show_bug.cgi?id=311336 + contentSnapShot = QPixmap(content->size() * q->devicePixelRatio()); + contentSnapShot.setDevicePixelRatio(q->devicePixelRatio()); + contentSnapShot.fill(Qt::transparent); + content->render(&contentSnapShot, QPoint(), QRegion(), QWidget::DrawChildren); +} + +void KMessageWidgetPrivate::slotTimeLineChanged(qreal value) +{ + q->setFixedHeight(qMin(value * 2, qreal(1.0)) * content->height()); + q->update(); +} + +void KMessageWidgetPrivate::slotTimeLineFinished() +{ + if (timeLine->direction() == QTimeLine::Forward) { + // Show + // We set the whole geometry here, because it may be wrong if a + // KMessageWidget is shown right when the toplevel window is created. + content->setGeometry(0, 0, q->width(), bestContentHeight()); + + // notify about finished animation + emit q->showAnimationFinished(); + } else { + // hide and notify about finished animation + q->hide(); + emit q->hideAnimationFinished(); + } +} + +int KMessageWidgetPrivate::bestContentHeight() const +{ + int height = content->heightForWidth(q->width()); + if (height == -1) { + height = content->sizeHint().height(); + } + return height; +} + +//--------------------------------------------------------------------- +// KMessageWidget +//--------------------------------------------------------------------- +KMessageWidget::KMessageWidget(QWidget *parent) + : QFrame(parent) + , d(new KMessageWidgetPrivate) +{ + d->init(this); +} + +KMessageWidget::KMessageWidget(const QString &text, QWidget *parent) + : QFrame(parent) + , d(new KMessageWidgetPrivate) +{ + d->init(this); + setText(text); +} + +KMessageWidget::~KMessageWidget() +{ + delete d; +} + +QString KMessageWidget::text() const +{ + return d->textLabel->text(); +} + +void KMessageWidget::setText(const QString &text) +{ + d->textLabel->setText(text); + updateGeometry(); +} + +KMessageWidget::MessageType KMessageWidget::messageType() const +{ + return d->messageType; +} + +static QColor darkShade(QColor c) +{ + qreal contrast = 0.7; // taken from kcolorscheme for the dark shade + + qreal darkAmount; + if (c.lightnessF() < 0.006) { /* too dark */ + darkAmount = 0.02 + 0.40 * contrast; + } else if (c.lightnessF() > 0.93) { /* too bright */ + darkAmount = -0.06 - 0.60 * contrast; + } else { + darkAmount = (-c.lightnessF()) * (0.55 + contrast * 0.35); + } + + qreal v = c.lightnessF() + darkAmount; + v = v > 0.0 ? (v < 1.0 ? v : 1.0) : 0.0; + c.setHsvF(c.hslHueF(), c.hslSaturationF(), v); + return c; +} + +void KMessageWidget::setMessageType(KMessageWidget::MessageType type) +{ + d->messageType = type; + QColor bg0, bg1, bg2, border, fg; + switch (type) { + case Positive: + bg1.setRgb(0, 110, 40); // values taken from kcolorscheme.cpp (Positive) + break; + case Information: + bg1 = palette().highlight().color(); + break; + case Warning: + bg1.setRgb(176, 128, 0); // values taken from kcolorscheme.cpp (Neutral) + break; + case Error: + bg1.setRgb(191, 3, 3); // values taken from kcolorscheme.cpp (Negative) + break; + } + + // Colors + fg = palette().highlightedText().color(); + bg0 = bg1.lighter(110); + bg2 = bg1.darker(110); + border = darkShade(bg1); + + d->content->setStyleSheet( + QString(QLatin1String(".QFrame {" + "background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1," + " stop: 0 %1," + " stop: 0.1 %2," + " stop: 1.0 %3);" + "border-radius: 5px;" + "border: 1px solid %4;" + "margin: %5px;" + "}" + ".QLabel { color: %6; }" + )) + .arg(bg0.name()) + .arg(bg1.name()) + .arg(bg2.name()) + .arg(border.name()) + // DefaultFrameWidth returns the size of the external margin + border width. We know our border is 1px, so we subtract this from the frame normal QStyle FrameWidth to get our margin + .arg(style()->pixelMetric(QStyle::PM_DefaultFrameWidth, 0, this) - 1) + .arg(fg.name()) + ); +} + +QSize KMessageWidget::sizeHint() const +{ + ensurePolished(); + return d->content->sizeHint(); +} + +QSize KMessageWidget::minimumSizeHint() const +{ + ensurePolished(); + return d->content->minimumSizeHint(); +} + +bool KMessageWidget::event(QEvent *event) +{ + if (event->type() == QEvent::Polish && !d->content->layout()) { + d->createLayout(); + } + return QFrame::event(event); +} + +void KMessageWidget::resizeEvent(QResizeEvent *event) +{ + QFrame::resizeEvent(event); + + if (d->timeLine->state() == QTimeLine::NotRunning) { + d->content->resize(width(), d->bestContentHeight()); + } +} + +int KMessageWidget::heightForWidth(int width) const +{ + ensurePolished(); + return d->content->heightForWidth(width); +} + +void KMessageWidget::paintEvent(QPaintEvent *event) +{ + QFrame::paintEvent(event); + if (d->timeLine->state() == QTimeLine::Running) { + QPainter painter(this); + painter.setOpacity(d->timeLine->currentValue() * d->timeLine->currentValue()); + painter.drawPixmap(0, 0, d->contentSnapShot); + } +} + +bool KMessageWidget::wordWrap() const +{ + return d->wordWrap; +} + +void KMessageWidget::setWordWrap(bool wordWrap) +{ + d->wordWrap = wordWrap; + d->textLabel->setWordWrap(wordWrap); + QSizePolicy policy = sizePolicy(); + policy.setHeightForWidth(wordWrap); + setSizePolicy(policy); + d->updateLayout(); + // Without this, when user does wordWrap -> !wordWrap -> wordWrap, a minimum + // height is set, causing the widget to be too high. + // Mostly visible in test programs. + if (wordWrap) { + setMinimumHeight(0); + } +} + +bool KMessageWidget::isCloseButtonVisible() const +{ + return d->closeButton->isVisible(); +} + +void KMessageWidget::setCloseButtonVisible(bool show) +{ + d->closeButton->setVisible(show); + updateGeometry(); +} + +void KMessageWidget::addAction(QAction *action) +{ + QFrame::addAction(action); + d->updateLayout(); +} + +void KMessageWidget::removeAction(QAction *action) +{ + QFrame::removeAction(action); + d->updateLayout(); +} + +void KMessageWidget::animatedShow() +{ + if (!style()->styleHint(QStyle::SH_Widget_Animate, 0, this)) { + show(); + emit showAnimationFinished(); + return; + } + + if (isVisible()) { + return; + } + + QFrame::show(); + setFixedHeight(0); + int wantedHeight = d->bestContentHeight(); + d->content->setGeometry(0, -wantedHeight, width(), wantedHeight); + + d->updateSnapShot(); + + d->timeLine->setDirection(QTimeLine::Forward); + if (d->timeLine->state() == QTimeLine::NotRunning) { + d->timeLine->start(); + } +} + +void KMessageWidget::animatedHide() +{ + if (!style()->styleHint(QStyle::SH_Widget_Animate, 0, this)) { + hide(); + emit hideAnimationFinished(); + return; + } + + if (!isVisible()) { + hide(); + return; + } + + d->content->move(0, -d->content->height()); + d->updateSnapShot(); + + d->timeLine->setDirection(QTimeLine::Backward); + if (d->timeLine->state() == QTimeLine::NotRunning) { + d->timeLine->start(); + } +} + +bool KMessageWidget::isHideAnimationRunning() const +{ + return (d->timeLine->direction() == QTimeLine::Backward) + && (d->timeLine->state() == QTimeLine::Running); +} + +bool KMessageWidget::isShowAnimationRunning() const +{ + return (d->timeLine->direction() == QTimeLine::Forward) + && (d->timeLine->state() == QTimeLine::Running); +} + +QIcon KMessageWidget::icon() const +{ + return d->icon; +} + +void KMessageWidget::setIcon(const QIcon &icon) +{ + d->icon = icon; + if (d->icon.isNull()) { + d->iconLabel->hide(); + } else { + const int size = style()->pixelMetric(QStyle::PM_ToolBarIconSize); + d->iconLabel->setPixmap(d->icon.pixmap(size)); + d->iconLabel->show(); + } +} + +#include "moc_kmessagewidget.cpp" diff --git a/desktop-widgets/kmessagewidget.h b/desktop-widgets/kmessagewidget.h new file mode 100644 index 000000000..885d2a78f --- /dev/null +++ b/desktop-widgets/kmessagewidget.h @@ -0,0 +1,342 @@ +/* This file is part of the KDE libraries + * + * Copyright (c) 2011 Aurélien Gâteau <agateau@kde.org> + * Copyright (c) 2014 Dominik Haumann <dhaumann@kde.org> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + */ +#ifndef KMESSAGEWIDGET_H +#define KMESSAGEWIDGET_H + +#include <QFrame> + +class KMessageWidgetPrivate; + +/** + * @short A widget to provide feedback or propose opportunistic interactions. + * + * KMessageWidget can be used to provide inline positive or negative + * feedback, or to implement opportunistic interactions. + * + * As a feedback widget, KMessageWidget provides a less intrusive alternative + * to "OK Only" message boxes. If you want to avoid a modal KMessageBox, + * consider using KMessageWidget instead. + * + * Examples of KMessageWidget look as follows, all of them having an icon set + * with setIcon(), and the first three show a close button: + * + * \image html kmessagewidget.png "KMessageWidget with different message types" + * + * <b>Negative feedback</b> + * + * The KMessageWidget can be used as a secondary indicator of failure: the + * first indicator is usually the fact the action the user expected to happen + * did not happen. + * + * Example: User fills a form, clicks "Submit". + * + * @li Expected feedback: form closes + * @li First indicator of failure: form stays there + * @li Second indicator of failure: a KMessageWidget appears on top of the + * form, explaining the error condition + * + * When used to provide negative feedback, KMessageWidget should be placed + * close to its context. In the case of a form, it should appear on top of the + * form entries. + * + * KMessageWidget should get inserted in the existing layout. Space should not + * be reserved for it, otherwise it becomes "dead space", ignored by the user. + * KMessageWidget should also not appear as an overlay to prevent blocking + * access to elements the user needs to interact with to fix the failure. + * + * <b>Positive feedback</b> + * + * KMessageWidget can be used for positive feedback but it shouldn't be + * overused. It is often enough to provide feedback by simply showing the + * results of an action. + * + * Examples of acceptable uses: + * + * @li Confirm success of "critical" transactions + * @li Indicate completion of background tasks + * + * Example of unadapted uses: + * + * @li Indicate successful saving of a file + * @li Indicate a file has been successfully removed + * + * <b>Opportunistic interaction</b> + * + * Opportunistic interaction is the situation where the application suggests to + * the user an action he could be interested in perform, either based on an + * action the user just triggered or an event which the application noticed. + * + * Example of acceptable uses: + * + * @li A browser can propose remembering a recently entered password + * @li A music collection can propose ripping a CD which just got inserted + * @li A chat application may notify the user a "special friend" just connected + * + * @author Aurélien Gâteau <agateau@kde.org> + * @since 4.7 + */ +class KMessageWidget : public QFrame +{ + Q_OBJECT + Q_ENUMS(MessageType) + + Q_PROPERTY(QString text READ text WRITE setText) + Q_PROPERTY(bool wordWrap READ wordWrap WRITE setWordWrap) + Q_PROPERTY(bool closeButtonVisible READ isCloseButtonVisible WRITE setCloseButtonVisible) + Q_PROPERTY(MessageType messageType READ messageType WRITE setMessageType) + Q_PROPERTY(QIcon icon READ icon WRITE setIcon) +public: + + /** + * Available message types. + * The background colors are chosen depending on the message type. + */ + enum MessageType { + Positive, + Information, + Warning, + Error + }; + + /** + * Constructs a KMessageWidget with the specified @p parent. + */ + explicit KMessageWidget(QWidget *parent = 0); + + /** + * Constructs a KMessageWidget with the specified @p parent and + * contents @p text. + */ + explicit KMessageWidget(const QString &text, QWidget *parent = 0); + + /** + * Destructor. + */ + ~KMessageWidget(); + + /** + * Get the text of this message widget. + * @see setText() + */ + QString text() const; + + /** + * Check whether word wrap is enabled. + * + * If word wrap is enabled, the message widget wraps the displayed text + * as required to the available width of the widget. This is useful to + * avoid breaking widget layouts. + * + * @see setWordWrap() + */ + bool wordWrap() const; + + /** + * Check whether the close button is visible. + * + * @see setCloseButtonVisible() + */ + bool isCloseButtonVisible() const; + + /** + * Get the type of this message. + * By default, the type is set to KMessageWidget::Information. + * + * @see KMessageWidget::MessageType, setMessageType() + */ + MessageType messageType() const; + + /** + * Add @p action to the message widget. + * For each action a button is added to the message widget in the + * order the actions were added. + * + * @param action the action to add + * @see removeAction(), QWidget::actions() + */ + void addAction(QAction *action); + + /** + * Remove @p action from the message widget. + * + * @param action the action to remove + * @see KMessageWidget::MessageType, addAction(), setMessageType() + */ + void removeAction(QAction *action); + + /** + * Returns the preferred size of the message widget. + */ + QSize sizeHint() const Q_DECL_OVERRIDE; + + /** + * Returns the minimum size of the message widget. + */ + QSize minimumSizeHint() const Q_DECL_OVERRIDE; + + /** + * Returns the required height for @p width. + * @param width the width in pixels + */ + int heightForWidth(int width) const Q_DECL_OVERRIDE; + + /** + * The icon shown on the left of the text. By default, no icon is shown. + * @since 4.11 + */ + QIcon icon() const; + + /** + * Check whether the hide animation started by calling animatedHide() + * is still running. If animations are disabled, this function always + * returns @e false. + * + * @see animatedHide(), hideAnimationFinished() + * @since 5.0 + */ + bool isHideAnimationRunning() const; + + /** + * Check whether the show animation started by calling animatedShow() + * is still running. If animations are disabled, this function always + * returns @e false. + * + * @see animatedShow(), showAnimationFinished() + * @since 5.0 + */ + bool isShowAnimationRunning() const; + +public Q_SLOTS: + /** + * Set the text of the message widget to @p text. + * If the message widget is already visible, the text changes on the fly. + * + * @param text the text to display, rich text is allowed + * @see text() + */ + void setText(const QString &text); + + /** + * Set word wrap to @p wordWrap. If word wrap is enabled, the text() + * of the message widget is wrapped to fit the available width. + * If word wrap is disabled, the message widget's minimum size is + * such that the entire text fits. + * + * @param wordWrap disable/enable word wrap + * @see wordWrap() + */ + void setWordWrap(bool wordWrap); + + /** + * Set the visibility of the close button. If @p visible is @e true, + * a close button is shown that calls animatedHide() if clicked. + * + * @see closeButtonVisible(), animatedHide() + */ + void setCloseButtonVisible(bool visible); + + /** + * Set the message type to @p type. + * By default, the message type is set to KMessageWidget::Information. + * + * @see messageType(), KMessageWidget::MessageType + */ + void setMessageType(KMessageWidget::MessageType type); + + /** + * Show the widget using an animation. + */ + void animatedShow(); + + /** + * Hide the widget using an animation. + */ + void animatedHide(); + + /** + * Define an icon to be shown on the left of the text + * @since 4.11 + */ + void setIcon(const QIcon &icon); + +Q_SIGNALS: + /** + * This signal is emitted when the user clicks a link in the text label. + * The URL referred to by the href anchor is passed in contents. + * @param contents text of the href anchor + * @see QLabel::linkActivated() + * @since 4.10 + */ + void linkActivated(const QString &contents); + + /** + * This signal is emitted when the user hovers over a link in the text label. + * The URL referred to by the href anchor is passed in contents. + * @param contents text of the href anchor + * @see QLabel::linkHovered() + * @since 4.11 + */ + void linkHovered(const QString &contents); + + /** + * This signal is emitted when the hide animation is finished, started by + * calling animatedHide(). If animations are disabled, this signal is + * emitted immediately after the message widget got hidden. + * + * @note This signal is @e not emitted if the widget was hidden by + * calling hide(), so this signal is only useful in conjunction + * with animatedHide(). + * + * @see animatedHide() + * @since 5.0 + */ + void hideAnimationFinished(); + + /** + * This signal is emitted when the show animation is finished, started by + * calling animatedShow(). If animations are disabled, this signal is + * emitted immediately after the message widget got shown. + * + * @note This signal is @e not emitted if the widget was shown by + * calling show(), so this signal is only useful in conjunction + * with animatedShow(). + * + * @see animatedShow() + * @since 5.0 + */ + void showAnimationFinished(); + +protected: + void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE; + + bool event(QEvent *event) Q_DECL_OVERRIDE; + + void resizeEvent(QResizeEvent *event) Q_DECL_OVERRIDE; + +private: + KMessageWidgetPrivate *const d; + friend class KMessageWidgetPrivate; + + Q_PRIVATE_SLOT(d, void slotTimeLineChanged(qreal)) + Q_PRIVATE_SLOT(d, void slotTimeLineFinished()) +}; + +#endif /* KMESSAGEWIDGET_H */ diff --git a/desktop-widgets/listfilter.ui b/desktop-widgets/listfilter.ui new file mode 100644 index 000000000..48d813d21 --- /dev/null +++ b/desktop-widgets/listfilter.ui @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>FilterWidget</class> + <widget class="QWidget" name="FilterWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>166</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>5</number> + </property> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Text label</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="filterInternalList"> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + <property name="placeholderText"> + <string>Filter this list</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QListView" name="filterList"/> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/desktop-widgets/locationInformation.ui b/desktop-widgets/locationInformation.ui new file mode 100644 index 000000000..58d065648 --- /dev/null +++ b/desktop-widgets/locationInformation.ui @@ -0,0 +1,156 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>LocationInformation</class> + <widget class="QGroupBox" name="LocationInformation"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>556</width> + <height>707</height> + </rect> + </property> + <property name="windowTitle"> + <string>GroupBox</string> + </property> + <property name="title"> + <string/> + </property> + <layout class="QGridLayout" name="gridLayout"> + <property name="horizontalSpacing"> + <number>6</number> + </property> + <property name="verticalSpacing"> + <number>4</number> + </property> + <item row="1" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Name</string> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Description</string> + </property> + </widget> + </item> + <item row="6" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Notes</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="diveSiteCoordinates"/> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Coordinates</string> + </property> + </widget> + </item> + <item row="6" column="1" rowspan="2" colspan="2"> + <widget class="QPlainTextEdit" name="diveSiteNotes"/> + </item> + <item row="1" column="1" colspan="2"> + <widget class="QLineEdit" name="diveSiteName"/> + </item> + <item row="4" column="1" colspan="2"> + <widget class="QLineEdit" name="diveSiteDescription"/> + </item> + <item row="2" column="2"> + <widget class="QToolButton" name="geoCodeButton"> + <property name="toolTip"> + <string>Reverse geo lookup</string> + </property> + <property name="text"> + <string>...</string> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/satellite</normaloff>:/satellite</iconset> + </property> + </widget> + </item> + <item row="0" column="0" colspan="3"> + <widget class="KMessageWidget" name="diveSiteMessage"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item row="8" column="0" colspan="3"> + <widget class="QGroupBox" name="diveSiteGroupBox"> + <property name="title"> + <string>Dive sites on same coordinates</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QListView" name="diveSiteListView"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::MultiSelection</enum> + </property> + <property name="modelColumn"> + <number>0</number> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="7" column="0"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Tags</string> + </property> + </widget> + </item> + <item row="3" column="1" colspan="2"> + <widget class="QLabel" name="locationTags"> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>KMessageWidget</class> + <extends>QFrame</extends> + <header>kmessagewidget.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <resources> + <include location="../subsurface.qrc"/> + </resources> + <connections/> +</ui> diff --git a/desktop-widgets/locationinformation.cpp b/desktop-widgets/locationinformation.cpp new file mode 100644 index 000000000..aee0b7328 --- /dev/null +++ b/desktop-widgets/locationinformation.cpp @@ -0,0 +1,618 @@ +#include "locationinformation.h" +#include "dive.h" +#include "mainwindow.h" +#include "divelistview.h" +#include "qthelper.h" +#include "globe.h" +#include "filtermodels.h" +#include "divelocationmodel.h" +#include "divesitehelpers.h" +#include "modeldelegates.h" + +#include <QDebug> +#include <QShowEvent> +#include <QItemSelectionModel> +#include <qmessagebox.h> +#include <cstdlib> +#include <QDesktopWidget> +#include <QScrollBar> + +LocationInformationWidget::LocationInformationWidget(QWidget *parent) : QGroupBox(parent), modified(false) +{ + ui.setupUi(this); + ui.diveSiteMessage->setCloseButtonVisible(false); + + acceptAction = new QAction(tr("Apply changes"), this); + connect(acceptAction, SIGNAL(triggered(bool)), this, SLOT(acceptChanges())); + + rejectAction = new QAction(tr("Discard changes"), this); + connect(rejectAction, SIGNAL(triggered(bool)), this, SLOT(rejectChanges())); + + ui.diveSiteMessage->setText(tr("Dive site management")); + ui.diveSiteMessage->addAction(acceptAction); + ui.diveSiteMessage->addAction(rejectAction); + + connect(this, SIGNAL(startFilterDiveSite(uint32_t)), MultiFilterSortModel::instance(), SLOT(startFilterDiveSite(uint32_t))); + connect(this, SIGNAL(stopFilterDiveSite()), MultiFilterSortModel::instance(), SLOT(stopFilterDiveSite())); + connect(ui.geoCodeButton, SIGNAL(clicked()), this, SLOT(reverseGeocode())); + + SsrfSortFilterProxyModel *filter_model = new SsrfSortFilterProxyModel(this); + filter_model->setSourceModel(LocationInformationModel::instance()); + filter_model->setFilterRow(filter_same_gps_cb); + ui.diveSiteListView->setModel(filter_model); + ui.diveSiteListView->setModelColumn(LocationInformationModel::NAME); + ui.diveSiteListView->installEventFilter(this); +#ifndef NO_MARBLE + // Globe Management Code. + connect(this, &LocationInformationWidget::requestCoordinates, + GlobeGPS::instance(), &GlobeGPS::prepareForGetDiveCoordinates); + connect(this, &LocationInformationWidget::endRequestCoordinates, + GlobeGPS::instance(), &GlobeGPS::endGetDiveCoordinates); + connect(GlobeGPS::instance(), &GlobeGPS::coordinatesChanged, + this, &LocationInformationWidget::updateGpsCoordinates); + connect(this, &LocationInformationWidget::endEditDiveSite, + GlobeGPS::instance(), &GlobeGPS::repopulateLabels); +#endif +} + +bool LocationInformationWidget::eventFilter(QObject *, QEvent *ev) +{ + if (ev->type() == QEvent::ContextMenu) { + QContextMenuEvent *ctx = (QContextMenuEvent *)ev; + QMenu contextMenu; + contextMenu.addAction(tr("Merge into current site"), this, SLOT(mergeSelectedDiveSites())); + contextMenu.exec(ctx->globalPos()); + return true; + } + return false; +} + +void LocationInformationWidget::mergeSelectedDiveSites() +{ + if (QMessageBox::warning(MainWindow::instance(), tr("Merging dive sites"), + tr("You are about to merge dive sites, you can't undo that action \n Are you sure you want to continue?"), + QMessageBox::Ok, QMessageBox::Cancel) != QMessageBox::Ok) + return; + + QModelIndexList selection = ui.diveSiteListView->selectionModel()->selectedIndexes(); + uint32_t *selected_dive_sites = (uint32_t *)malloc(sizeof(uint32_t) * selection.count()); + int i = 0; + Q_FOREACH (const QModelIndex &idx, selection) { + selected_dive_sites[i] = (uint32_t)idx.data(LocationInformationModel::UUID_ROLE).toInt(); + i++; + } + merge_dive_sites(displayed_dive_site.uuid, selected_dive_sites, i); + LocationInformationModel::instance()->update(); + QSortFilterProxyModel *m = (QSortFilterProxyModel *)ui.diveSiteListView->model(); + m->invalidate(); + free(selected_dive_sites); +} + +void LocationInformationWidget::updateLabels() +{ + if (displayed_dive_site.name) + ui.diveSiteName->setText(displayed_dive_site.name); + else + ui.diveSiteName->clear(); + if (displayed_dive_site.description) + ui.diveSiteDescription->setText(displayed_dive_site.description); + else + ui.diveSiteDescription->clear(); + if (displayed_dive_site.notes) + ui.diveSiteNotes->setPlainText(displayed_dive_site.notes); + else + ui.diveSiteNotes->clear(); + if (displayed_dive_site.latitude.udeg || displayed_dive_site.longitude.udeg) { + const char *coords = printGPSCoords(displayed_dive_site.latitude.udeg, displayed_dive_site.longitude.udeg); + ui.diveSiteCoordinates->setText(coords); + free((void *)coords); + } else { + ui.diveSiteCoordinates->clear(); + } + + ui.locationTags->setText(constructLocationTags(displayed_dive_site.uuid)); + + emit startFilterDiveSite(displayed_dive_site.uuid); + emit startEditDiveSite(displayed_dive_site.uuid); +} + +void LocationInformationWidget::updateGpsCoordinates() +{ + QString oldText = ui.diveSiteCoordinates->text(); + const char *coords = printGPSCoords(displayed_dive_site.latitude.udeg, displayed_dive_site.longitude.udeg); + ui.diveSiteCoordinates->setText(coords); + free((void *)coords); + if (oldText != ui.diveSiteCoordinates->text()) + markChangedWidget(ui.diveSiteCoordinates); +} + +void LocationInformationWidget::acceptChanges() +{ + char *uiString; + struct dive_site *currentDs; + uiString = ui.diveSiteName->text().toUtf8().data(); + + if (get_dive_site_by_uuid(displayed_dive_site.uuid) != NULL) + currentDs = get_dive_site_by_uuid(displayed_dive_site.uuid); + else + currentDs = get_dive_site_by_uuid(create_dive_site_from_current_dive(uiString)); + + currentDs->latitude = displayed_dive_site.latitude; + currentDs->longitude = displayed_dive_site.longitude; + if (!same_string(uiString, currentDs->name)) { + free(currentDs->name); + currentDs->name = copy_string(uiString); + } + uiString = ui.diveSiteDescription->text().toUtf8().data(); + if (!same_string(uiString, currentDs->description)) { + free(currentDs->description); + currentDs->description = copy_string(uiString); + } + uiString = ui.diveSiteNotes->document()->toPlainText().toUtf8().data(); + if (!same_string(uiString, currentDs->notes)) { + free(currentDs->notes); + currentDs->notes = copy_string(uiString); + } + if (!ui.diveSiteCoordinates->text().isEmpty()) { + double lat, lon; + parseGpsText(ui.diveSiteCoordinates->text(), &lat, &lon); + currentDs->latitude.udeg = lat * 1000000.0; + currentDs->longitude.udeg = lon * 1000000.0; + } + if (dive_site_is_empty(currentDs)) { + LocationInformationModel::instance()->removeRow(get_divesite_idx(currentDs)); + displayed_dive.dive_site_uuid = 0; + } + copy_dive_site(currentDs, &displayed_dive_site); + mark_divelist_changed(true); + resetState(); + emit endRequestCoordinates(); + emit endEditDiveSite(); + emit stopFilterDiveSite(); + emit coordinatesChanged(); +} + +void LocationInformationWidget::rejectChanges() +{ + resetState(); + emit endRequestCoordinates(); + emit stopFilterDiveSite(); + emit endEditDiveSite(); + emit coordinatesChanged(); +} + +void LocationInformationWidget::showEvent(QShowEvent *ev) +{ + if (displayed_dive_site.uuid) { + updateLabels(); + ui.geoCodeButton->setEnabled(dive_site_has_gps_location(&displayed_dive_site)); + QSortFilterProxyModel *m = qobject_cast<QSortFilterProxyModel *>(ui.diveSiteListView->model()); + emit startFilterDiveSite(displayed_dive_site.uuid); + if (m) + m->invalidate(); + } + emit requestCoordinates(); + + QGroupBox::showEvent(ev); +} + +void LocationInformationWidget::markChangedWidget(QWidget *w) +{ + QPalette p; + qreal h, s, l, a; + if (!modified) + enableEdition(); + qApp->palette().color(QPalette::Text).getHslF(&h, &s, &l, &a); + p.setBrush(QPalette::Base, (l <= 0.3) ? QColor(Qt::yellow).lighter() : (l <= 0.6) ? QColor(Qt::yellow).light() : /* else */ QColor(Qt::yellow).darker(300)); + w->setPalette(p); + modified = true; +} + +void LocationInformationWidget::resetState() +{ + modified = false; + resetPallete(); + MainWindow::instance()->dive_list()->setEnabled(true); + MainWindow::instance()->setEnabledToolbar(true); + ui.diveSiteMessage->setText(tr("Dive site management")); +} + +void LocationInformationWidget::enableEdition() +{ + MainWindow::instance()->dive_list()->setEnabled(false); + MainWindow::instance()->setEnabledToolbar(false); + ui.diveSiteMessage->setText(tr("You are editing a dive site")); +} + +void LocationInformationWidget::on_diveSiteCoordinates_textChanged(const QString &text) +{ + uint lat = displayed_dive_site.latitude.udeg; + uint lon = displayed_dive_site.longitude.udeg; + const char *coords = printGPSCoords(lat, lon); + if (!same_string(qPrintable(text), coords)) { + double latitude, longitude; + if (parseGpsText(text, &latitude, &longitude)) { + displayed_dive_site.latitude.udeg = latitude * 1000000; + displayed_dive_site.longitude.udeg = longitude * 1000000; + markChangedWidget(ui.diveSiteCoordinates); + emit coordinatesChanged(); + ui.geoCodeButton->setEnabled(latitude != 0 && longitude != 0); + } else { + ui.geoCodeButton->setEnabled(false); + } + } + free((void *)coords); +} + +void LocationInformationWidget::on_diveSiteDescription_textChanged(const QString &text) +{ + if (!same_string(qPrintable(text), displayed_dive_site.description)) + markChangedWidget(ui.diveSiteDescription); +} + +void LocationInformationWidget::on_diveSiteName_textChanged(const QString &text) +{ + if (!same_string(qPrintable(text), displayed_dive_site.name)) + markChangedWidget(ui.diveSiteName); +} + +void LocationInformationWidget::on_diveSiteNotes_textChanged() +{ + if (!same_string(qPrintable(ui.diveSiteNotes->toPlainText()), displayed_dive_site.notes)) + markChangedWidget(ui.diveSiteNotes); +} + +void LocationInformationWidget::resetPallete() +{ + QPalette p; + ui.diveSiteCoordinates->setPalette(p); + ui.diveSiteDescription->setPalette(p); + ui.diveSiteName->setPalette(p); + ui.diveSiteNotes->setPalette(p); +} + +void LocationInformationWidget::reverseGeocode() +{ + ReverseGeoLookupThread *geoLookup = ReverseGeoLookupThread::instance(); + geoLookup->lookup(&displayed_dive_site); + updateLabels(); +} + +DiveLocationFilterProxyModel::DiveLocationFilterProxyModel(QObject *parent) +{ +} + +DiveLocationLineEdit *location_line_edit = 0; + +bool DiveLocationFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + if (source_row == 0) + return true; + + QString sourceString = sourceModel()->index(source_row, DiveLocationModel::NAME).data(Qt::DisplayRole).toString(); + return sourceString.toLower().startsWith(location_line_edit->text().toLower()); +} + +bool DiveLocationFilterProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const +{ + return source_left.data().toString() <= source_right.data().toString(); +} + + +DiveLocationModel::DiveLocationModel(QObject *o) +{ + resetModel(); +} + +void DiveLocationModel::resetModel() +{ + beginResetModel(); + endResetModel(); +} + +QVariant DiveLocationModel::data(const QModelIndex &index, int role) const +{ + static const QIcon plusIcon(":plus"); + static const QIcon geoCode(":geocode"); + + if (index.row() <= 1) { // two special cases. + if (index.column() == UUID) { + return RECENTLY_ADDED_DIVESITE; + } + switch (role) { + case Qt::DisplayRole: + return new_ds_value[index.row()]; + case Qt::ToolTipRole: + return displayed_dive_site.uuid ? + tr("Create a new dive site, copying relevant information from the current dive.") : + tr("Create a new dive site with this name"); + case Qt::DecorationRole: + return plusIcon; + } + } + + // The dive sites are -2 because of the first two items. + struct dive_site *ds = get_dive_site(index.row() - 2); + switch (role) { + case Qt::EditRole: + case Qt::DisplayRole: + switch (index.column()) { + case UUID: + return ds->uuid; + case NAME: + return ds->name; + case LATITUDE: + return ds->latitude.udeg; + case LONGITUDE: + return ds->longitude.udeg; + case DESCRIPTION: + return ds->description; + case NOTES: + return ds->name; + } + break; + case Qt::DecorationRole: { + if (dive_site_has_gps_location(ds)) + return geoCode; + } + } + return QVariant(); +} + +int DiveLocationModel::columnCount(const QModelIndex &parent) const +{ + return COLUMNS; +} + +int DiveLocationModel::rowCount(const QModelIndex &parent) const +{ + return dive_site_table.nr + 2; +} + +bool DiveLocationModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid()) + return false; + if (index.row() > 1) + return false; + + new_ds_value[index.row()] = value.toString(); + + dataChanged(index, index); + return true; +} + +DiveLocationLineEdit::DiveLocationLineEdit(QWidget *parent) : QLineEdit(parent), + proxy(new DiveLocationFilterProxyModel()), + model(new DiveLocationModel()), + view(new DiveLocationListView()), + currType(NO_DIVE_SITE) +{ + currUuid = 0; + location_line_edit = this; + + proxy->setSourceModel(model); + proxy->setFilterKeyColumn(DiveLocationModel::NAME); + + view->setModel(proxy); + view->setModelColumn(DiveLocationModel::NAME); + view->setItemDelegate(new LocationFilterDelegate()); + view->setEditTriggers(QAbstractItemView::NoEditTriggers); + view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + view->setSelectionBehavior(QAbstractItemView::SelectRows); + view->setSelectionMode(QAbstractItemView::SingleSelection); + view->setParent(0, Qt::Popup); + view->installEventFilter(this); + view->setFocusPolicy(Qt::NoFocus); + view->setFocusProxy(this); + view->setMouseTracking(true); + + connect(this, &QLineEdit::textEdited, this, &DiveLocationLineEdit::setTemporaryDiveSiteName); + connect(view, &QAbstractItemView::activated, this, &DiveLocationLineEdit::itemActivated); + connect(view, &QAbstractItemView::entered, this, &DiveLocationLineEdit::entered); + connect(view, &DiveLocationListView::currentIndexChanged, this, &DiveLocationLineEdit::currentChanged); +} + +bool DiveLocationLineEdit::eventFilter(QObject *o, QEvent *e) +{ + if (e->type() == QEvent::KeyPress) { + QKeyEvent *keyEv = (QKeyEvent *)e; + + if (keyEv->key() == Qt::Key_Escape) { + view->hide(); + return true; + } + + if (keyEv->key() == Qt::Key_Return || + keyEv->key() == Qt::Key_Enter) { +#if __APPLE__ + // for some reason it seems like on a Mac hitting return/enter + // doesn't call 'activated' for that index. so let's do it manually + if (view->currentIndex().isValid()) + itemActivated(view->currentIndex()); +#endif + view->hide(); + return false; + } + + if (keyEv->key() == Qt::Key_Tab) { + itemActivated(view->currentIndex()); + view->hide(); + return false; + } + event(e); + } else if (e->type() == QEvent::MouseButtonPress) { + if (!view->underMouse()) { + view->hide(); + return true; + } + } + + return false; +} + +void DiveLocationLineEdit::focusOutEvent(QFocusEvent *ev) +{ + if (!view->isVisible()) { + QLineEdit::focusOutEvent(ev); + } +} + +void DiveLocationLineEdit::itemActivated(const QModelIndex &index) +{ + QModelIndex idx = index; + if (index.column() == DiveLocationModel::UUID) + idx = index.model()->index(index.row(), DiveLocationModel::NAME); + + QModelIndex uuidIndex = index.model()->index(index.row(), DiveLocationModel::UUID); + uint32_t uuid = uuidIndex.data().toInt(); + currType = uuid == 1 ? NEW_DIVE_SITE : EXISTING_DIVE_SITE; + currUuid = uuid; + setText(idx.data().toString()); + if (currUuid == NEW_DIVE_SITE) + qDebug() << "Setting a New dive site"; + else + qDebug() << "Setting a Existing dive site"; + if (view->isVisible()) + view->hide(); + emit diveSiteSelected(currUuid); +} + +void DiveLocationLineEdit::refreshDiveSiteCache() +{ + model->resetModel(); +} + +static struct dive_site *get_dive_site_name_start_which_str(const QString &str) +{ + struct dive_site *ds; + int i; + for_each_dive_site (i, ds) { + QString dsName(ds->name); + if (dsName.toLower().startsWith(str.toLower())) { + return ds; + } + } + return NULL; +} + +void DiveLocationLineEdit::setTemporaryDiveSiteName(const QString &s) +{ + QModelIndex i0 = model->index(0, DiveLocationModel::NAME); + QModelIndex i1 = model->index(1, DiveLocationModel::NAME); + model->setData(i0, text()); + + QString i1_name = INVALID_DIVE_SITE_NAME; + if (struct dive_site *ds = get_dive_site_name_start_which_str(text())) { + const QString orig_name = QString(ds->name).toLower(); + const QString new_name = text().toLower(); + if (new_name != orig_name) + i1_name = QString(ds->name); + } + + model->setData(i1, i1_name); + proxy->invalidate(); + fixPopupPosition(); + if (!view->isVisible()) + view->show(); +} + +void DiveLocationLineEdit::keyPressEvent(QKeyEvent *ev) +{ + QLineEdit::keyPressEvent(ev); + if (ev->key() != Qt::Key_Left && + ev->key() != Qt::Key_Right && + ev->key() != Qt::Key_Escape && + ev->key() != Qt::Key_Return) { + + if (ev->key() != Qt::Key_Up && ev->key() != Qt::Key_Down) { + currType = NEW_DIVE_SITE; + currUuid = RECENTLY_ADDED_DIVESITE; + } else { + showPopup(); + } + } else if (ev->key() == Qt::Key_Escape) { + view->hide(); + } +} + +void DiveLocationLineEdit::fixPopupPosition() +{ + const QRect screen = QApplication::desktop()->availableGeometry(this); + const int maxVisibleItems = 5; + Qt::LayoutDirection dir = layoutDirection(); + QPoint pos; + int rh, w; + int h = (view->sizeHintForRow(0) * qMin(maxVisibleItems, view->model()->rowCount()) + 3) + 3; + QScrollBar *hsb = view->horizontalScrollBar(); + if (hsb && hsb->isVisible()) + h += view->horizontalScrollBar()->sizeHint().height(); + + rh = height(); + pos = mapToGlobal(QPoint(0, height() - 2)); + w = width(); + + if (w > screen.width()) + w = screen.width(); + if ((pos.x() + w) > (screen.x() + screen.width())) + pos.setX(screen.x() + screen.width() - w); + if (pos.x() < screen.x()) + pos.setX(screen.x()); + + int top = pos.y() - rh - screen.top() + 2; + int bottom = screen.bottom() - pos.y(); + h = qMax(h, view->minimumHeight()); + if (h > bottom) { + h = qMin(qMax(top, bottom), h); + if (top > bottom) + pos.setY(pos.y() - h - rh + 2); + } + + view->setGeometry(pos.x(), pos.y(), w, h); + if (!view->currentIndex().isValid() && view->model()->rowCount()) { + view->setCurrentIndex(view->model()->index(0, 0)); + } +} + +void DiveLocationLineEdit::setCurrentDiveSiteUuid(uint32_t uuid) +{ + currUuid = uuid; + if (uuid == 0) { + currType = NO_DIVE_SITE; + } + struct dive_site *ds = get_dive_site_by_uuid(uuid); + if (!ds) + clear(); + else + setText(ds->name); +} + +void DiveLocationLineEdit::showPopup() +{ + fixPopupPosition(); + if (!view->isVisible()) { + setTemporaryDiveSiteName(text()); + proxy->invalidate(); + view->show(); + } +} + +DiveLocationLineEdit::DiveSiteType DiveLocationLineEdit::currDiveSiteType() const +{ + return currType; +} + +uint32_t DiveLocationLineEdit::currDiveSiteUuid() const +{ + return currUuid; +} + +DiveLocationListView::DiveLocationListView(QWidget *parent) +{ +} + +void DiveLocationListView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) +{ + QListView::currentChanged(current, previous); + emit currentIndexChanged(current); +} diff --git a/desktop-widgets/locationinformation.h b/desktop-widgets/locationinformation.h new file mode 100644 index 000000000..243df939b --- /dev/null +++ b/desktop-widgets/locationinformation.h @@ -0,0 +1,114 @@ +#ifndef LOCATIONINFORMATION_H +#define LOCATIONINFORMATION_H + +#include "ui_locationInformation.h" +#include <stdint.h> +#include <QAbstractListModel> +#include <QSortFilterProxyModel> + +class LocationInformationWidget : public QGroupBox { +Q_OBJECT +public: + LocationInformationWidget(QWidget *parent = 0); + virtual bool eventFilter(QObject*, QEvent*); + +protected: + void showEvent(QShowEvent *); + +public slots: + void acceptChanges(); + void rejectChanges(); + void updateGpsCoordinates(); + void markChangedWidget(QWidget *w); + void enableEdition(); + void resetState(); + void resetPallete(); + void on_diveSiteCoordinates_textChanged(const QString& text); + void on_diveSiteDescription_textChanged(const QString& text); + void on_diveSiteName_textChanged(const QString& text); + void on_diveSiteNotes_textChanged(); + void reverseGeocode(); + void mergeSelectedDiveSites(); +private slots: + void updateLabels(); +signals: + void startEditDiveSite(uint32_t uuid); + void endEditDiveSite(); + void coordinatesChanged(); + void startFilterDiveSite(uint32_t uuid); + void stopFilterDiveSite(); + void requestCoordinates(); + void endRequestCoordinates(); + +private: + Ui::LocationInformation ui; + bool modified; + QAction *acceptAction, *rejectAction; +}; + +class DiveLocationFilterProxyModel : public QSortFilterProxyModel { + Q_OBJECT +public: + DiveLocationFilterProxyModel(QObject *parent = 0); + virtual bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const; + virtual bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const; +}; + +class DiveLocationModel : public QAbstractTableModel { + Q_OBJECT +public: + enum columns{UUID, NAME, LATITUDE, LONGITUDE, DESCRIPTION, NOTES, COLUMNS}; + DiveLocationModel(QObject *o = 0); + void resetModel(); + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + int rowCount(const QModelIndex& parent = QModelIndex()) const; + int columnCount(const QModelIndex& parent = QModelIndex()) const; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole); +private: + QString new_ds_value[2]; +}; + +class DiveLocationListView : public QListView { + Q_OBJECT +public: + DiveLocationListView(QWidget *parent = 0); +protected: + virtual void currentChanged(const QModelIndex& current, const QModelIndex& previous); +signals: + void currentIndexChanged(const QModelIndex& current); +}; + +class DiveLocationLineEdit : public QLineEdit { + Q_OBJECT +public: + enum DiveSiteType { NO_DIVE_SITE, NEW_DIVE_SITE, EXISTING_DIVE_SITE }; + DiveLocationLineEdit(QWidget *parent =0 ); + void refreshDiveSiteCache(); + void setTemporaryDiveSiteName(const QString& s); + bool eventFilter(QObject*, QEvent*); + void itemActivated(const QModelIndex& index); + DiveSiteType currDiveSiteType() const; + uint32_t currDiveSiteUuid() const; + void fixPopupPosition(); + void setCurrentDiveSiteUuid(uint32_t uuid); + +signals: + void diveSiteSelected(uint32_t uuid); + void entered(const QModelIndex& index); + void currentChanged(const QModelIndex& index); + +protected: + void keyPressEvent(QKeyEvent *ev); + void focusOutEvent(QFocusEvent *ev); + void showPopup(); + +private: + using QLineEdit::setText; + DiveLocationFilterProxyModel *proxy; + DiveLocationModel *model; + DiveLocationListView *view; + DiveSiteType currType; + uint32_t currUuid; +}; + +#endif diff --git a/desktop-widgets/maintab.cpp b/desktop-widgets/maintab.cpp new file mode 100644 index 000000000..0afb7b4c0 --- /dev/null +++ b/desktop-widgets/maintab.cpp @@ -0,0 +1,1612 @@ +/* + * maintab.cpp + * + * classes for the "notebook" area of the main window of Subsurface + * + */ +#include "maintab.h" +#include "mainwindow.h" +#include "globe.h" +#include "helpers.h" +#include "statistics.h" +#include "modeldelegates.h" +#include "diveplannermodel.h" +#include "divelistview.h" +#include "display.h" +#include "profile/profilewidget2.h" +#include "diveplanner.h" +#include "divesitehelpers.h" +#include "cylindermodel.h" +#include "weightmodel.h" +#include "divepicturemodel.h" +#include "divecomputerextradatamodel.h" +#include "divelocationmodel.h" +#include "divesite.h" +#include "locationinformation.h" +#include "divesite.h" + +#include <QCompleter> +#include <QSettings> +#include <QScrollBar> +#include <QShortcut> +#include <QMessageBox> +#include <QDesktopServices> +#include <QStringList> + +MainTab::MainTab(QWidget *parent) : QTabWidget(parent), + weightModel(new WeightModel(this)), + cylindersModel(CylindersModel::instance()), + extraDataModel(new ExtraDataModel(this)), + editMode(NONE), + divePictureModel(DivePictureModel::instance()), + copyPaste(false), + currentTrip(0) +{ + ui.setupUi(this); + ui.dateEdit->setDisplayFormat(getDateFormat()); + + memset(&displayed_dive, 0, sizeof(displayed_dive)); + memset(&displayedTrip, 0, sizeof(displayedTrip)); + + ui.cylinders->setModel(cylindersModel); + ui.weights->setModel(weightModel); + ui.photosView->setModel(divePictureModel); + connect(ui.photosView, SIGNAL(photoDoubleClicked(QString)), this, SLOT(photoDoubleClicked(QString))); + ui.extraData->setModel(extraDataModel); + closeMessage(); + + connect(ui.editDiveSiteButton, SIGNAL(clicked()), MainWindow::instance(), SIGNAL(startDiveSiteEdit())); +#ifndef NO_MARBLE + connect(ui.location, &DiveLocationLineEdit::entered, GlobeGPS::instance(), &GlobeGPS::centerOnIndex); + connect(ui.location, &DiveLocationLineEdit::currentChanged, GlobeGPS::instance(), &GlobeGPS::centerOnIndex); +#endif + + QAction *action = new QAction(tr("Apply changes"), this); + connect(action, SIGNAL(triggered(bool)), this, SLOT(acceptChanges())); + addMessageAction(action); + + action = new QAction(tr("Discard changes"), this); + connect(action, SIGNAL(triggered(bool)), this, SLOT(rejectChanges())); + addMessageAction(action); + + QShortcut *closeKey = new QShortcut(QKeySequence(Qt::Key_Escape), this); + connect(closeKey, SIGNAL(activated()), this, SLOT(escDetected())); + + if (qApp->style()->objectName() == "oxygen") + setDocumentMode(true); + else + setDocumentMode(false); + + // we start out with the fields read-only; once things are + // filled from a dive, they are made writeable + setEnabled(false); + + Q_FOREACH (QObject *obj, ui.statisticsTab->children()) { + QLabel *label = qobject_cast<QLabel *>(obj); + if (label) + label->setAlignment(Qt::AlignHCenter); + } + ui.cylinders->setTitle(tr("Cylinders")); + ui.cylinders->setBtnToolTip(tr("Add cylinder")); + connect(ui.cylinders, SIGNAL(addButtonClicked()), this, SLOT(addCylinder_clicked())); + + ui.weights->setTitle(tr("Weights")); + ui.weights->setBtnToolTip(tr("Add weight system")); + connect(ui.weights, SIGNAL(addButtonClicked()), this, SLOT(addWeight_clicked())); + + // This needs to be the same order as enum dive_comp_type in dive.h! + ui.DiveType->insertItems(0, QStringList() << tr("OC") << tr("CCR") << tr("pSCR") << tr("Freedive")); + connect(ui.DiveType, SIGNAL(currentIndexChanged(int)), this, SLOT(divetype_Changed(int))); + + connect(ui.cylinders->view(), SIGNAL(clicked(QModelIndex)), this, SLOT(editCylinderWidget(QModelIndex))); + connect(ui.weights->view(), SIGNAL(clicked(QModelIndex)), this, SLOT(editWeightWidget(QModelIndex))); + + ui.cylinders->view()->setItemDelegateForColumn(CylindersModel::TYPE, new TankInfoDelegate(this)); + ui.cylinders->view()->setItemDelegateForColumn(CylindersModel::USE, new TankUseDelegate(this)); + ui.weights->view()->setItemDelegateForColumn(WeightModel::TYPE, new WSInfoDelegate(this)); + ui.cylinders->view()->setColumnHidden(CylindersModel::DEPTH, true); + completers.buddy = new QCompleter(&buddyModel, ui.buddy); + completers.divemaster = new QCompleter(&diveMasterModel, ui.divemaster); + completers.suit = new QCompleter(&suitModel, ui.suit); + completers.tags = new QCompleter(&tagModel, ui.tagWidget); + completers.buddy->setCaseSensitivity(Qt::CaseInsensitive); + completers.divemaster->setCaseSensitivity(Qt::CaseInsensitive); + completers.suit->setCaseSensitivity(Qt::CaseInsensitive); + completers.tags->setCaseSensitivity(Qt::CaseInsensitive); + ui.buddy->setCompleter(completers.buddy); + ui.divemaster->setCompleter(completers.divemaster); + ui.suit->setCompleter(completers.suit); + ui.tagWidget->setCompleter(completers.tags); + ui.diveNotesMessage->hide(); + ui.diveEquipmentMessage->hide(); + ui.diveInfoMessage->hide(); + ui.diveStatisticsMessage->hide(); + setMinimumHeight(0); + setMinimumWidth(0); + + // Current display of things on Gnome3 looks like shit, so + // let`s fix that. + if (isGnome3Session()) { + QPalette p; + p.setColor(QPalette::Window, QColor(Qt::white)); + ui.scrollArea->viewport()->setPalette(p); + ui.scrollArea_2->viewport()->setPalette(p); + ui.scrollArea_3->viewport()->setPalette(p); + ui.scrollArea_4->viewport()->setPalette(p); + + // GroupBoxes in Gnome3 looks like I'v drawn them... + static const QString gnomeCss( + "QGroupBox {" + " background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1," + " stop: 0 #E0E0E0, stop: 1 #FFFFFF);" + " border: 2px solid gray;" + " border-radius: 5px;" + " margin-top: 1ex;" + "}" + "QGroupBox::title {" + " subcontrol-origin: margin;" + " subcontrol-position: top center;" + " padding: 0 3px;" + " background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1," + " stop: 0 #E0E0E0, stop: 1 #FFFFFF);" + "}"); + Q_FOREACH (QGroupBox *box, findChildren<QGroupBox *>()) { + box->setStyleSheet(gnomeCss); + } + } + // QLineEdit and QLabels should have minimal margin on the left and right but not waste vertical space + QMargins margins(3, 2, 1, 0); + Q_FOREACH (QLabel *label, findChildren<QLabel *>()) { + label->setContentsMargins(margins); + } + ui.cylinders->view()->horizontalHeader()->setContextMenuPolicy(Qt::ActionsContextMenu); + ui.weights->view()->horizontalHeader()->setContextMenuPolicy(Qt::ActionsContextMenu); + + QSettings s; + s.beginGroup("cylinders_dialog"); + for (int i = 0; i < CylindersModel::COLUMNS; i++) { + if ((i == CylindersModel::REMOVE) || (i == CylindersModel::TYPE)) + continue; + bool checked = s.value(QString("column%1_hidden").arg(i)).toBool(); + action = new QAction(cylindersModel->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString(), ui.cylinders->view()); + action->setCheckable(true); + action->setData(i); + action->setChecked(!checked); + connect(action, SIGNAL(triggered(bool)), this, SLOT(toggleTriggeredColumn())); + ui.cylinders->view()->setColumnHidden(i, checked); + ui.cylinders->view()->horizontalHeader()->addAction(action); + } + + QAction *deletePhoto = new QAction(this); + deletePhoto->setShortcut(Qt::Key_Delete); + deletePhoto->setShortcutContext(Qt::WidgetShortcut); + ui.photosView->addAction(deletePhoto); + ui.photosView->setSelectionMode(QAbstractItemView::SingleSelection); + connect(deletePhoto, SIGNAL(triggered(bool)), this, SLOT(removeSelectedPhotos())); + + ui.waitingSpinner->setRoundness(70.0); + ui.waitingSpinner->setMinimumTrailOpacity(15.0); + ui.waitingSpinner->setTrailFadePercentage(70.0); + ui.waitingSpinner->setNumberOfLines(8); + ui.waitingSpinner->setLineLength(5); + ui.waitingSpinner->setLineWidth(3); + ui.waitingSpinner->setInnerRadius(5); + ui.waitingSpinner->setRevolutionsPerSecond(1); + + connect(ReverseGeoLookupThread::instance(), SIGNAL(finished()), + LocationInformationModel::instance(), SLOT(update())); + + connect(ReverseGeoLookupThread::instance(), &QThread::finished, + this, &MainTab::setCurrentLocationIndex); + + connect(ui.diveNotesMessage, &KMessageWidget::showAnimationFinished, + ui.location, &DiveLocationLineEdit::fixPopupPosition); + + acceptingEdit = false; + + ui.diveTripLocation->hide(); +} + +MainTab::~MainTab() +{ + QSettings s; + s.beginGroup("cylinders_dialog"); + for (int i = 0; i < CylindersModel::COLUMNS; i++) { + if ((i == CylindersModel::REMOVE) || (i == CylindersModel::TYPE)) + continue; + s.setValue(QString("column%1_hidden").arg(i), ui.cylinders->view()->isColumnHidden(i)); + } +} + +void MainTab::setCurrentLocationIndex() +{ + if (current_dive) { + struct dive_site *ds = get_dive_site_by_uuid(current_dive->dive_site_uuid); + if (ds) + ui.location->setCurrentDiveSiteUuid(ds->uuid); + else + ui.location->clear(); + } +} + +void MainTab::enableGeoLookupEdition() +{ + ui.waitingSpinner->stop(); +} + +void MainTab::disableGeoLookupEdition() +{ + ui.waitingSpinner->start(); +} + +void MainTab::toggleTriggeredColumn() +{ + QAction *action = qobject_cast<QAction *>(sender()); + int col = action->data().toInt(); + QTableView *view = ui.cylinders->view(); + + if (action->isChecked()) { + view->showColumn(col); + if (view->columnWidth(col) <= 15) + view->setColumnWidth(col, 80); + } else + view->hideColumn(col); +} + +void MainTab::addDiveStarted() +{ + enableEdition(ADD); +} + +void MainTab::addMessageAction(QAction *action) +{ + ui.diveEquipmentMessage->addAction(action); + ui.diveNotesMessage->addAction(action); + ui.diveInfoMessage->addAction(action); + ui.diveStatisticsMessage->addAction(action); +} + +void MainTab::hideMessage() +{ + ui.diveNotesMessage->animatedHide(); + ui.diveEquipmentMessage->animatedHide(); + ui.diveInfoMessage->animatedHide(); + ui.diveStatisticsMessage->animatedHide(); + updateTextLabels(false); +} + +void MainTab::closeMessage() +{ + hideMessage(); + ui.diveNotesMessage->setCloseButtonVisible(false); + ui.diveEquipmentMessage->setCloseButtonVisible(false); + ui.diveInfoMessage->setCloseButtonVisible(false); + ui.diveStatisticsMessage->setCloseButtonVisible(false); +} + +void MainTab::displayMessage(QString str) +{ + ui.diveNotesMessage->setCloseButtonVisible(false); + ui.diveEquipmentMessage->setCloseButtonVisible(false); + ui.diveInfoMessage->setCloseButtonVisible(false); + ui.diveStatisticsMessage->setCloseButtonVisible(false); + ui.diveNotesMessage->setText(str); + ui.diveNotesMessage->animatedShow(); + ui.diveEquipmentMessage->setText(str); + ui.diveEquipmentMessage->animatedShow(); + ui.diveInfoMessage->setText(str); + ui.diveInfoMessage->animatedShow(); + ui.diveStatisticsMessage->setText(str); + ui.diveStatisticsMessage->animatedShow(); + updateTextLabels(); +} + +void MainTab::updateTextLabels(bool showUnits) +{ + if (showUnits) { + ui.airTempLabel->setText(tr("Air temp. [%1]").arg(get_temp_unit())); + ui.waterTempLabel->setText(tr("Water temp. [%1]").arg(get_temp_unit())); + } else { + ui.airTempLabel->setText(tr("Air temp.")); + ui.waterTempLabel->setText(tr("Water temp.")); + } +} + +void MainTab::enableEdition(EditMode newEditMode) +{ + const bool isTripEdit = MainWindow::instance() && + MainWindow::instance()->dive_list()->selectedTrips().count() == 1; + + if (((newEditMode == DIVE || newEditMode == NONE) && current_dive == NULL) || editMode != NONE) + return; + modified = false; + copyPaste = false; + if ((newEditMode == DIVE || newEditMode == NONE) && + !isTripEdit && + current_dive->dc.model && + strcmp(current_dive->dc.model, "manually added dive") == 0) { + // editCurrentDive will call enableEdition with newEditMode == MANUALLY_ADDED_DIVE + // so exit this function here after editCurrentDive() returns + + + + // FIXME : can we get rid of this recursive crap? + + + + MainWindow::instance()->editCurrentDive(); + return; + } + + ui.editDiveSiteButton->setEnabled(false); + MainWindow::instance()->dive_list()->setEnabled(false); + MainWindow::instance()->setEnabledToolbar(false); + + if (isTripEdit) { + // we are editing trip location and notes + displayMessage(tr("This trip is being edited.")); + currentTrip = current_dive->divetrip; + ui.dateEdit->setEnabled(false); + editMode = TRIP; + } else { + ui.dateEdit->setEnabled(true); + if (amount_selected > 1) { + displayMessage(tr("Multiple dives are being edited.")); + } else { + displayMessage(tr("This dive is being edited.")); + } + editMode = newEditMode != NONE ? newEditMode : DIVE; + } +} + +void MainTab::clearEquipment() +{ + cylindersModel->clear(); + weightModel->clear(); +} + +void MainTab::nextInputField(QKeyEvent *event) +{ + keyPressEvent(event); +} + +void MainTab::clearInfo() +{ + ui.sacText->clear(); + ui.otuText->clear(); + ui.maxcnsText->clear(); + ui.oxygenHeliumText->clear(); + ui.gasUsedText->clear(); + ui.dateText->clear(); + ui.diveTimeText->clear(); + ui.surfaceIntervalText->clear(); + ui.maximumDepthText->clear(); + ui.averageDepthText->clear(); + ui.waterTemperatureText->clear(); + ui.airTemperatureText->clear(); + ui.airPressureText->clear(); + ui.salinityText->clear(); + ui.tagWidget->clear(); +} + +void MainTab::clearStats() +{ + ui.depthLimits->clear(); + ui.sacLimits->clear(); + ui.divesAllText->clear(); + ui.tempLimits->clear(); + ui.totalTimeAllText->clear(); + ui.timeLimits->clear(); +} + +#define UPDATE_TEXT(d, field) \ + if (clear || !d.field) \ + ui.field->setText(QString()); \ + else \ + ui.field->setText(d.field) + +#define UPDATE_TEMP(d, field) \ + if (clear || d.field.mkelvin == 0) \ + ui.field->setText(""); \ + else \ + ui.field->setText(get_temperature_string(d.field, true)) + +bool MainTab::isEditing() +{ + return editMode != NONE; +} + +void MainTab::showLocation() +{ + if (get_dive_site_by_uuid(displayed_dive.dive_site_uuid)) + ui.location->setCurrentDiveSiteUuid(displayed_dive.dive_site_uuid); + else + ui.location->clear(); +} + +// Seems wrong, since we can also call updateDiveInfo(), but since the updateDiveInfo +// has a parameter on it's definition it didn't worked on the signal slot connection. +void MainTab::refreshDiveInfo() +{ + updateDiveInfo(); +} + +void MainTab::updateDiveInfo(bool clear) +{ + ui.location->refreshDiveSiteCache(); + EditMode rememberEM = editMode; + // don't execute this while adding / planning a dive + if (editMode == ADD || editMode == MANUALLY_ADDED_DIVE || MainWindow::instance()->graphics()->isPlanner()) + return; + if (!isEnabled() && !clear ) + setEnabled(true); + if (isEnabled() && clear) + setEnabled(false); + editMode = IGNORE; // don't trigger on changes to the widgets + + // This method updates ALL tabs whenever a new dive or trip is + // selected. + // If exactly one trip has been selected, we show the location / notes + // for the trip in the Info tab, otherwise we show the info of the + // selected_dive + temperature_t temp; + struct dive *prevd; + char buf[1024]; + + process_selected_dives(); + process_all_dives(&displayed_dive, &prevd); + + divePictureModel->updateDivePictures(); + + ui.notes->setText(QString()); + if (!clear) { + QString tmp(displayed_dive.notes); + if (tmp.indexOf("<table") != -1) + ui.notes->setHtml(tmp); + else + ui.notes->setPlainText(tmp); + } + UPDATE_TEXT(displayed_dive, notes); + UPDATE_TEXT(displayed_dive, suit); + UPDATE_TEXT(displayed_dive, divemaster); + UPDATE_TEXT(displayed_dive, buddy); + UPDATE_TEMP(displayed_dive, airtemp); + UPDATE_TEMP(displayed_dive, watertemp); + ui.DiveType->setCurrentIndex(get_dive_dc(&displayed_dive, dc_number)->divemode); + + if (!clear) { + struct dive_site *ds = NULL; + // if we are showing a dive and editing it, let's refer to the displayed_dive_site as that + // already may contain changes, otherwise start with the dive site referred to by the displayed + // dive + if (rememberEM == DIVE) { + ds = &displayed_dive_site; + } else { + ds = get_dive_site_by_uuid(displayed_dive.dive_site_uuid); + if (ds) + copy_dive_site(ds, &displayed_dive_site); + } + + if (ds) { + ui.location->setCurrentDiveSiteUuid(ds->uuid); + ui.locationTags->setText(constructLocationTags(ds->uuid)); + } else { + ui.location->clear(); + clear_dive_site(&displayed_dive_site); + } + + // Subsurface always uses "local time" as in "whatever was the local time at the location" + // so all time stamps have no time zone information and are in UTC + QDateTime localTime = QDateTime::fromTime_t(displayed_dive.when - gettimezoneoffset(displayed_dive.when)); + localTime.setTimeSpec(Qt::UTC); + ui.dateEdit->setDate(localTime.date()); + ui.timeEdit->setTime(localTime.time()); + if (MainWindow::instance() && MainWindow::instance()->dive_list()->selectedTrips().count() == 1) { + setTabText(0, tr("Trip notes")); + currentTrip = *MainWindow::instance()->dive_list()->selectedTrips().begin(); + // only use trip relevant fields + ui.divemaster->setVisible(false); + ui.DivemasterLabel->setVisible(false); + ui.buddy->setVisible(false); + ui.BuddyLabel->setVisible(false); + ui.suit->setVisible(false); + ui.SuitLabel->setVisible(false); + ui.rating->setVisible(false); + ui.RatingLabel->setVisible(false); + ui.visibility->setVisible(false); + ui.visibilityLabel->setVisible(false); + ui.tagWidget->setVisible(false); + ui.TagLabel->setVisible(false); + ui.airTempLabel->setVisible(false); + ui.airtemp->setVisible(false); + ui.DiveType->setVisible(false); + ui.TypeLabel->setVisible(false); + ui.waterTempLabel->setVisible(false); + ui.watertemp->setVisible(false); + ui.diveTripLocation->show(); + ui.location->hide(); + ui.editDiveSiteButton->hide(); + // rename the remaining fields and fill data from selected trip + ui.LocationLabel->setText(tr("Trip location")); + ui.diveTripLocation->setText(currentTrip->location); + ui.locationTags->clear(); + //TODO: Fix this. + //ui.location->setText(currentTrip->location); + ui.NotesLabel->setText(tr("Trip notes")); + ui.notes->setText(currentTrip->notes); + clearEquipment(); + ui.equipmentTab->setEnabled(false); + } else { + setTabText(0, tr("Notes")); + currentTrip = NULL; + // make all the fields visible writeable + ui.diveTripLocation->hide(); + ui.location->show(); + ui.editDiveSiteButton->show(); + ui.divemaster->setVisible(true); + ui.buddy->setVisible(true); + ui.suit->setVisible(true); + ui.SuitLabel->setVisible(true); + ui.rating->setVisible(true); + ui.RatingLabel->setVisible(true); + ui.visibility->setVisible(true); + ui.visibilityLabel->setVisible(true); + ui.BuddyLabel->setVisible(true); + ui.DivemasterLabel->setVisible(true); + ui.TagLabel->setVisible(true); + ui.tagWidget->setVisible(true); + ui.airTempLabel->setVisible(true); + ui.airtemp->setVisible(true); + ui.TypeLabel->setVisible(true); + ui.DiveType->setVisible(true); + ui.waterTempLabel->setVisible(true); + ui.watertemp->setVisible(true); + /* and fill them from the dive */ + ui.rating->setCurrentStars(displayed_dive.rating); + ui.visibility->setCurrentStars(displayed_dive.visibility); + // reset labels in case we last displayed trip notes + ui.LocationLabel->setText(tr("Location")); + ui.NotesLabel->setText(tr("Notes")); + ui.equipmentTab->setEnabled(true); + cylindersModel->updateDive(); + weightModel->updateDive(); + extraDataModel->updateDive(); + taglist_get_tagstring(displayed_dive.tag_list, buf, 1024); + ui.tagWidget->setText(QString(buf)); + } + ui.maximumDepthText->setText(get_depth_string(displayed_dive.maxdepth, true)); + ui.averageDepthText->setText(get_depth_string(displayed_dive.meandepth, true)); + ui.maxcnsText->setText(QString("%1\%").arg(displayed_dive.maxcns)); + ui.otuText->setText(QString("%1").arg(displayed_dive.otu)); + ui.waterTemperatureText->setText(get_temperature_string(displayed_dive.watertemp, true)); + ui.airTemperatureText->setText(get_temperature_string(displayed_dive.airtemp, true)); + ui.DiveType->setCurrentIndex(get_dive_dc(&displayed_dive, dc_number)->divemode); + + volume_t gases[MAX_CYLINDERS] = {}; + get_gas_used(&displayed_dive, gases); + QString volumes; + int mean[MAX_CYLINDERS], duration[MAX_CYLINDERS]; + per_cylinder_mean_depth(&displayed_dive, select_dc(&displayed_dive), mean, duration); + volume_t sac; + QString gaslist, SACs, separator; + + gaslist = ""; SACs = ""; volumes = ""; separator = ""; + for (int i = 0; i < MAX_CYLINDERS; i++) { + if (!is_cylinder_used(&displayed_dive, i)) + continue; + gaslist.append(separator); volumes.append(separator); SACs.append(separator); + separator = "\n"; + + gaslist.append(gasname(&displayed_dive.cylinder[i].gasmix)); + if (!gases[i].mliter) + continue; + volumes.append(get_volume_string(gases[i], true)); + if (duration[i]) { + sac.mliter = gases[i].mliter / (depth_to_atm(mean[i], &displayed_dive) * duration[i] / 60); + SACs.append(get_volume_string(sac, true).append(tr("/min"))); + } + } + ui.gasUsedText->setText(volumes); + ui.oxygenHeliumText->setText(gaslist); + ui.dateText->setText(get_short_dive_date_string(displayed_dive.when)); + if (displayed_dive.dc.divemode != FREEDIVE) + ui.diveTimeText->setText(get_time_string_s(displayed_dive.duration.seconds + 30, 0, false)); + else + ui.diveTimeText->setText(get_time_string_s(displayed_dive.duration.seconds, 0, true)); + if (prevd) + ui.surfaceIntervalText->setText(get_time_string_s(displayed_dive.when - (prevd->when + prevd->duration.seconds), 4, + (displayed_dive.dc.divemode == FREEDIVE))); + else + ui.surfaceIntervalText->clear(); + if (mean[0]) + ui.sacText->setText(SACs); + else + ui.sacText->clear(); + if (displayed_dive.surface_pressure.mbar) + /* this is ALWAYS displayed in mbar */ + ui.airPressureText->setText(QString("%1mbar").arg(displayed_dive.surface_pressure.mbar)); + else + ui.airPressureText->clear(); + if (displayed_dive.salinity) + ui.salinityText->setText(QString("%1g/l").arg(displayed_dive.salinity / 10.0)); + else + ui.salinityText->clear(); + ui.depthLimits->setMaximum(get_depth_string(stats_selection.max_depth, true)); + ui.depthLimits->setMinimum(get_depth_string(stats_selection.min_depth, true)); + // the overall average depth is really confusing when listed between the + // deepest and shallowest dive - let's just not set it + // ui.depthLimits->setAverage(get_depth_string(stats_selection.avg_depth, true)); + ui.depthLimits->overrideMaxToolTipText(tr("Deepest dive")); + ui.depthLimits->overrideMinToolTipText(tr("Shallowest dive")); + if (amount_selected > 1 && stats_selection.max_sac.mliter) + ui.sacLimits->setMaximum(get_volume_string(stats_selection.max_sac, true).append(tr("/min"))); + else + ui.sacLimits->setMaximum(""); + if (amount_selected > 1 && stats_selection.min_sac.mliter) + ui.sacLimits->setMinimum(get_volume_string(stats_selection.min_sac, true).append(tr("/min"))); + else + ui.sacLimits->setMinimum(""); + if (stats_selection.avg_sac.mliter) + ui.sacLimits->setAverage(get_volume_string(stats_selection.avg_sac, true).append(tr("/min"))); + else + ui.sacLimits->setAverage(""); + ui.sacLimits->overrideMaxToolTipText(tr("Highest total SAC of a dive")); + ui.sacLimits->overrideMinToolTipText(tr("Lowest total SAC of a dive")); + ui.sacLimits->overrideAvgToolTipText(tr("Average total SAC of all selected dives")); + ui.divesAllText->setText(QString::number(stats_selection.selection_size)); + temp.mkelvin = stats_selection.max_temp; + ui.tempLimits->setMaximum(get_temperature_string(temp, true)); + temp.mkelvin = stats_selection.min_temp; + ui.tempLimits->setMinimum(get_temperature_string(temp, true)); + if (stats_selection.combined_temp && stats_selection.combined_count) { + const char *unit; + get_temp_units(0, &unit); + ui.tempLimits->setAverage(QString("%1%2").arg(stats_selection.combined_temp / stats_selection.combined_count, 0, 'f', 1).arg(unit)); + } + ui.tempLimits->overrideMaxToolTipText(tr("Highest temperature")); + ui.tempLimits->overrideMinToolTipText(tr("Lowest temperature")); + ui.tempLimits->overrideAvgToolTipText(tr("Average temperature of all selected dives")); + ui.totalTimeAllText->setText(get_time_string_s(stats_selection.total_time.seconds, 0, (displayed_dive.dc.divemode == FREEDIVE))); + int seconds = stats_selection.total_time.seconds; + if (stats_selection.selection_size) + seconds /= stats_selection.selection_size; + ui.timeLimits->setAverage(get_time_string_s(seconds, 0,(displayed_dive.dc.divemode == FREEDIVE))); + if (amount_selected > 1) { + ui.timeLimits->setMaximum(get_time_string_s(stats_selection.longest_time.seconds, 0, (displayed_dive.dc.divemode == FREEDIVE))); + ui.timeLimits->setMinimum(get_time_string_s(stats_selection.shortest_time.seconds, 0, (displayed_dive.dc.divemode == FREEDIVE))); + } + ui.timeLimits->overrideMaxToolTipText(tr("Longest dive")); + ui.timeLimits->overrideMinToolTipText(tr("Shortest dive")); + ui.timeLimits->overrideAvgToolTipText(tr("Average length of all selected dives")); + // now let's get some gas use statistics + QVector<QPair<QString, int> > gasUsed; + QString gasUsedString; + volume_t vol; + selectedDivesGasUsed(gasUsed); + for (int j = 0; j < 20; j++) { + if (gasUsed.isEmpty()) + break; + QPair<QString, int> gasPair = gasUsed.last(); + gasUsed.pop_back(); + vol.mliter = gasPair.second; + gasUsedString.append(gasPair.first).append(": ").append(get_volume_string(vol, true)).append("\n"); + } + if (!gasUsed.isEmpty()) + gasUsedString.append("..."); + volume_t o2_tot = {}, he_tot = {}; + selected_dives_gas_parts(&o2_tot, &he_tot); + + /* No need to show the gas mixing information if diving + * with pure air, and only display the he / O2 part when + * it is used. + */ + if (he_tot.mliter || o2_tot.mliter) { + gasUsedString.append(tr("These gases could be\nmixed from Air and using:\n")); + if (he_tot.mliter) + gasUsedString.append(QString("He: %1").arg(get_volume_string(he_tot, true))); + if (he_tot.mliter && o2_tot.mliter) + gasUsedString.append(tr(" and ")); + if (o2_tot.mliter) + gasUsedString.append(QString("O2: %2\n").arg(get_volume_string(o2_tot, true))); + } + ui.gasConsumption->setText(gasUsedString); + if(ui.locationTags->text().isEmpty()) + ui.locationTags->hide(); + else + ui.locationTags->show(); + /* unset the special value text for date and time, just in case someone dove at midnight */ + ui.dateEdit->setSpecialValueText(QString("")); + ui.timeEdit->setSpecialValueText(QString("")); + + } else { + /* clear the fields */ + clearInfo(); + clearStats(); + clearEquipment(); + ui.rating->setCurrentStars(0); + ui.visibility->setCurrentStars(0); + ui.location->clear(); + /* set date and time to minimums which triggers showing the special value text */ + ui.dateEdit->setSpecialValueText(QString("-")); + ui.dateEdit->setMinimumDate(QDate(1, 1, 1)); + ui.dateEdit->setDate(QDate(1, 1, 1)); + ui.timeEdit->setSpecialValueText(QString("-")); + ui.timeEdit->setMinimumTime(QTime(0, 0, 0, 0)); + ui.timeEdit->setTime(QTime(0, 0, 0, 0)); + } + editMode = rememberEM; + ui.cylinders->view()->hideColumn(CylindersModel::DEPTH); + if (get_dive_dc(&displayed_dive, dc_number)->divemode == CCR) + ui.cylinders->view()->showColumn(CylindersModel::USE); + else + ui.cylinders->view()->hideColumn(CylindersModel::USE); + + if (verbose) + qDebug() << "Set the current dive site:" << displayed_dive.dive_site_uuid; + emit diveSiteChanged(get_dive_site_by_uuid(displayed_dive.dive_site_uuid)); +} + +void MainTab::addCylinder_clicked() +{ + if (editMode == NONE) + enableEdition(); + cylindersModel->add(); +} + +void MainTab::addWeight_clicked() +{ + if (editMode == NONE) + enableEdition(); + weightModel->add(); +} + +void MainTab::reload() +{ + suitModel.updateModel(); + buddyModel.updateModel(); + diveMasterModel.updateModel(); + tagModel.updateModel(); + LocationInformationModel::instance()->update(); +} + +// tricky little macro to edit all the selected dives +// loop over all dives, for each selected dive do WHAT, but do it +// last for the current dive; this is required in case the invocation +// wants to compare things to the original value in current_dive like it should +#define MODIFY_SELECTED_DIVES(WHAT) \ + do { \ + struct dive *mydive = NULL; \ + int _i; \ + for_each_dive (_i, mydive) { \ + if (!mydive->selected || mydive == cd) \ + continue; \ + \ + WHAT; \ + } \ + mydive = cd; \ + WHAT; \ + mark_divelist_changed(true); \ + } while (0) + +#define EDIT_TEXT(what) \ + if (same_string(mydive->what, cd->what) || copyPaste) { \ + free(mydive->what); \ + mydive->what = copy_string(displayed_dive.what); \ + } + +MainTab::EditMode MainTab::getEditMode() const +{ + return editMode; +} + +#define EDIT_VALUE(what) \ + if (mydive->what == cd->what || copyPaste) { \ + mydive->what = displayed_dive.what; \ + } + +void MainTab::refreshDisplayedDiveSite() +{ + if (displayed_dive_site.uuid) { + copy_dive_site(get_dive_site_by_uuid(displayed_dive_site.uuid), &displayed_dive_site); + ui.location->setCurrentDiveSiteUuid(displayed_dive_site.uuid); + } +} + +// when this is called we already have updated the current_dive and know that it exists +// there is no point in calling this function if there is no current dive +uint32_t MainTab::updateDiveSite(uint32_t pickedUuid, int divenr) +{ + struct dive *cd = get_dive(divenr); + if (!cd) + return 0; + + if (ui.location->text().isEmpty()) + return 0; + + if (pickedUuid == 0) + return 0; + + const uint32_t origUuid = cd->dive_site_uuid; + struct dive_site *origDs = get_dive_site_by_uuid(origUuid); + struct dive_site *newDs = NULL; + bool createdNewDive = false; + + if (pickedUuid == origUuid) + return origUuid; + + if (pickedUuid == RECENTLY_ADDED_DIVESITE) { + pickedUuid = create_dive_site(ui.location->text().isEmpty() ? qPrintable(tr("New dive site")) : qPrintable(ui.location->text()), displayed_dive.when); + createdNewDive = true; + } + + newDs = get_dive_site_by_uuid(pickedUuid); + + // Copy everything from the displayed_dive_site, so we have the latitude, longitude, notes, etc. + // The user *might* be using wrongly the 'choose dive site' just to edit the name of it, sigh. + if (origDs) { + if(createdNewDive) { + copy_dive_site(origDs, newDs); + free(newDs->name); + newDs->name = copy_string(qPrintable(ui.location->text().constData())); + newDs->uuid = pickedUuid; + qDebug() << "Creating and copying dive site"; + } else if (newDs->latitude.udeg == 0 && newDs->longitude.udeg == 0) { + newDs->latitude.udeg = origDs->latitude.udeg; + newDs->longitude.udeg = origDs->longitude.udeg; + qDebug() << "Copying GPS information"; + } + } + + if (origDs && pickedUuid != origDs->uuid && same_string(origDs->notes, "SubsurfaceWebservice")) { + if (!is_dive_site_used(origDs->uuid, false)) { + if (verbose) + qDebug() << "delete the autogenerated dive site" << origDs->name; + delete_dive_site(origDs->uuid); + } + } + + cd->dive_site_uuid = pickedUuid; + qDebug() << "Setting the dive site id on the dive:" << pickedUuid; + return pickedUuid; +} + +void MainTab::acceptChanges() +{ + int i, addedId = -1; + struct dive *d; + bool do_replot = false; + + if(ui.location->hasFocus()) { + this->setFocus(); + } + + acceptingEdit = true; + tabBar()->setTabIcon(0, QIcon()); // Notes + tabBar()->setTabIcon(1, QIcon()); // Equipment + ui.dateEdit->setEnabled(true); + hideMessage(); + ui.equipmentTab->setEnabled(true); + if (editMode == ADD) { + // We need to add the dive we just created to the dive list and select it. + // Easy, right? + struct dive *added_dive = clone_dive(&displayed_dive); + record_dive(added_dive); + addedId = added_dive->id; + // make sure that the dive site is handled as well + updateDiveSite(ui.location->currDiveSiteUuid(), get_idx_by_uniq_id(added_dive->id)); + + // unselect everything as far as the UI is concerned and select the new + // dive - we'll have to undo/redo this later after we resort the dive_table + // but we need the dive selected for the middle part of this function - this + // way we can reuse the code used for editing dives + MainWindow::instance()->dive_list()->unselectDives(); + selected_dive = get_divenr(added_dive); + amount_selected = 1; + } else if (MainWindow::instance() && MainWindow::instance()->dive_list()->selectedTrips().count() == 1) { + /* now figure out if things have changed */ + if (displayedTrip.notes && !same_string(displayedTrip.notes, currentTrip->notes)) { + currentTrip->notes = copy_string(displayedTrip.notes); + mark_divelist_changed(true); + } + if (displayedTrip.location && !same_string(displayedTrip.location, currentTrip->location)) { + currentTrip->location = copy_string(displayedTrip.location); + mark_divelist_changed(true); + } + currentTrip = NULL; + ui.dateEdit->setEnabled(true); + } else { + if (editMode == MANUALLY_ADDED_DIVE) { + // preserve any changes to the profile + free(current_dive->dc.sample); + copy_samples(&displayed_dive.dc, ¤t_dive->dc); + addedId = displayed_dive.id; + } + struct dive *cd = current_dive; + struct divecomputer *displayed_dc = get_dive_dc(&displayed_dive, dc_number); + // now check if something has changed and if yes, edit the selected dives that + // were identical with the master dive shown (and mark the divelist as changed) + if (!same_string(displayed_dive.suit, cd->suit)) + MODIFY_SELECTED_DIVES(EDIT_TEXT(suit)); + if (!same_string(displayed_dive.notes, cd->notes)) + MODIFY_SELECTED_DIVES(EDIT_TEXT(notes)); + if (displayed_dive.rating != cd->rating) + MODIFY_SELECTED_DIVES(EDIT_VALUE(rating)); + if (displayed_dive.visibility != cd->visibility) + MODIFY_SELECTED_DIVES(EDIT_VALUE(visibility)); + if (displayed_dive.airtemp.mkelvin != cd->airtemp.mkelvin) + MODIFY_SELECTED_DIVES(EDIT_VALUE(airtemp.mkelvin)); + if (displayed_dc->divemode != current_dc->divemode) { + MODIFY_SELECTED_DIVES( + if (get_dive_dc(mydive, dc_number)->divemode == current_dc->divemode || copyPaste) { + get_dive_dc(mydive, dc_number)->divemode = displayed_dc->divemode; + } + ); + MODIFY_SELECTED_DIVES(update_setpoint_events(get_dive_dc(mydive, dc_number))); + do_replot = true; + } + if (displayed_dive.watertemp.mkelvin != cd->watertemp.mkelvin) + MODIFY_SELECTED_DIVES(EDIT_VALUE(watertemp.mkelvin)); + if (displayed_dive.when != cd->when) { + time_t offset = cd->when - displayed_dive.when; + MODIFY_SELECTED_DIVES(mydive->when -= offset;); + } + + if (displayed_dive.dive_site_uuid != cd->dive_site_uuid) + MODIFY_SELECTED_DIVES(EDIT_VALUE(dive_site_uuid)); + + // three text fields are somewhat special and are represented as tags + // in the UI - they need somewhat smarter handling + saveTaggedStrings(); + saveTags(); + + if (editMode != ADD && cylindersModel->changed) { + mark_divelist_changed(true); + MODIFY_SELECTED_DIVES( + for (int i = 0; i < MAX_CYLINDERS; i++) { + if (mydive != cd) { + if (same_string(mydive->cylinder[i].type.description, cd->cylinder[i].type.description) || copyPaste) { + // if we started out with the same cylinder description (for multi-edit) or if we do copt & paste + // make sure that we have the same cylinder type and copy the gasmix, but DON'T copy the start + // and end pressures (those are per dive after all) + if (!same_string(mydive->cylinder[i].type.description, displayed_dive.cylinder[i].type.description)) { + free((void*)mydive->cylinder[i].type.description); + mydive->cylinder[i].type.description = copy_string(displayed_dive.cylinder[i].type.description); + } + mydive->cylinder[i].type.size = displayed_dive.cylinder[i].type.size; + mydive->cylinder[i].type.workingpressure = displayed_dive.cylinder[i].type.workingpressure; + mydive->cylinder[i].gasmix = displayed_dive.cylinder[i].gasmix; + mydive->cylinder[i].cylinder_use = displayed_dive.cylinder[i].cylinder_use; + mydive->cylinder[i].depth = displayed_dive.cylinder[i].depth; + } + } + } + ); + for (int i = 0; i < MAX_CYLINDERS; i++) { + // copy the cylinder but make sure we have our own copy of the strings + free((void*)cd->cylinder[i].type.description); + cd->cylinder[i] = displayed_dive.cylinder[i]; + cd->cylinder[i].type.description = copy_string(displayed_dive.cylinder[i].type.description); + } + /* if cylinders changed we may have changed gas change events + * - so far this is ONLY supported for a single selected dive */ + struct divecomputer *tdc = ¤t_dive->dc; + struct divecomputer *sdc = &displayed_dive.dc; + while(tdc && sdc) { + free_events(tdc->events); + copy_events(sdc, tdc); + tdc = tdc->next; + sdc = sdc->next; + } + do_replot = true; + } + + if (weightModel->changed) { + mark_divelist_changed(true); + MODIFY_SELECTED_DIVES( + for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++) { + if (mydive != cd && (copyPaste || same_string(mydive->weightsystem[i].description, cd->weightsystem[i].description))) { + mydive->weightsystem[i] = displayed_dive.weightsystem[i]; + mydive->weightsystem[i].description = copy_string(displayed_dive.weightsystem[i].description); + } + } + ); + for (int i = 0; i < MAX_WEIGHTSYSTEMS; i++) { + cd->weightsystem[i] = displayed_dive.weightsystem[i]; + cd->weightsystem[i].description = copy_string(displayed_dive.weightsystem[i].description); + } + } + + // update the dive site for the selected dives that had the same dive site as the current dive + uint32_t oldUuid = cd->dive_site_uuid; + uint32_t newUuid = 0; + MODIFY_SELECTED_DIVES( + if (mydive->dive_site_uuid == current_dive->dive_site_uuid) { + newUuid = updateDiveSite(newUuid == 0 ? ui.location->currDiveSiteUuid() : newUuid, get_idx_by_uniq_id(mydive->id)); + } + ); + if (!is_dive_site_used(oldUuid, false)) { + if (verbose) { + struct dive_site *ds = get_dive_site_by_uuid(oldUuid); + qDebug() << "delete now unused dive site" << ((ds && ds->name) ? ds->name : "without name"); + } + delete_dive_site(oldUuid); + GlobeGPS::instance()->reload(); + } + // the code above can change the correct uuid for the displayed dive site - and the + // code below triggers an update of the display without re-initializing displayed_dive + // so let's make sure here that our data is consistent now that we have handled the + // dive sites + displayed_dive.dive_site_uuid = current_dive->dive_site_uuid; + struct dive_site *ds = get_dive_site_by_uuid(displayed_dive.dive_site_uuid); + if (ds) + copy_dive_site(ds, &displayed_dive_site); + + // each dive that was selected might have had the temperatures in its active divecomputer changed + // so re-populate the temperatures - easiest way to do this is by calling fixup_dive + for_each_dive (i, d) { + if (d->selected) + fixup_dive(d); + } + } + if (editMode != TRIP && current_dive->divetrip) { + current_dive->divetrip->when = current_dive->when; + find_new_trip_start_time(current_dive->divetrip); + } + if (editMode == ADD || editMode == MANUALLY_ADDED_DIVE) { + // we just added or edited the dive, let fixup_dive() make + // sure we get the max depth right + current_dive->maxdepth.mm = current_dc->maxdepth.mm = 0; + fixup_dive(current_dive); + set_dive_nr_for_current_dive(); + MainWindow::instance()->showProfile(); + mark_divelist_changed(true); + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::NOTHING); + } + int scrolledBy = MainWindow::instance()->dive_list()->verticalScrollBar()->sliderPosition(); + resetPallete(); + if (editMode == ADD || editMode == MANUALLY_ADDED_DIVE) { + // since a newly added dive could be in the middle of the dive_table we need + // to resort the dive list and make sure the newly added dive gets selected again + sort_table(&dive_table); + MainWindow::instance()->dive_list()->reload(DiveTripModel::CURRENT, true); + int newDiveNr = get_divenr(get_dive_by_uniq_id(addedId)); + MainWindow::instance()->dive_list()->unselectDives(); + MainWindow::instance()->dive_list()->selectDive(newDiveNr, true); + editMode = NONE; + MainWindow::instance()->refreshDisplay(); + MainWindow::instance()->graphics()->replot(); + emit addDiveFinished(); + } else { + editMode = NONE; + if (do_replot) + MainWindow::instance()->graphics()->replot(); + MainWindow::instance()->dive_list()->rememberSelection(); + sort_table(&dive_table); + MainWindow::instance()->refreshDisplay(); + MainWindow::instance()->dive_list()->restoreSelection(); + } + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::NOTHING); + MainWindow::instance()->dive_list()->verticalScrollBar()->setSliderPosition(scrolledBy); + MainWindow::instance()->dive_list()->setFocus(); + cylindersModel->changed = false; + weightModel->changed = false; + MainWindow::instance()->setEnabledToolbar(true); + acceptingEdit = false; + ui.editDiveSiteButton->setEnabled(true); +} + +void MainTab::resetPallete() +{ + QPalette p; + ui.buddy->setPalette(p); + ui.notes->setPalette(p); + ui.location->setPalette(p); + ui.divemaster->setPalette(p); + ui.suit->setPalette(p); + ui.airtemp->setPalette(p); + ui.DiveType->setPalette(p); + ui.watertemp->setPalette(p); + ui.dateEdit->setPalette(p); + ui.timeEdit->setPalette(p); + ui.tagWidget->setPalette(p); + ui.diveTripLocation->setPalette(p); +} + +#define EDIT_TEXT2(what, text) \ + textByteArray = text.toUtf8(); \ + free(what); \ + what = strdup(textByteArray.data()); + +#define FREE_IF_DIFFERENT(what) \ + if (displayed_dive.what != cd->what) \ + free(displayed_dive.what) + +void MainTab::rejectChanges() +{ + EditMode lastMode = editMode; + + if (lastMode != NONE && current_dive && + (modified || + memcmp(¤t_dive->cylinder[0], &displayed_dive.cylinder[0], sizeof(cylinder_t) * MAX_CYLINDERS) || + memcmp(¤t_dive->cylinder[0], &displayed_dive.weightsystem[0], sizeof(weightsystem_t) * MAX_WEIGHTSYSTEMS))) { + if (QMessageBox::warning(MainWindow::instance(), TITLE_OR_TEXT(tr("Discard the changes?"), + tr("You are about to discard your changes.")), + QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Discard) != QMessageBox::Discard) { + return; + } + } + ui.dateEdit->setEnabled(true); + editMode = NONE; + tabBar()->setTabIcon(0, QIcon()); // Notes + tabBar()->setTabIcon(1, QIcon()); // Equipment + hideMessage(); + resetPallete(); + // no harm done to call cancelPlan even if we were not in ADD or PLAN mode... + DivePlannerPointsModel::instance()->cancelPlan(); + if(lastMode == ADD) + MainWindow::instance()->dive_list()->restoreSelection(); + + // now make sure that the correct dive is displayed + if (selected_dive >= 0) + copy_dive(current_dive, &displayed_dive); + else + clear_dive(&displayed_dive); + updateDiveInfo(selected_dive < 0); + DivePictureModel::instance()->updateDivePictures(); + // the user could have edited the location and then canceled the edit + // let's get the correct location back in view +#ifndef NO_MARBLE + GlobeGPS::instance()->centerOnDiveSite(get_dive_site_by_uuid(displayed_dive.dive_site_uuid)); +#endif + // show the profile and dive info + MainWindow::instance()->graphics()->replot(); + MainWindow::instance()->setEnabledToolbar(true); + cylindersModel->changed = false; + weightModel->changed = false; + cylindersModel->updateDive(); + weightModel->updateDive(); + extraDataModel->updateDive(); + ui.editDiveSiteButton->setEnabled(true); +} +#undef EDIT_TEXT2 + +void MainTab::markChangedWidget(QWidget *w) +{ + QPalette p; + qreal h, s, l, a; + enableEdition(); + qApp->palette().color(QPalette::Text).getHslF(&h, &s, &l, &a); + p.setBrush(QPalette::Base, (l <= 0.3) ? QColor(Qt::yellow).lighter() : (l <= 0.6) ? QColor(Qt::yellow).light() : /* else */ QColor(Qt::yellow).darker(300)); + w->setPalette(p); + modified = true; +} + +void MainTab::on_buddy_textChanged() +{ + if (editMode == IGNORE || acceptingEdit == true) + return; + + if (same_string(displayed_dive.buddy, ui.buddy->toPlainText().toUtf8().data())) + return; + + QStringList text_list = ui.buddy->toPlainText().split(",", QString::SkipEmptyParts); + for (int i = 0; i < text_list.size(); i++) + text_list[i] = text_list[i].trimmed(); + QString text = text_list.join(", "); + free(displayed_dive.buddy); + displayed_dive.buddy = strdup(text.toUtf8().data()); + markChangedWidget(ui.buddy); +} + +void MainTab::on_divemaster_textChanged() +{ + if (editMode == IGNORE || acceptingEdit == true) + return; + + if (same_string(displayed_dive.divemaster, ui.divemaster->toPlainText().toUtf8().data())) + return; + + QStringList text_list = ui.divemaster->toPlainText().split(",", QString::SkipEmptyParts); + for (int i = 0; i < text_list.size(); i++) + text_list[i] = text_list[i].trimmed(); + QString text = text_list.join(", "); + free(displayed_dive.divemaster); + displayed_dive.divemaster = strdup(text.toUtf8().data()); + markChangedWidget(ui.divemaster); +} + +void MainTab::on_airtemp_textChanged(const QString &text) +{ + if (editMode == IGNORE || acceptingEdit == true) + return; + displayed_dive.airtemp.mkelvin = parseTemperatureToMkelvin(text); + markChangedWidget(ui.airtemp); + validate_temp_field(ui.airtemp, text); +} + +void MainTab::divetype_Changed(int index) +{ + if (editMode == IGNORE) + return; + struct divecomputer *displayed_dc = get_dive_dc(&displayed_dive, dc_number); + displayed_dc->divemode = (enum dive_comp_type) index; + update_setpoint_events(displayed_dc); + markChangedWidget(ui.DiveType); + MainWindow::instance()->graphics()->recalcCeiling(); +} + +void MainTab::on_watertemp_textChanged(const QString &text) +{ + if (editMode == IGNORE || acceptingEdit == true) + return; + displayed_dive.watertemp.mkelvin = parseTemperatureToMkelvin(text); + markChangedWidget(ui.watertemp); + validate_temp_field(ui.watertemp, text); +} + +void MainTab::validate_temp_field(QLineEdit *tempField, const QString &text) +{ + static bool missing_unit = false; + static bool missing_precision = false; + if (!text.contains(QRegExp("^[-+]{0,1}[0-9]+([,.][0-9]+){0,1}(°[CF]){0,1}$")) && + !text.isEmpty() && + !text.contains(QRegExp("^[-+]$"))) { + if (text.contains(QRegExp("^[-+]{0,1}[0-9]+([,.][0-9]+){0,1}(°)$")) && !missing_unit) { + if (!missing_unit) { + missing_unit = true; + return; + } + } + if (text.contains(QRegExp("^[-+]{0,1}[0-9]+([,.]){0,1}(°[CF]){0,1}$")) && !missing_precision) { + if (!missing_precision) { + missing_precision = true; + return; + } + } + QPalette p; + p.setBrush(QPalette::Base, QColor(Qt::red).lighter()); + tempField->setPalette(p); + } else { + missing_unit = false; + missing_precision = false; + } +} + +void MainTab::on_dateEdit_dateChanged(const QDate &date) +{ + if (editMode == IGNORE || acceptingEdit == true) + return; + markChangedWidget(ui.dateEdit); + QDateTime dateTime = QDateTime::fromTime_t(displayed_dive.when - gettimezoneoffset(displayed_dive.when)); + dateTime.setTimeSpec(Qt::UTC); + dateTime.setDate(date); + DivePlannerPointsModel::instance()->getDiveplan().when = displayed_dive.when = dateTime.toTime_t(); + emit dateTimeChanged(); +} + +void MainTab::on_timeEdit_timeChanged(const QTime &time) +{ + if (editMode == IGNORE || acceptingEdit == true) + return; + markChangedWidget(ui.timeEdit); + QDateTime dateTime = QDateTime::fromTime_t(displayed_dive.when - gettimezoneoffset(displayed_dive.when)); + dateTime.setTimeSpec(Qt::UTC); + dateTime.setTime(time); + DivePlannerPointsModel::instance()->getDiveplan().when = displayed_dive.when = dateTime.toTime_t(); + emit dateTimeChanged(); +} + +// changing the tags on multiple dives is semantically strange - what's the right thing to do? +// here's what I think... add the tags that were added to the displayed dive and remove the tags +// that were removed from it +void MainTab::saveTags() +{ + struct dive *cd = current_dive; + struct tag_entry *added_list = NULL; + struct tag_entry *removed_list = NULL; + struct tag_entry *tl; + + taglist_free(displayed_dive.tag_list); + displayed_dive.tag_list = NULL; + Q_FOREACH (const QString& tag, ui.tagWidget->getBlockStringList()) + taglist_add_tag(&displayed_dive.tag_list, tag.toUtf8().data()); + taglist_cleanup(&displayed_dive.tag_list); + + // figure out which tags were added and which tags were removed + added_list = taglist_added(cd->tag_list, displayed_dive.tag_list); + removed_list = taglist_added(displayed_dive.tag_list, cd->tag_list); + // dump_taglist("added tags:", added_list); + // dump_taglist("removed tags:", removed_list); + + // we need to check if the tags were changed before just overwriting them + if (added_list == NULL && removed_list == NULL) + return; + + MODIFY_SELECTED_DIVES( + // create a new tag list and all the existing tags that were not + // removed and then all the added tags + struct tag_entry *new_tag_list; + new_tag_list = NULL; + tl = mydive->tag_list; + while (tl) { + if (!taglist_contains(removed_list, tl->tag->name)) + taglist_add_tag(&new_tag_list, tl->tag->name); + tl = tl->next; + } + tl = added_list; + while (tl) { + taglist_add_tag(&new_tag_list, tl->tag->name); + tl = tl->next; + } + taglist_free(mydive->tag_list); + mydive->tag_list = new_tag_list; + ); + taglist_free(added_list); + taglist_free(removed_list); +} + +// buddy and divemaster are represented in the UI just like the tags, but the internal +// representation is just a string (with commas as delimiters). So we need to do the same +// thing we did for tags, just differently +void MainTab::saveTaggedStrings() +{ + QStringList addedList, removedList; + struct dive *cd = current_dive; + + diffTaggedStrings(cd->buddy, displayed_dive.buddy, addedList, removedList); + MODIFY_SELECTED_DIVES( + QStringList oldList = QString(mydive->buddy).split(QRegExp("\\s*,\\s*"), QString::SkipEmptyParts); + QString newString; + QString comma; + Q_FOREACH (const QString tag, oldList) { + if (!removedList.contains(tag, Qt::CaseInsensitive)) { + newString += comma + tag; + comma = ", "; + } + } + Q_FOREACH (const QString tag, addedList) { + if (!oldList.contains(tag, Qt::CaseInsensitive)) { + newString += comma + tag; + comma = ", "; + } + } + free(mydive->buddy); + mydive->buddy = copy_string(qPrintable(newString)); + ); + addedList.clear(); + removedList.clear(); + diffTaggedStrings(cd->divemaster, displayed_dive.divemaster, addedList, removedList); + MODIFY_SELECTED_DIVES( + QStringList oldList = QString(mydive->divemaster).split(QRegExp("\\s*,\\s*"), QString::SkipEmptyParts); + QString newString; + QString comma; + Q_FOREACH (const QString tag, oldList) { + if (!removedList.contains(tag, Qt::CaseInsensitive)) { + newString += comma + tag; + comma = ", "; + } + } + Q_FOREACH (const QString tag, addedList) { + if (!oldList.contains(tag, Qt::CaseInsensitive)) { + newString += comma + tag; + comma = ", "; + } + } + free(mydive->divemaster); + mydive->divemaster = copy_string(qPrintable(newString)); + ); +} + +void MainTab::diffTaggedStrings(QString currentString, QString displayedString, QStringList &addedList, QStringList &removedList) +{ + QStringList displayedList, currentList; + currentList = currentString.split(',', QString::SkipEmptyParts); + displayedList = displayedString.split(',', QString::SkipEmptyParts); + Q_FOREACH ( const QString tag, currentList) { + if (!displayedList.contains(tag, Qt::CaseInsensitive)) + removedList << tag.trimmed(); + } + Q_FOREACH (const QString tag, displayedList) { + if (!currentList.contains(tag, Qt::CaseInsensitive)) + addedList << tag.trimmed(); + } +} + +void MainTab::on_tagWidget_textChanged() +{ + char buf[1024]; + + if (editMode == IGNORE || acceptingEdit == true) + return; + + taglist_get_tagstring(displayed_dive.tag_list, buf, 1024); + if (same_string(buf, ui.tagWidget->toPlainText().toUtf8().data())) + return; + + markChangedWidget(ui.tagWidget); +} + +void MainTab::on_location_textChanged() +{ + if (editMode == IGNORE) + return; + + // we don't want to act on the edit until editing is finished, + // but we want to mark the field so it's obvious it is being edited + QString currentLocation; + struct dive_site *ds = get_dive_site_by_uuid(displayed_dive.dive_site_uuid); + if (ds) + currentLocation = ds->name; + if (ui.location->text() != currentLocation) + markChangedWidget(ui.location); +} + +void MainTab::on_location_diveSiteSelected() +{ + if (editMode == IGNORE || acceptingEdit == true) + return; + + if (ui.location->text().isEmpty()) { + displayed_dive.dive_site_uuid = 0; + markChangedWidget(ui.location); + emit diveSiteChanged(0); + return; + } else { + if (ui.location->currDiveSiteUuid() != displayed_dive.dive_site_uuid) { + markChangedWidget(ui.location); + } else { + QPalette p; + ui.location->setPalette(p); + } + } +} + +void MainTab::on_diveTripLocation_textEdited(const QString& text) +{ + if (currentTrip) { + free(displayedTrip.location); + displayedTrip.location = strdup(qPrintable(text)); + markChangedWidget(ui.diveTripLocation); + } +} + +void MainTab::on_suit_textChanged(const QString &text) +{ + if (editMode == IGNORE || acceptingEdit == true) + return; + free(displayed_dive.suit); + displayed_dive.suit = strdup(text.toUtf8().data()); + markChangedWidget(ui.suit); +} + +void MainTab::on_notes_textChanged() +{ + if (editMode == IGNORE || acceptingEdit == true) + return; + if (currentTrip) { + if (same_string(displayedTrip.notes, ui.notes->toPlainText().toUtf8().data())) + return; + free(displayedTrip.notes); + displayedTrip.notes = strdup(ui.notes->toPlainText().toUtf8().data()); + } else { + if (same_string(displayed_dive.notes, ui.notes->toPlainText().toUtf8().data())) + return; + free(displayed_dive.notes); + if (ui.notes->toHtml().indexOf("<table") != -1) + displayed_dive.notes = strdup(ui.notes->toHtml().toUtf8().data()); + else + displayed_dive.notes = strdup(ui.notes->toPlainText().toUtf8().data()); + } + markChangedWidget(ui.notes); +} + +void MainTab::on_rating_valueChanged(int value) +{ + if (acceptingEdit == true) + return; + if (displayed_dive.rating != value) { + displayed_dive.rating = value; + modified = true; + enableEdition(); + } +} + +void MainTab::on_visibility_valueChanged(int value) +{ + if (acceptingEdit == true) + return; + if (displayed_dive.visibility != value) { + displayed_dive.visibility = value; + modified = true; + enableEdition(); + } +} + +#undef MODIFY_SELECTED_DIVES +#undef EDIT_TEXT +#undef EDIT_VALUE + +void MainTab::editCylinderWidget(const QModelIndex &index) +{ + // we need a local copy or bad things happen when enableEdition() is called + QModelIndex editIndex = index; + if (cylindersModel->changed && editMode == NONE) { + enableEdition(); + return; + } + if (editIndex.isValid() && editIndex.column() != CylindersModel::REMOVE) { + if (editMode == NONE) + enableEdition(); + ui.cylinders->edit(editIndex); + } +} + +void MainTab::editWeightWidget(const QModelIndex &index) +{ + if (editMode == NONE) + enableEdition(); + + if (index.isValid() && index.column() != WeightModel::REMOVE) + ui.weights->edit(index); +} + +void MainTab::escDetected() +{ + if (editMode != NONE) + rejectChanges(); +} + +void MainTab::photoDoubleClicked(const QString filePath) +{ + QDesktopServices::openUrl(QUrl::fromLocalFile(filePath)); +} + +void MainTab::removeSelectedPhotos() +{ + if (!ui.photosView->selectionModel()->hasSelection()) + return; + + QModelIndex photoIndex = ui.photosView->selectionModel()->selectedIndexes().first(); + QString fileUrl = photoIndex.data(Qt::DisplayPropertyRole).toString(); + DivePictureModel::instance()->removePicture(fileUrl); +} + +#define SHOW_SELECTIVE(_component) \ + if (what._component) \ + ui._component->setText(displayed_dive._component); + +void MainTab::showAndTriggerEditSelective(struct dive_components what) +{ + // take the data in our copyPasteDive and apply it to selected dives + enableEdition(); + copyPaste = true; + SHOW_SELECTIVE(buddy); + SHOW_SELECTIVE(divemaster); + SHOW_SELECTIVE(suit); + if (what.notes) { + QString tmp(displayed_dive.notes); + if (tmp.contains("<table")) + ui.notes->setHtml(tmp); + else + ui.notes->setPlainText(tmp); + } + if (what.rating) + ui.rating->setCurrentStars(displayed_dive.rating); + if (what.visibility) + ui.visibility->setCurrentStars(displayed_dive.visibility); + if (what.divesite) + ui.location->setCurrentDiveSiteUuid(displayed_dive.dive_site_uuid); + if (what.tags) { + char buf[1024]; + taglist_get_tagstring(displayed_dive.tag_list, buf, 1024); + ui.tagWidget->setText(QString(buf)); + } + if (what.cylinders) { + cylindersModel->updateDive(); + cylindersModel->changed = true; + } + if (what.weights) { + weightModel->updateDive(); + weightModel->changed = true; + } +} diff --git a/desktop-widgets/maintab.h b/desktop-widgets/maintab.h new file mode 100644 index 000000000..20b4da690 --- /dev/null +++ b/desktop-widgets/maintab.h @@ -0,0 +1,129 @@ +/* + * maintab.h + * + * header file for the main tab of Subsurface + * + */ +#ifndef MAINTAB_H +#define MAINTAB_H + +#include <QTabWidget> +#include <QDialog> +#include <QMap> +#include <QUuid> + +#include "ui_maintab.h" +#include "completionmodels.h" +#include "divelocationmodel.h" +#include "dive.h" + +class WeightModel; +class CylindersModel; +class ExtraDataModel; +class DivePictureModel; +class QCompleter; + +struct Completers { + QCompleter *divemaster; + QCompleter *buddy; + QCompleter *suit; + QCompleter *tags; +}; + +class MainTab : public QTabWidget { + Q_OBJECT +public: + enum EditMode { + NONE, + DIVE, + TRIP, + ADD, + MANUALLY_ADDED_DIVE, + IGNORE + }; + + MainTab(QWidget *parent = 0); + ~MainTab(); + void clearStats(); + void clearInfo(); + void clearEquipment(); + void reload(); + void initialUiSetup(); + bool isEditing(); + void updateCoordinatesText(qreal lat, qreal lon); + void refreshDisplayedDiveSite(); + void nextInputField(QKeyEvent *event); + void showAndTriggerEditSelective(struct dive_components what); + +signals: + void addDiveFinished(); + void dateTimeChanged(); + void diveSiteChanged(struct dive_site * ds); +public +slots: + void addCylinder_clicked(); + void addWeight_clicked(); + void refreshDiveInfo(); + void updateDiveInfo(bool clear = false); + void acceptChanges(); + void rejectChanges(); + void on_location_diveSiteSelected(); + void on_location_textChanged(); + void on_divemaster_textChanged(); + void on_buddy_textChanged(); + void on_suit_textChanged(const QString &text); + void on_diveTripLocation_textEdited(const QString& text); + void on_notes_textChanged(); + void on_airtemp_textChanged(const QString &text); + void divetype_Changed(int); + void on_watertemp_textChanged(const QString &text); + void validate_temp_field(QLineEdit *tempField, const QString &text); + void on_dateEdit_dateChanged(const QDate &date); + void on_timeEdit_timeChanged(const QTime & time); + void on_rating_valueChanged(int value); + void on_visibility_valueChanged(int value); + void on_tagWidget_textChanged(); + void editCylinderWidget(const QModelIndex &index); + void editWeightWidget(const QModelIndex &index); + void addDiveStarted(); + void addMessageAction(QAction *action); + void hideMessage(); + void closeMessage(); + void displayMessage(QString str); + void enableEdition(EditMode newEditMode = NONE); + void toggleTriggeredColumn(); + void updateTextLabels(bool showUnits = true); + void escDetected(void); + void photoDoubleClicked(const QString filePath); + void removeSelectedPhotos(); + void showLocation(); + void enableGeoLookupEdition(); + void disableGeoLookupEdition(); + void setCurrentLocationIndex(); + EditMode getEditMode() const; +private: + Ui::MainTab ui; + WeightModel *weightModel; + CylindersModel *cylindersModel; + ExtraDataModel *extraDataModel; + EditMode editMode; + BuddyCompletionModel buddyModel; + DiveMasterCompletionModel diveMasterModel; + SuitCompletionModel suitModel; + TagCompletionModel tagModel; + DivePictureModel *divePictureModel; + Completers completers; + bool modified; + bool copyPaste; + void resetPallete(); + void saveTags(); + void saveTaggedStrings(); + void diffTaggedStrings(QString currentString, QString displayedString, QStringList &addedList, QStringList &removedList); + void markChangedWidget(QWidget *w); + dive_trip_t *currentTrip; + dive_trip_t displayedTrip; + bool acceptingEdit; + uint32_t updateDiveSite(uint32_t pickedUuid, int divenr); +}; + +#endif // MAINTAB_H diff --git a/desktop-widgets/maintab.ui b/desktop-widgets/maintab.ui new file mode 100644 index 000000000..7bc516b1a --- /dev/null +++ b/desktop-widgets/maintab.ui @@ -0,0 +1,1267 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainTab</class> + <widget class="QTabWidget" name="MainTab"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>463</width> + <height>815</height> + </rect> + </property> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="notesTab"> + <attribute name="title"> + <string>Notes</string> + </attribute> + <attribute name="toolTip"> + <string>General notes about the current selection</string> + </attribute> + <layout class="QGridLayout" name="diveNotesLayout"> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="topMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>5</number> + </property> + <property name="bottomMargin"> + <number>5</number> + </property> + <property name="spacing"> + <number>0</number> + </property> + <item row="2" column="1"> + <widget class="KMessageWidget" name="diveNotesMessage"/> + </item> + <item row="3" column="1"> + <widget class="QScrollArea" name="scrollArea"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Plain</enum> + </property> + <property name="widgetResizable"> + <bool>true</bool> + </property> + <widget class="QWidget" name="scrollAreaWidgetContents"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>445</width> + <height>726</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <layout class="QGridLayout" name="gridLayout"> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>5</number> + </property> + <property name="horizontalSpacing"> + <number>8</number> + </property> + <property name="verticalSpacing"> + <number>0</number> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Date</string> + </property> + <property name="alignment"> + <set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Time</string> + </property> + <property name="alignment"> + <set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QLabel" name="airTempLabel"> + <property name="text"> + <string>Air temp.</string> + </property> + <property name="alignment"> + <set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QLabel" name="waterTempLabel"> + <property name="text"> + <string>Water temp.</string> + </property> + <property name="alignment"> + <set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QDateEdit" name="dateEdit"> + <property name="calendarPopup"> + <bool>true</bool> + </property> + <property name="timeSpec"> + <enum>Qt::UTC</enum> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QTimeEdit" name="timeEdit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="timeSpec"> + <enum>Qt::UTC</enum> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QLineEdit" name="airtemp"> + <property name="readOnly"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="1" column="3"> + <widget class="QLineEdit" name="watertemp"> + <property name="readOnly"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>5</number> + </property> + <item> + <layout class="QHBoxLayout" name="LocationLayout" stretch="0,1"> + <item> + <widget class="QLabel" name="LocationLabel"> + <property name="text"> + <string>Location</string> + </property> + <property name="alignment"> + <set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="locationTags"> + <property name="text"> + <string/> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <property name="spacing"> + <number>2</number> + </property> + <item> + <widget class="DiveLocationLineEdit" name="location"/> + </item> + <item> + <widget class="QToolButton" name="editDiveSiteButton"> + <property name="toolTip"> + <string>Edit dive site</string> + </property> + <property name="text"> + <string>...</string> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/geocode</normaloff>:/geocode</iconset> + </property> + </widget> + </item> + <item> + <widget class="QtWaitingSpinner" name="waitingSpinner" native="true"/> + </item> + </layout> + </item> + <item> + <widget class="QLineEdit" name="diveTripLocation"/> + </item> + </layout> + </item> + <item> + <layout class="QGridLayout" name="gridLayout_4"> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>5</number> + </property> + <property name="horizontalSpacing"> + <number>5</number> + </property> + <property name="verticalSpacing"> + <number>0</number> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="DivemasterLabel"> + <property name="text"> + <string>Divemaster</string> + </property> + <property name="alignment"> + <set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="BuddyLabel"> + <property name="text"> + <string>Buddy</string> + </property> + <property name="alignment"> + <set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="TagWidget" name="divemaster"> + <property name="readOnly"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="TagWidget" name="buddy"> + <property name="readOnly"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QGridLayout" name="gridLayout_3" columnstretch="0,0,1"> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>5</number> + </property> + <property name="horizontalSpacing"> + <number>5</number> + </property> + <property name="verticalSpacing"> + <number>0</number> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="RatingLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Rating</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="visibilityLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Visibility</string> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QLabel" name="SuitLabel"> + <property name="text"> + <string>Suit</string> + </property> + </widget> + </item> + <item row="1" column="0" alignment="Qt::AlignVCenter"> + <widget class="StarWidget" name="rating" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + </widget> + </item> + <item row="1" column="1" alignment="Qt::AlignVCenter"> + <widget class="StarWidget" name="visibility" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QLineEdit" name="suit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="readOnly"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QGridLayout" name="gridLayout_2"> + <property name="horizontalSpacing"> + <number>5</number> + </property> + <property name="verticalSpacing"> + <number>0</number> + </property> + <item row="1" column="1"> + <widget class="QComboBox" name="DiveType"/> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="TagLabel"> + <property name="text"> + <string>Tags</string> + </property> + <property name="alignment"> + <set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="TypeLabel"> + <property name="text"> + <string>Dive mode</string> + </property> + <property name="alignment"> + <set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="TagWidget" name="tagWidget"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="verticalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="lineWrapMode"> + <enum>QPlainTextEdit::NoWrap</enum> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="NotesLabel"> + <property name="text"> + <string>Notes</string> + </property> + <property name="alignment"> + <set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="notesAndSocialNetworksLayout"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <widget class="QTextEdit" name="notes"> + <property name="readOnly"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QWidget" name="socialNetworks" native="true"> + <layout class="QVBoxLayout" name="socialNetworksLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + </layout> + </widget> + </item> + </layout> + </item> + </layout> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="equipmentTab"> + <attribute name="title"> + <string>Equipment</string> + </attribute> + <attribute name="toolTip"> + <string>Used equipment in the current selection</string> + </attribute> + <layout class="QGridLayout" name="equiptmentTabLayout"> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="topMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>5</number> + </property> + <property name="bottomMargin"> + <number>5</number> + </property> + <item row="0" column="0"> + <widget class="KMessageWidget" name="diveEquipmentMessage"/> + </item> + <item row="1" column="0"> + <widget class="QScrollArea" name="scrollArea_2"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Plain</enum> + </property> + <property name="widgetResizable"> + <bool>true</bool> + </property> + <widget class="QWidget" name="scrollAreaWidgetContents_2"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>445</width> + <height>754</height> + </rect> + </property> + <layout class="QGridLayout" name="equipmentTabScrollAreaLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <property name="spacing"> + <number>2</number> + </property> + <item row="1" column="0"> + <widget class="QWidget" name="widget" native="true"> + <layout class="QVBoxLayout" name="cylinderWeightsLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QSplitter" name="splitter"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <widget class="TableView" name="cylinders" native="true"/> + <widget class="TableView" name="weights" native="true"/> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="infoTab"> + <attribute name="title"> + <string>Info</string> + </attribute> + <attribute name="toolTip"> + <string>Dive information</string> + </attribute> + <layout class="QGridLayout" name="diveInfoLayout"> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="topMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>5</number> + </property> + <property name="bottomMargin"> + <number>5</number> + </property> + <item row="0" column="0"> + <widget class="KMessageWidget" name="diveInfoMessage"/> + </item> + <item row="1" column="0"> + <widget class="QScrollArea" name="scrollArea_3"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Plain</enum> + </property> + <property name="widgetResizable"> + <bool>true</bool> + </property> + <widget class="QWidget" name="scrollAreaWidgetContents_3"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>287</width> + <height>320</height> + </rect> + </property> + <layout class="QGridLayout" name="diveInfoScrollAreaLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <property name="spacing"> + <number>2</number> + </property> + <item row="0" column="0"> + <widget class="QGroupBox" name="groupBox_5"> + <property name="title"> + <string>Date</string> + </property> + <layout class="QHBoxLayout" name="diveInfoDateLayout"> + <item> + <widget class="QLabel" name="dateText"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="0" column="2"> + <widget class="QGroupBox" name="groupBox_12"> + <property name="title"> + <string>Interval</string> + </property> + <layout class="QHBoxLayout" name="diveInfoSurfintervallLayout"> + <item> + <widget class="QLabel" name="surfaceIntervalText"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="1" column="0"> + <widget class="QGroupBox" name="groupBox_3"> + <property name="title"> + <string>Gases used</string> + </property> + <layout class="QHBoxLayout" name="diveInfoGasesUsedLayout"> + <item> + <widget class="QLabel" name="oxygenHeliumText"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="1" column="1"> + <widget class="QGroupBox" name="groupBox_4"> + <property name="title"> + <string>Gas consumed</string> + </property> + <layout class="QHBoxLayout" name="diveInfoGasConsumedLayout"> + <item> + <widget class="QLabel" name="gasUsedText"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="1" column="2"> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>SAC</string> + </property> + <layout class="QHBoxLayout" name="diveInfoSacLayout"> + <item> + <widget class="QLabel" name="sacText"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="2" column="0"> + <widget class="QGroupBox" name="groupBox_15"> + <property name="title"> + <string>CNS</string> + </property> + <layout class="QHBoxLayout" name="diveInfoCnsLayout"> + <item> + <widget class="QLabel" name="maxcnsText"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="2" column="1"> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>OTU</string> + </property> + <layout class="QHBoxLayout" name="diveInfoOtuLayout"> + <item> + <widget class="QLabel" name="otuText"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="3" column="1"> + <widget class="QGroupBox" name="groupBox_6"> + <property name="title"> + <string>Max. depth</string> + </property> + <layout class="QHBoxLayout" name="diveInfoMaxDepthLayout"> + <item> + <widget class="QLabel" name="maximumDepthText"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="3" column="0"> + <widget class="QGroupBox" name="groupBox_7"> + <property name="title"> + <string>Avg. depth</string> + </property> + <layout class="QHBoxLayout" name="diveInfoAvgDepthLayout"> + <item> + <widget class="QLabel" name="averageDepthText"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="2" column="2"> + <widget class="QGroupBox" name="groupBox_10"> + <property name="title"> + <string>Air pressure</string> + </property> + <layout class="QHBoxLayout" name="diveInfoAirPressureLayout"> + <item> + <widget class="QLabel" name="airPressureText"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="3" column="2"> + <widget class="QGroupBox" name="groupBox_9"> + <property name="title"> + <string>Air temp.</string> + </property> + <layout class="QHBoxLayout" name="diveInfoAirTempLayout"> + <item> + <widget class="QLabel" name="airTemperatureText"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="4" column="0"> + <widget class="QGroupBox" name="groupBox_8"> + <property name="title"> + <string>Water temp.</string> + </property> + <layout class="QHBoxLayout" name="diveInfoWaterTempLayout"> + <item> + <widget class="QLabel" name="waterTemperatureText"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="0" column="1"> + <widget class="QGroupBox" name="groupBox_11"> + <property name="title"> + <string>Dive time</string> + </property> + <layout class="QHBoxLayout" name="diveInfoDiveTimeLayout"> + <item> + <widget class="QLabel" name="diveTimeText"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="4" column="1"> + <widget class="QGroupBox" name="groupBox_1"> + <property name="title"> + <string>Salinity</string> + </property> + <layout class="QHBoxLayout" name="diveInfoSalinityLayout"> + <item> + <widget class="QLabel" name="salinityText"> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="5" column="0"> + <spacer> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="statisticsTab"> + <attribute name="title"> + <string>Stats</string> + </attribute> + <attribute name="toolTip"> + <string>Simple statistics about the selection</string> + </attribute> + <layout class="QGridLayout" name="statsLayout"> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="topMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>5</number> + </property> + <property name="bottomMargin"> + <number>5</number> + </property> + <item row="1" column="1"> + <widget class="QScrollArea" name="scrollArea_4"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Plain</enum> + </property> + <property name="widgetResizable"> + <bool>true</bool> + </property> + <widget class="QWidget" name="scrollAreaWidgetContents_4"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>297</width> + <height>187</height> + </rect> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <item> + <widget class="QGroupBox" name="groupBoxb"> + <property name="title"> + <string>Depth</string> + </property> + <layout class="QHBoxLayout" name="statsDepthLayout"> + <item> + <widget class="MinMaxAvgWidget" name="depthLimits" native="true"/> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_14"> + <property name="title"> + <string>Duration</string> + </property> + <layout class="QHBoxLayout" name="statsDurationLayout"> + <item> + <widget class="MinMaxAvgWidget" name="timeLimits" native="true"/> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <widget class="QGroupBox" name="groupBox_8b"> + <property name="title"> + <string>Temperature</string> + </property> + <layout class="QHBoxLayout" name="statsTempLayout"> + <item> + <widget class="MinMaxAvgWidget" name="tempLimits" native="true"/> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_11b"> + <property name="title"> + <string>Total time</string> + </property> + <layout class="QHBoxLayout" name="statsTotalTimeLayout"> + <item> + <widget class="QLabel" name="totalTimeAllText"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_7b"> + <property name="title"> + <string>Dives</string> + </property> + <layout class="QHBoxLayout" name="statsDivesLayout"> + <item> + <widget class="QLabel" name="divesAllText"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer_3"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_7"> + <item> + <widget class="QGroupBox" name="groupBox_4b"> + <property name="title"> + <string>SAC</string> + </property> + <layout class="QHBoxLayout" name="statsSacLayout"> + <item> + <widget class="MinMaxAvgWidget" name="sacLimits" native="true"/> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_13"> + <property name="title"> + <string>Gas consumption</string> + </property> + <layout class="QHBoxLayout" name="statsGasConsumptionLayout"> + <item> + <widget class="QLabel" name="gasConsumption"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer_4"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + </widget> + </item> + <item row="0" column="1"> + <widget class="KMessageWidget" name="diveStatisticsMessage"/> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab"> + <attribute name="title"> + <string>Photos</string> + </attribute> + <attribute name="toolTip"> + <string>All photos from the current selection</string> + </attribute> + <layout class="QVBoxLayout" name="photosLayout"> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="topMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>5</number> + </property> + <property name="bottomMargin"> + <number>5</number> + </property> + <item> + <widget class="DivePictureWidget" name="photosView"> + <property name="viewMode"> + <enum>QListView::IconMode</enum> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="tab_2"> + <attribute name="title"> + <string>Extra data</string> + </attribute> + <attribute name="toolTip"> + <string>Adittional data from the dive computer</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="topMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>5</number> + </property> + <property name="bottomMargin"> + <number>5</number> + </property> + <item> + <widget class="QTableView" name="extraData"/> + </item> + </layout> + </widget> + </widget> + <customwidgets> + <customwidget> + <class>KMessageWidget</class> + <extends>QFrame</extends> + <header>kmessagewidget.h</header> + <container>1</container> + </customwidget> + <customwidget> + <class>StarWidget</class> + <extends>QWidget</extends> + <header>starwidget.h</header> + <container>1</container> + </customwidget> + <customwidget> + <class>MinMaxAvgWidget</class> + <extends>QWidget</extends> + <header>simplewidgets.h</header> + <container>1</container> + </customwidget> + <customwidget> + <class>TableView</class> + <extends>QWidget</extends> + <header>tableview.h</header> + <container>1</container> + </customwidget> + <customwidget> + <class>TagWidget</class> + <extends>QPlainTextEdit</extends> + <header>tagwidget.h</header> + </customwidget> + <customwidget> + <class>DivePictureWidget</class> + <extends>QListView</extends> + <header>divepicturewidget.h</header> + </customwidget> + <customwidget> + <class>QtWaitingSpinner</class> + <extends>QWidget</extends> + <header>qtwaitingspinner.h</header> + <container>1</container> + </customwidget> + <customwidget> + <class>DiveLocationLineEdit</class> + <extends>QLineEdit</extends> + <header>locationinformation.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>dateEdit</tabstop> + <tabstop>timeEdit</tabstop> + <tabstop>airtemp</tabstop> + <tabstop>watertemp</tabstop> + <tabstop>divemaster</tabstop> + <tabstop>buddy</tabstop> + <tabstop>rating</tabstop> + <tabstop>visibility</tabstop> + <tabstop>suit</tabstop> + <tabstop>notes</tabstop> + </tabstops> + <resources> + <include location="../subsurface.qrc"/> + </resources> + <connections/> +</ui> diff --git a/desktop-widgets/mainwindow.cpp b/desktop-widgets/mainwindow.cpp new file mode 100644 index 000000000..e1e0d81a2 --- /dev/null +++ b/desktop-widgets/mainwindow.cpp @@ -0,0 +1,1923 @@ +/* + * mainwindow.cpp + * + * classes for the main UI window in Subsurface + */ +#include "mainwindow.h" + +#include <QFileDialog> +#include <QMessageBox> +#include <QDesktopWidget> +#include <QSettings> +#include <QShortcut> +#include <QToolBar> +#include "version.h" +#include "divelistview.h" +#include "downloadfromdivecomputer.h" +#include "preferences.h" +#include "subsurfacewebservices.h" +#include "divecomputermanagementdialog.h" +#include "about.h" +#include "updatemanager.h" +#include "planner.h" +#include "filtermodels.h" +#include "profile/profilewidget2.h" +#include "globe.h" +#include "divecomputer.h" +#include "maintab.h" +#include "diveplanner.h" +#ifndef NO_PRINTING +#include <QPrintDialog> +#include "printdialog.h" +#endif +#include "tankinfomodel.h" +#include "weigthsysteminfomodel.h" +#include "yearlystatisticsmodel.h" +#include "diveplannermodel.h" +#include "divelogimportdialog.h" +#include "divelogexportdialog.h" +#include "usersurvey.h" +#include "divesitehelpers.h" +#include "windowtitleupdate.h" +#include "locationinformation.h" + +#ifndef NO_USERMANUAL +#include "usermanual.h" +#endif +#include "divepicturemodel.h" +#include "git-access.h" +#include <QNetworkProxy> +#include <QUndoStack> +#include <qthelper.h> +#include <QtConcurrentRun> +#include "subsurface-core/color.h" + +#if defined(FBSUPPORT) +#include "socialnetworks.h" +#endif + +QProgressDialog *progressDialog = NULL; +bool progressDialogCanceled = false; + +extern "C" int updateProgress(int percent) +{ + if (progressDialog) + progressDialog->setValue(percent); + return progressDialogCanceled; +} + +MainWindow *MainWindow::m_Instance = NULL; + +MainWindow::MainWindow() : QMainWindow(), + actionNextDive(0), + actionPreviousDive(0), + helpView(0), + state(VIEWALL), + survey(0) +{ + Q_ASSERT_X(m_Instance == NULL, "MainWindow", "MainWindow recreated!"); + m_Instance = this; + ui.setupUi(this); + read_hashes(); + // Define the States of the Application Here, Currently the states are situations where the different + // widgets will change on the mainwindow. + + // for the "default" mode + MainTab *mainTab = new MainTab(); + DiveListView *diveListView = new DiveListView(); + ProfileWidget2 *profileWidget = new ProfileWidget2(); + +#ifndef NO_MARBLE + GlobeGPS *globeGps = GlobeGPS::instance(); +#else + QWidget *globeGps = NULL; +#endif + + PlannerSettingsWidget *plannerSettings = new PlannerSettingsWidget(); + DivePlannerWidget *plannerWidget = new DivePlannerWidget(); + PlannerDetails *plannerDetails = new PlannerDetails(); + + // what is a sane order for those icons? we should have the ones the user is + // most likely to want towards the top so they are always visible + // and the ones that someone likely sets and then never touches again towards the bottom + profileToolbarActions << ui.profCalcCeiling << ui.profCalcAllTissues << // start with various ceilings + ui.profIncrement3m << ui.profDcCeiling << + ui.profPhe << ui.profPn2 << ui.profPO2 << // partial pressure graphs + ui.profRuler << ui.profScaled << // measuring and scaling + ui.profTogglePicture << ui.profTankbar << + ui.profMod << ui.profNdl_tts << // various values that a user is either interested in or not + ui.profEad << ui.profSAC << + ui.profHR << // very few dive computers support this + ui.profTissues; // maybe less frequently used + + QToolBar *toolBar = new QToolBar(); + Q_FOREACH (QAction *a, profileToolbarActions) + toolBar->addAction(a); + toolBar->setOrientation(Qt::Vertical); + toolBar->setIconSize(QSize(24,24)); + QWidget *profileContainer = new QWidget(); + QHBoxLayout *profLayout = new QHBoxLayout(); + profLayout->setSpacing(0); + profLayout->setMargin(0); + profLayout->setContentsMargins(0,0,0,0); + profLayout->addWidget(toolBar); + profLayout->addWidget(profileWidget); + profileContainer->setLayout(profLayout); + + LocationInformationWidget * diveSiteEdit = new LocationInformationWidget(); + connect(diveSiteEdit, &LocationInformationWidget::endEditDiveSite, + this, &MainWindow::setDefaultState); + + connect(diveSiteEdit, &LocationInformationWidget::endEditDiveSite, + mainTab, &MainTab::refreshDiveInfo); + + connect(diveSiteEdit, &LocationInformationWidget::endEditDiveSite, + mainTab, &MainTab::refreshDisplayedDiveSite); + + QWidget *diveSitePictures = new QWidget(); // Placeholder + + std::pair<QByteArray, QVariant> enabled = std::make_pair("enabled", QVariant(true)); + std::pair<QByteArray, QVariant> disabled = std::make_pair("enabled", QVariant(false)); + PropertyList enabledList; + PropertyList disabledList; + enabledList.push_back(enabled); + disabledList.push_back(disabled); + + registerApplicationState("Default", mainTab, profileContainer, diveListView, globeGps ); + registerApplicationState("AddDive", mainTab, profileContainer, diveListView, globeGps ); + registerApplicationState("EditDive", mainTab, profileContainer, diveListView, globeGps ); + registerApplicationState("PlanDive", plannerWidget, profileContainer, plannerSettings, plannerDetails ); + registerApplicationState("EditPlannedDive", plannerWidget, profileContainer, diveListView, globeGps ); + registerApplicationState("EditDiveSite", diveSiteEdit, profileContainer, diveListView, globeGps); + + setStateProperties("Default", enabledList, enabledList, enabledList,enabledList); + setStateProperties("AddDive", enabledList, enabledList, enabledList,enabledList); + setStateProperties("EditDive", enabledList, enabledList, enabledList,enabledList); + setStateProperties("PlanDive", enabledList, enabledList, enabledList,enabledList); + setStateProperties("EditPlannedDive", enabledList, enabledList, enabledList,enabledList); + setStateProperties("EditDiveSite", enabledList, disabledList, disabledList, enabledList); + + setApplicationState("Default"); + + ui.multiFilter->hide(); + + setWindowIcon(QIcon(":subsurface-icon")); + if (!QIcon::hasThemeIcon("window-close")) { + QIcon::setThemeName("subsurface"); + } + connect(dive_list(), SIGNAL(currentDiveChanged(int)), this, SLOT(current_dive_changed(int))); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(readSettings())); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), diveListView, SLOT(update())); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), diveListView, SLOT(reloadHeaderActions())); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), information(), SLOT(updateDiveInfo())); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), divePlannerWidget(), SLOT(settingsChanged())); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), divePlannerSettingsWidget(), SLOT(settingsChanged())); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), TankInfoModel::instance(), SLOT(update())); + connect(ui.actionRecent1, SIGNAL(triggered(bool)), this, SLOT(recentFileTriggered(bool))); + connect(ui.actionRecent2, SIGNAL(triggered(bool)), this, SLOT(recentFileTriggered(bool))); + connect(ui.actionRecent3, SIGNAL(triggered(bool)), this, SLOT(recentFileTriggered(bool))); + connect(ui.actionRecent4, SIGNAL(triggered(bool)), this, SLOT(recentFileTriggered(bool))); + connect(information(), SIGNAL(addDiveFinished()), graphics(), SLOT(setProfileState())); + connect(DivePlannerPointsModel::instance(), SIGNAL(planCreated()), this, SLOT(planCreated())); + connect(DivePlannerPointsModel::instance(), SIGNAL(planCanceled()), this, SLOT(planCanceled())); + connect(plannerDetails->printPlan(), SIGNAL(pressed()), divePlannerWidget(), SLOT(printDecoPlan())); + connect(this, SIGNAL(startDiveSiteEdit()), this, SLOT(on_actionDiveSiteEdit_triggered())); + +#ifndef NO_MARBLE + connect(information(), SIGNAL(diveSiteChanged(struct dive_site *)), globeGps, SLOT(centerOnDiveSite(struct dive_site *))); +#endif + wtu = new WindowTitleUpdate(); + connect(WindowTitleUpdate::instance(), SIGNAL(updateTitle()), this, SLOT(setAutomaticTitle())); +#ifdef NO_PRINTING + plannerDetails->printPlan()->hide(); + ui.menuFile->removeAction(ui.actionPrint); +#endif +#ifndef USE_LIBGIT23_API + ui.menuFile->removeAction(ui.actionCloudstorageopen); + ui.menuFile->removeAction(ui.actionCloudstoragesave); + qDebug() << "disabled / made invisible the cloud storage stuff"; +#else + enableDisableCloudActions(); +#endif + + ui.mainErrorMessage->hide(); + graphics()->setEmptyState(); + initialUiSetup(); + readSettings(); + diveListView->reload(DiveTripModel::TREE); + diveListView->reloadHeaderActions(); + diveListView->setFocus(); + GlobeGPS::instance()->reload(); + diveListView->expand(dive_list()->model()->index(0, 0)); + diveListView->scrollTo(dive_list()->model()->index(0, 0), QAbstractItemView::PositionAtCenter); + divePlannerWidget()->settingsChanged(); + divePlannerSettingsWidget()->settingsChanged(); +#ifdef NO_MARBLE + ui.menuView->removeAction(ui.actionViewGlobe); +#endif +#ifdef NO_USERMANUAL + ui.menuHelp->removeAction(ui.actionUserManual); +#endif + memset(©PasteDive, 0, sizeof(copyPasteDive)); + memset(&what, 0, sizeof(what)); + + updateManager = new UpdateManager(this); + undoStack = new QUndoStack(this); + QAction *undoAction = undoStack->createUndoAction(this, tr("&Undo")); + QAction *redoAction = undoStack->createRedoAction(this, tr("&Redo")); + undoAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Z)); + redoAction->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Z)); + QList<QAction*>undoRedoActions; + undoRedoActions.append(undoAction); + undoRedoActions.append(redoAction); + ui.menu_Edit->addActions(undoRedoActions); + + ReverseGeoLookupThread *geoLookup = ReverseGeoLookupThread::instance(); + connect(geoLookup, SIGNAL(started()),information(), SLOT(disableGeoLookupEdition())); + connect(geoLookup, SIGNAL(finished()), information(), SLOT(enableGeoLookupEdition())); +#ifndef NO_PRINTING + // copy the bundled print templates to the user path; no overwriting occurs! + copyPath(getPrintingTemplatePathBundle(), getPrintingTemplatePathUser()); + find_all_templates(); +#endif + +#if defined(FBSUPPORT) + FacebookManager *fb = FacebookManager::instance(); + connect(fb, SIGNAL(justLoggedIn(bool)), ui.actionFacebook, SLOT(setEnabled(bool))); + connect(fb, SIGNAL(justLoggedOut(bool)), ui.actionFacebook, SLOT(setEnabled(bool))); + connect(ui.actionFacebook, SIGNAL(triggered(bool)), fb, SLOT(sendDive())); + ui.actionFacebook->setEnabled(fb->loggedIn()); +#else + ui.actionFacebook->setEnabled(false); +#endif + + + ui.menubar->show(); + set_git_update_cb(&updateProgress); +} + +MainWindow::~MainWindow() +{ + write_hashes(); + m_Instance = NULL; +} + +void MainWindow::setStateProperties(const QByteArray& state, const PropertyList& tl, const PropertyList& tr, const PropertyList& bl, const PropertyList& br) +{ + stateProperties[state] = PropertiesForQuadrant(tl, tr, bl, br); +} + +void MainWindow::on_actionDiveSiteEdit_triggered() { + setApplicationState("EditDiveSite"); +} + +void MainWindow::enableDisableCloudActions() +{ +#ifdef USE_LIBGIT23_API + ui.actionCloudstorageopen->setEnabled(prefs.cloud_verification_status == CS_VERIFIED); + ui.actionCloudstoragesave->setEnabled(prefs.cloud_verification_status == CS_VERIFIED); +#endif +} + +PlannerDetails *MainWindow::plannerDetails() const { + return qobject_cast<PlannerDetails*>(applicationState["PlanDive"].bottomRight); +} + +PlannerSettingsWidget *MainWindow::divePlannerSettingsWidget() { + return qobject_cast<PlannerSettingsWidget*>(applicationState["PlanDive"].bottomLeft); +} + +void MainWindow::setDefaultState() { + setApplicationState("Default"); + if (information()->getEditMode() != MainTab::NONE) { + ui.bottomLeft->currentWidget()->setEnabled(false); + } +} + +void MainWindow::setLoadedWithFiles(bool f) +{ + filesAsArguments = f; +} + +bool MainWindow::filesFromCommandLine() const +{ + return filesAsArguments; +} + +MainWindow *MainWindow::instance() +{ + return m_Instance; +} + +// this gets called after we download dives from a divecomputer +void MainWindow::refreshDisplay(bool doRecreateDiveList) +{ + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + information()->reload(); + TankInfoModel::instance()->update(); + GlobeGPS::instance()->reload(); + if (doRecreateDiveList) + recreateDiveList(); + + setApplicationState("Default"); + dive_list()->setEnabled(true); + dive_list()->setFocus(); + WSInfoModel::instance()->updateInfo(); + if (amount_selected == 0) + cleanUpEmpty(); +} + +void MainWindow::recreateDiveList() +{ + dive_list()->reload(DiveTripModel::CURRENT); + TagFilterModel::instance()->repopulate(); + BuddyFilterModel::instance()->repopulate(); + LocationFilterModel::instance()->repopulate(); + SuitsFilterModel::instance()->repopulate(); +} + +void MainWindow::configureToolbar() { + if (selected_dive>0) { + if (current_dive->dc.divemode == FREEDIVE) { + ui.profCalcCeiling->setDisabled(true); + ui.profCalcAllTissues ->setDisabled(true); + ui.profIncrement3m->setDisabled(true); + ui.profDcCeiling->setDisabled(true); + ui.profPhe->setDisabled(true); + ui.profPn2->setDisabled(true); //TODO is the same as scuba? + ui.profPO2->setDisabled(true); //TODO is the same as scuba? + ui.profRuler->setDisabled(false); + ui.profScaled->setDisabled(false); // measuring and scaling + ui.profTogglePicture->setDisabled(false); + ui.profTankbar->setDisabled(true); + ui.profMod->setDisabled(true); + ui.profNdl_tts->setDisabled(true); + ui.profEad->setDisabled(true); + ui.profSAC->setDisabled(true); + ui.profHR->setDisabled(false); + ui.profTissues->setDisabled(true); + } else { + ui.profCalcCeiling->setDisabled(false); + ui.profCalcAllTissues ->setDisabled(false); + ui.profIncrement3m->setDisabled(false); + ui.profDcCeiling->setDisabled(false); + ui.profPhe->setDisabled(false); + ui.profPn2->setDisabled(false); + ui.profPO2->setDisabled(false); // partial pressure graphs + ui.profRuler->setDisabled(false); + ui.profScaled->setDisabled(false); // measuring and scaling + ui.profTogglePicture->setDisabled(false); + ui.profTankbar->setDisabled(false); + ui.profMod->setDisabled(false); + ui.profNdl_tts->setDisabled(false); // various values that a user is either interested in or not + ui.profEad->setDisabled(false); + ui.profSAC->setDisabled(false); + ui.profHR->setDisabled(false); // very few dive computers support this + ui.profTissues->setDisabled(false);; // maybe less frequently used + } + } +} + +void MainWindow::current_dive_changed(int divenr) +{ + if (divenr >= 0) { + select_dive(divenr); + } + graphics()->plotDive(); + information()->updateDiveInfo(); + configureToolbar(); + GlobeGPS::instance()->reload(); +} + +void MainWindow::on_actionNew_triggered() +{ + on_actionClose_triggered(); +} + +void MainWindow::on_actionOpen_triggered() +{ + if (!okToClose(tr("Please save or cancel the current dive edit before opening a new file."))) + return; + + // yes, this look wrong to use getSaveFileName() for the open dialog, but we need to be able + // to enter file names that don't exist in order to use our git syntax /path/to/dir[branch] + // with is a potentially valid input, but of course won't exist. So getOpenFileName() wouldn't work + QFileDialog dialog(this, tr("Open file"), lastUsedDir(), filter()); + dialog.setFileMode(QFileDialog::AnyFile); + dialog.setViewMode(QFileDialog::Detail); + dialog.setLabelText(QFileDialog::Accept, tr("Open")); + dialog.setLabelText(QFileDialog::Reject, tr("Cancel")); + dialog.setAcceptMode(QFileDialog::AcceptOpen); + QStringList filenames; + if (dialog.exec()) + filenames = dialog.selectedFiles(); + if (filenames.isEmpty()) + return; + updateLastUsedDir(QFileInfo(filenames.first()).dir().path()); + closeCurrentFile(); + // some file dialogs decide to add the default extension to a filename without extension + // so we would get dir[branch].ssrf when trying to select dir[branch]. + // let's detect that and remove the incorrect extension + QStringList cleanFilenames; + QRegularExpression reg(".*\\[[^]]+]\\.ssrf", QRegularExpression::CaseInsensitiveOption); + + Q_FOREACH (QString filename, filenames) { + if (reg.match(filename).hasMatch()) + filename.remove(QRegularExpression("\\.ssrf$", QRegularExpression::CaseInsensitiveOption)); + cleanFilenames << filename; + } + loadFiles(cleanFilenames); +} + +void MainWindow::on_actionSave_triggered() +{ + file_save(); +} + +void MainWindow::on_actionSaveAs_triggered() +{ + file_save_as(); +} + +void MainWindow::on_actionCloudstorageopen_triggered() +{ + if (!okToClose(tr("Please save or cancel the current dive edit before opening a new file."))) + return; + + QString filename; + if (getCloudURL(filename)) { + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + return; + } + qDebug() << filename; + + closeCurrentFile(); + + int error; + + showProgressBar(); + QByteArray fileNamePtr = QFile::encodeName(filename); + error = parse_file(fileNamePtr.data()); + if (!error) { + set_filename(fileNamePtr.data(), true); + setTitle(MWTF_FILENAME); + } + getNotificationWidget()->hideNotification(); + process_dives(false, false); + hideProgressBar(); + refreshDisplay(); + ui.actionAutoGroup->setChecked(autogroup); +} + +void MainWindow::on_actionCloudstoragesave_triggered() +{ + QString filename; + if (getCloudURL(filename)) { + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + return; + } + qDebug() << filename; + if (information()->isEditing()) + information()->acceptChanges(); + + showProgressBar(); + + if (save_dives(filename.toUtf8().data())) { + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + return; + } + + hideProgressBar(); + + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + set_filename(filename.toUtf8().data(), true); + setTitle(MWTF_FILENAME); + mark_divelist_changed(false); +} + +void learnImageDirs(QStringList dirnames) +{ + QList<QFuture<void> > futures; + foreach (QString dir, dirnames) { + futures << QtConcurrent::run(learnImages, QDir(dir), 10, false); + } + DivePictureModel::instance()->updateDivePicturesWhenDone(futures); +} + +void MainWindow::on_actionHash_images_triggered() +{ + QFuture<void> future; + QFileDialog dialog(this, tr("Traverse image directories"), lastUsedDir(), filter()); + dialog.setFileMode(QFileDialog::Directory); + dialog.setViewMode(QFileDialog::Detail); + dialog.setLabelText(QFileDialog::Accept, tr("Scan")); + dialog.setLabelText(QFileDialog::Reject, tr("Cancel")); + QStringList dirnames; + if (dialog.exec()) + dirnames = dialog.selectedFiles(); + if (dirnames.isEmpty()) + return; + future = QtConcurrent::run(learnImageDirs,dirnames); + MainWindow::instance()->getNotificationWidget()->showNotification(tr("Scanning images...(this can take a while)"), KMessageWidget::Information); + MainWindow::instance()->getNotificationWidget()->setFuture(future); + +} + +ProfileWidget2 *MainWindow::graphics() const +{ + return qobject_cast<ProfileWidget2*>(applicationState["Default"].topRight->layout()->itemAt(1)->widget()); +} + +void MainWindow::cleanUpEmpty() +{ + information()->clearStats(); + information()->clearInfo(); + information()->clearEquipment(); + information()->updateDiveInfo(true); + graphics()->setEmptyState(); + dive_list()->reload(DiveTripModel::TREE); + GlobeGPS::instance()->reload(); + if (!existing_filename) + setTitle(MWTF_DEFAULT); + disableShortcuts(); +} + +bool MainWindow::okToClose(QString message) +{ + if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING || + information()->isEditing() ) { + QMessageBox::warning(this, tr("Warning"), message); + return false; + } + if (unsaved_changes() && askSaveChanges() == false) + return false; + + return true; +} + +void MainWindow::closeCurrentFile() +{ + graphics()->setEmptyState(); + /* free the dives and trips */ + clear_git_id(); + clear_dive_file_data(); + cleanUpEmpty(); + mark_divelist_changed(false); + + clear_events(); + + dcList.dcMap.clear(); +} + +void MainWindow::on_actionClose_triggered() +{ + if (okToClose(tr("Please save or cancel the current dive edit before closing the file."))) { + closeCurrentFile(); + // hide any pictures and the filter + DivePictureModel::instance()->updateDivePictures(); + ui.multiFilter->closeFilter(); + recreateDiveList(); + } +} + +QString MainWindow::lastUsedDir() +{ + QSettings settings; + QString lastDir = QDir::homePath(); + + settings.beginGroup("FileDialog"); + if (settings.contains("LastDir")) + if (QDir::setCurrent(settings.value("LastDir").toString())) + lastDir = settings.value("LastDir").toString(); + return lastDir; +} + +void MainWindow::updateLastUsedDir(const QString &dir) +{ + QSettings s; + s.beginGroup("FileDialog"); + s.setValue("LastDir", dir); +} + +void MainWindow::on_actionPrint_triggered() +{ +#ifndef NO_PRINTING + PrintDialog dlg(this); + + dlg.exec(); +#endif +} + +void MainWindow::disableShortcuts(bool disablePaste) +{ + ui.actionPreviousDC->setShortcut(QKeySequence()); + ui.actionNextDC->setShortcut(QKeySequence()); + ui.copy->setShortcut(QKeySequence()); + if (disablePaste) + ui.paste->setShortcut(QKeySequence()); +} + +void MainWindow::enableShortcuts() +{ + ui.actionPreviousDC->setShortcut(Qt::Key_Left); + ui.actionNextDC->setShortcut(Qt::Key_Right); + ui.copy->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_C)); + ui.paste->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_V)); +} + +void MainWindow::showProfile() +{ + enableShortcuts(); + graphics()->setProfileState(); + setApplicationState("Default"); +} + +void MainWindow::on_actionPreferences_triggered() +{ + PreferencesDialog::instance()->show(); + PreferencesDialog::instance()->raise(); +} + +void MainWindow::on_actionQuit_triggered() +{ + if (information()->isEditing()) { + information()->rejectChanges(); + if (information()->isEditing()) + // didn't discard the edits + return; + } + if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING) { + DivePlannerPointsModel::instance()->cancelPlan(); + if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING) + // The planned dive was not discarded + return; + } + + if (unsaved_changes() && (askSaveChanges() == false)) + return; + writeSettings(); + QApplication::quit(); +} + +void MainWindow::on_actionDownloadDC_triggered() +{ + DownloadFromDCWidget dlg(this); + + dlg.exec(); +} + +void MainWindow::on_actionDownloadWeb_triggered() +{ + SubsurfaceWebServices dlg(this); + + dlg.exec(); +} + +void MainWindow::on_actionDivelogs_de_triggered() +{ + DivelogsDeWebServices::instance()->downloadDives(); +} + +void MainWindow::on_actionEditDeviceNames_triggered() +{ + DiveComputerManagementDialog::instance()->init(); + DiveComputerManagementDialog::instance()->update(); + DiveComputerManagementDialog::instance()->show(); +} + +bool MainWindow::plannerStateClean() +{ + if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING || + information()->isEditing()) { + QMessageBox::warning(this, tr("Warning"), tr("Please save or cancel the current dive edit before trying to add a dive.")); + return false; + } + return true; +} + +void MainWindow::refreshProfile() +{ + showProfile(); + configureToolbar(); + graphics()->replot(get_dive(selected_dive)); + DivePictureModel::instance()->updateDivePictures(); +} + +void MainWindow::planCanceled() +{ + // while planning we might have modified the displayed_dive + // let's refresh what's shown on the profile + refreshProfile(); + refreshDisplay(false); +} + +void MainWindow::planCreated() +{ + // get the new dive selected and assign a number if reasonable + graphics()->setProfileState(); + if (displayed_dive.id == 0) { + // we might have added a new dive (so displayed_dive was cleared out by clone_dive() + dive_list()->unselectDives(); + select_dive(dive_table.nr - 1); + dive_list()->selectDive(selected_dive); + set_dive_nr_for_current_dive(); + } + // make sure our UI is in a consistent state + information()->updateDiveInfo(); + showProfile(); + refreshDisplay(); +} + +void MainWindow::setPlanNotes() +{ + plannerDetails()->divePlanOutput()->setHtml(displayed_dive.notes); +} + +void MainWindow::printPlan() +{ +#ifndef NO_PRINTING + QString diveplan = plannerDetails()->divePlanOutput()->toHtml(); + QString withDisclaimer = QString("<img height=50 src=\":subsurface-icon\"> ") + diveplan + QString(disclaimer); + + QPrinter printer; + QPrintDialog *dialog = new QPrintDialog(&printer, this); + dialog->setWindowTitle(tr("Print runtime table")); + if (dialog->exec() != QDialog::Accepted) + return; + + plannerDetails()->divePlanOutput()->setHtml(withDisclaimer); + plannerDetails()->divePlanOutput()->print(&printer); + plannerDetails()->divePlanOutput()->setHtml(diveplan); +#endif +} + +void MainWindow::setupForAddAndPlan(const char *model) +{ + // clean out the dive and give it an id and the correct dc model + clear_dive(&displayed_dive); + clear_dive_site(&displayed_dive_site); + displayed_dive.id = dive_getUniqID(&displayed_dive); + displayed_dive.when = QDateTime::currentMSecsSinceEpoch() / 1000L + gettimezoneoffset() + 3600; + displayed_dive.dc.model = model; // don't translate! this is stored in the XML file + // setup the dive cylinders + DivePlannerPointsModel::instance()->clear(); + DivePlannerPointsModel::instance()->setupCylinders(); +} + +void MainWindow::on_actionReplanDive_triggered() +{ + if (!plannerStateClean() || !current_dive || !current_dive->dc.model) + return; + else if (strcmp(current_dive->dc.model, "planned dive")) { + if (QMessageBox::warning(this, tr("Warning"), tr("Trying to replan a dive that's not a planned dive."), + QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Cancel) + return; + } + // put us in PLAN mode + DivePlannerPointsModel::instance()->clear(); + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::PLAN); + + graphics()->setPlanState(); + graphics()->clearHandlers(); + setApplicationState("PlanDive"); + divePlannerWidget()->setReplanButton(true); + DivePlannerPointsModel::instance()->loadFromDive(current_dive); + reset_cylinders(&displayed_dive, true); +} + +void MainWindow::on_actionDivePlanner_triggered() +{ + if (!plannerStateClean()) + return; + + // put us in PLAN mode + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::PLAN); + setApplicationState("PlanDive"); + + graphics()->setPlanState(); + + // create a simple starting dive, using the first gas from the just copied cylinders + setupForAddAndPlan("planned dive"); // don't translate, stored in XML file + DivePlannerPointsModel::instance()->setupStartTime(); + DivePlannerPointsModel::instance()->createSimpleDive(); + DivePictureModel::instance()->updateDivePictures(); + divePlannerWidget()->setReplanButton(false); +} + +DivePlannerWidget* MainWindow::divePlannerWidget() { + return qobject_cast<DivePlannerWidget*>(applicationState["PlanDive"].topLeft); +} + +void MainWindow::on_actionAddDive_triggered() +{ + if (!plannerStateClean()) + return; + + if (dive_list()->selectedTrips().count() >= 1) { + dive_list()->rememberSelection(); + dive_list()->clearSelection(); + } + + setApplicationState("AddDive"); + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::ADD); + + // setup things so we can later create our starting dive + setupForAddAndPlan("manually added dive"); // don't translate, stored in the XML file + + // now show the mostly empty main tab + information()->updateDiveInfo(); + + // show main tab + information()->setCurrentIndex(0); + + information()->addDiveStarted(); + + graphics()->setAddState(); + DivePlannerPointsModel::instance()->createSimpleDive(); + configureToolbar(); + graphics()->plotDive(); +} + +void MainWindow::on_actionEditDive_triggered() +{ + if (information()->isEditing() || DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING) { + QMessageBox::warning(this, tr("Warning"), tr("Please, first finish the current edition before trying to do another.")); + return; + } + + const bool isTripEdit = dive_list()->selectedTrips().count() >= 1; + if (!current_dive || isTripEdit || (current_dive->dc.model && strcmp(current_dive->dc.model, "manually added dive"))) { + QMessageBox::warning(this, tr("Warning"), tr("Trying to edit a dive that's not a manually added dive.")); + return; + } + + DivePlannerPointsModel::instance()->clear(); + disableShortcuts(); + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::ADD); + graphics()->setAddState(); + GlobeGPS::instance()->endGetDiveCoordinates(); + setApplicationState("EditDive"); + DivePlannerPointsModel::instance()->loadFromDive(current_dive); + information()->enableEdition(MainTab::MANUALLY_ADDED_DIVE); +} + +void MainWindow::on_actionRenumber_triggered() +{ + RenumberDialog::instance()->renumberOnlySelected(false); + RenumberDialog::instance()->show(); +} + +void MainWindow::on_actionAutoGroup_triggered() +{ + autogroup = ui.actionAutoGroup->isChecked(); + if (autogroup) + autogroup_dives(); + else + remove_autogen_trips(); + refreshDisplay(); + mark_divelist_changed(true); +} + +void MainWindow::on_actionYearlyStatistics_triggered() +{ + QDialog d; + QVBoxLayout *l = new QVBoxLayout(&d); + YearlyStatisticsModel *m = new YearlyStatisticsModel(); + QTreeView *view = new QTreeView(); + view->setModel(m); + l->addWidget(view); + d.resize(width() * .8, height() / 2); + d.move(width() * .1, height() / 4); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), &d); + connect(close, SIGNAL(activated()), &d, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), &d); + connect(quit, SIGNAL(activated()), this, SLOT(close())); + d.setWindowFlags(Qt::Window | Qt::CustomizeWindowHint + | Qt::WindowCloseButtonHint | Qt::WindowTitleHint); + d.setWindowTitle(tr("Yearly statistics")); + d.setWindowIcon(QIcon(":/subsurface-icon")); + d.exec(); +} + +#define BEHAVIOR QList<int>() + +#define TOGGLE_COLLAPSABLE( X ) \ + ui.mainSplitter->setCollapsible(0, X); \ + ui.mainSplitter->setCollapsible(1, X); \ + ui.topSplitter->setCollapsible(0, X); \ + ui.topSplitter->setCollapsible(1, X); \ + ui.bottomSplitter->setCollapsible(0, X); \ + ui.bottomSplitter->setCollapsible(1, X); + +void MainWindow::on_actionViewList_triggered() +{ + TOGGLE_COLLAPSABLE( true ); + beginChangeState(LIST_MAXIMIZED); + ui.topSplitter->setSizes(BEHAVIOR << EXPANDED << COLLAPSED); + ui.mainSplitter->setSizes(BEHAVIOR << COLLAPSED << EXPANDED); +} + +void MainWindow::on_actionViewProfile_triggered() +{ + TOGGLE_COLLAPSABLE( true ); + beginChangeState(PROFILE_MAXIMIZED); + ui.topSplitter->setSizes(BEHAVIOR << COLLAPSED << EXPANDED); + ui.mainSplitter->setSizes(BEHAVIOR << EXPANDED << COLLAPSED); +} + +void MainWindow::on_actionViewInfo_triggered() +{ + TOGGLE_COLLAPSABLE( true ); + beginChangeState(INFO_MAXIMIZED); + ui.topSplitter->setSizes(BEHAVIOR << EXPANDED << COLLAPSED); + ui.mainSplitter->setSizes(BEHAVIOR << EXPANDED << COLLAPSED); +} + +void MainWindow::on_actionViewGlobe_triggered() +{ + TOGGLE_COLLAPSABLE( true ); + beginChangeState(GLOBE_MAXIMIZED); + ui.mainSplitter->setSizes(BEHAVIOR << COLLAPSED << EXPANDED); + ui.bottomSplitter->setSizes(BEHAVIOR << COLLAPSED << EXPANDED); +} +#undef BEHAVIOR + +void MainWindow::on_actionViewAll_triggered() +{ + TOGGLE_COLLAPSABLE( false ); + beginChangeState(VIEWALL); + static QList<int> mainSizes; + const int appH = qApp->desktop()->size().height(); + const int appW = qApp->desktop()->size().width(); + if (mainSizes.empty()) { + mainSizes.append(appH * 0.7); + mainSizes.append(appH * 0.3); + } + static QList<int> infoProfileSizes; + if (infoProfileSizes.empty()) { + infoProfileSizes.append(appW * 0.3); + infoProfileSizes.append(appW * 0.7); + } + + static QList<int> listGlobeSizes; + if (listGlobeSizes.empty()) { + listGlobeSizes.append(appW * 0.7); + listGlobeSizes.append(appW * 0.3); + } + + QSettings settings; + settings.beginGroup("MainWindow"); + if (settings.value("mainSplitter").isValid()) { + ui.mainSplitter->restoreState(settings.value("mainSplitter").toByteArray()); + ui.topSplitter->restoreState(settings.value("topSplitter").toByteArray()); + ui.bottomSplitter->restoreState(settings.value("bottomSplitter").toByteArray()); + if (ui.mainSplitter->sizes().first() == 0 || ui.mainSplitter->sizes().last() == 0) + ui.mainSplitter->setSizes(mainSizes); + if (ui.topSplitter->sizes().first() == 0 || ui.topSplitter->sizes().last() == 0) + ui.topSplitter->setSizes(infoProfileSizes); + if (ui.bottomSplitter->sizes().first() == 0 || ui.bottomSplitter->sizes().last() == 0) + ui.bottomSplitter->setSizes(listGlobeSizes); + + } else { + ui.mainSplitter->setSizes(mainSizes); + ui.topSplitter->setSizes(infoProfileSizes); + ui.bottomSplitter->setSizes(listGlobeSizes); + } + ui.mainSplitter->setCollapsible(0, false); + ui.mainSplitter->setCollapsible(1, false); + ui.topSplitter->setCollapsible(0, false); + ui.topSplitter->setCollapsible(1, false); + ui.bottomSplitter->setCollapsible(0,false); + ui.bottomSplitter->setCollapsible(1,false); +} + +#undef TOGGLE_COLLAPSABLE + +void MainWindow::beginChangeState(CurrentState s) +{ + if (state == VIEWALL && state != s) { + saveSplitterSizes(); + } + state = s; +} + +void MainWindow::saveSplitterSizes() +{ + QSettings settings; + settings.beginGroup("MainWindow"); + settings.setValue("mainSplitter", ui.mainSplitter->saveState()); + settings.setValue("topSplitter", ui.topSplitter->saveState()); + settings.setValue("bottomSplitter", ui.bottomSplitter->saveState()); +} + +void MainWindow::on_actionPreviousDC_triggered() +{ + unsigned nrdc = number_of_computers(current_dive); + dc_number = (dc_number + nrdc - 1) % nrdc; + configureToolbar(); + graphics()->plotDive(); + information()->updateDiveInfo(); +} + +void MainWindow::on_actionNextDC_triggered() +{ + unsigned nrdc = number_of_computers(current_dive); + dc_number = (dc_number + 1) % nrdc; + configureToolbar(); + graphics()->plotDive(); + information()->updateDiveInfo(); +} + +void MainWindow::on_actionFullScreen_triggered(bool checked) +{ + if (checked) { + setWindowState(windowState() | Qt::WindowFullScreen); + } else { + setWindowState(windowState() & ~Qt::WindowFullScreen); + } +} + +void MainWindow::on_actionAboutSubsurface_triggered() +{ + SubsurfaceAbout dlg(this); + + dlg.exec(); +} + +void MainWindow::on_action_Check_for_Updates_triggered() +{ + if (!updateManager) + updateManager = new UpdateManager(this); + + updateManager->checkForUpdates(); +} + +void MainWindow::on_actionUserManual_triggered() +{ +#ifndef NO_USERMANUAL + if (!helpView) { + helpView = new UserManual(); + } + helpView->show(); +#endif +} + +void MainWindow::on_actionUserSurvey_triggered() +{ + if(!survey) { + survey = new UserSurvey(this); + } + survey->show(); +} + +QString MainWindow::filter() +{ + QString f; + f += "Dive log files ( *.ssrf "; + f += "*.can *.CAN "; + f += "*.db *.DB " ; + f += "*.sql *.SQL " ; + f += "*.dld *.DLD "; + f += "*.jlb *.JLB "; + f += "*.lvd *.LVD "; + f += "*.sde *.SDE "; + f += "*.udcf *.UDCF "; + f += "*.uddf *.UDDF "; + f += "*.xml *.XML "; + f += "*.dlf *.DLF "; + f += ");;"; + + f += "Subsurface (*.ssrf);;"; + f += "Cochran (*.can *.CAN);;"; + f += "DiveLogs.de (*.dld *.DLD);;"; + f += "JDiveLog (*.jlb *.JLB);;"; + f += "Liquivision (*.lvd *.LVD);;"; + f += "Suunto (*.sde *.SDE *.db *.DB);;"; + f += "UDCF (*.udcf *.UDCF);;"; + f += "UDDF (*.uddf *.UDDF);;"; + f += "XML (*.xml *.XML)"; + f += "Divesoft (*.dlf *.DLF)"; + f += "Datatrak/WLog Files (*.log *.LOG)"; + + return f; +} + +bool MainWindow::askSaveChanges() +{ + QString message; + QMessageBox response(this); + + if (existing_filename) + message = tr("Do you want to save the changes that you made in the file %1?") + .arg(displayedFilename(existing_filename)); + else + message = tr("Do you want to save the changes that you made in the data file?"); + + response.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); + response.setDefaultButton(QMessageBox::Save); + response.setText(message); + response.setWindowTitle(tr("Save changes?")); // Not displayed on MacOSX as described in Qt API + response.setInformativeText(tr("Changes will be lost if you don't save them.")); + response.setIcon(QMessageBox::Warning); + response.setWindowModality(Qt::WindowModal); + int ret = response.exec(); + + switch (ret) { + case QMessageBox::Save: + file_save(); + return true; + case QMessageBox::Discard: + return true; + } + return false; +} + +void MainWindow::initialUiSetup() +{ + QSettings settings; + settings.beginGroup("MainWindow"); + if (settings.value("maximized", isMaximized()).value<bool>()) { + showMaximized(); + } else { + restoreGeometry(settings.value("geometry").toByteArray()); + restoreState(settings.value("windowState", 0).toByteArray()); + } + + state = (CurrentState)settings.value("lastState", 0).toInt(); + switch (state) { + case VIEWALL: + on_actionViewAll_triggered(); + break; + case GLOBE_MAXIMIZED: + on_actionViewGlobe_triggered(); + break; + case INFO_MAXIMIZED: + on_actionViewInfo_triggered(); + break; + case LIST_MAXIMIZED: + on_actionViewList_triggered(); + break; + case PROFILE_MAXIMIZED: + on_actionViewProfile_triggered(); + break; + } + settings.endGroup(); + show(); +} + +const char *getSetting(QSettings &s, QString name) +{ + QVariant v; + v = s.value(name); + if (v.isValid()) { + return strdup(v.toString().toUtf8().data()); + } + return NULL; +} + +#define TOOLBOX_PREF_BUTTON(pref, setting, button) \ + prefs.pref = s.value(#setting).toBool(); \ + ui.button->setChecked(prefs.pref); + +void MainWindow::readSettings() +{ + static bool firstRun = true; + QSettings s; + // the static object for preferences already reads in the settings + // and sets up the font, so just get what we need for the toolbox and other widgets here + + s.beginGroup("TecDetails"); + TOOLBOX_PREF_BUTTON(calcalltissues, calcalltissues, profCalcAllTissues); + TOOLBOX_PREF_BUTTON(calcceiling, calcceiling, profCalcCeiling); + TOOLBOX_PREF_BUTTON(dcceiling, dcceiling, profDcCeiling); + TOOLBOX_PREF_BUTTON(ead, ead, profEad); + TOOLBOX_PREF_BUTTON(calcceiling3m, calcceiling3m, profIncrement3m); + TOOLBOX_PREF_BUTTON(mod, mod, profMod); + TOOLBOX_PREF_BUTTON(calcndltts, calcndltts, profNdl_tts); + TOOLBOX_PREF_BUTTON(pp_graphs.phe, phegraph, profPhe); + TOOLBOX_PREF_BUTTON(pp_graphs.pn2, pn2graph, profPn2); + TOOLBOX_PREF_BUTTON(pp_graphs.po2, po2graph, profPO2); + TOOLBOX_PREF_BUTTON(hrgraph, hrgraph, profHR); + TOOLBOX_PREF_BUTTON(rulergraph, rulergraph, profRuler); + TOOLBOX_PREF_BUTTON(show_sac, show_sac, profSAC); + TOOLBOX_PREF_BUTTON(show_pictures_in_profile, show_pictures_in_profile, profTogglePicture); + TOOLBOX_PREF_BUTTON(tankbar, tankbar, profTankbar); + TOOLBOX_PREF_BUTTON(percentagegraph, percentagegraph, profTissues); + TOOLBOX_PREF_BUTTON(zoomed_plot, zoomed_plot, profScaled); + s.endGroup(); // note: why doesn't the list of 17 buttons match the order in the gui? + s.beginGroup("DiveComputer"); + default_dive_computer_vendor = getSetting(s, "dive_computer_vendor"); + default_dive_computer_product = getSetting(s, "dive_computer_product"); + default_dive_computer_device = getSetting(s, "dive_computer_device"); + default_dive_computer_download_mode = s.value("dive_computer_download_mode").toInt(); + s.endGroup(); + QNetworkProxy proxy; + proxy.setType(QNetworkProxy::ProxyType(prefs.proxy_type)); + proxy.setHostName(prefs.proxy_host); + proxy.setPort(prefs.proxy_port); + if (prefs.proxy_auth) { + proxy.setUser(prefs.proxy_user); + proxy.setPassword(prefs.proxy_pass); + } + QNetworkProxy::setApplicationProxy(proxy); + +#if !defined(SUBSURFACE_MOBILE) + loadRecentFiles(&s); + if (firstRun) { + checkSurvey(&s); + firstRun = false; + } +#endif +} + +#undef TOOLBOX_PREF_BUTTON + +void MainWindow::checkSurvey(QSettings *s) +{ + s->beginGroup("UserSurvey"); + if (!s->contains("FirstUse42")) { + QVariant value = QDate().currentDate(); + s->setValue("FirstUse42", value); + } + // wait a week for production versions, but not at all for non-tagged builds + QString ver(subsurface_version()); + int waitTime = 7; + QDate firstUse42 = s->value("FirstUse42").toDate(); + if (run_survey || (firstUse42.daysTo(QDate().currentDate()) > waitTime && !s->contains("SurveyDone"))) { + if (!survey) + survey = new UserSurvey(this); + survey->show(); + } + s->endGroup(); +} + +void MainWindow::writeSettings() +{ + QSettings settings; + + settings.beginGroup("MainWindow"); + settings.setValue("geometry", saveGeometry()); + settings.setValue("windowState", saveState()); + settings.setValue("maximized", isMaximized()); + settings.setValue("lastState", (int)state); + if (state == VIEWALL) + saveSplitterSizes(); + settings.endGroup(); +} + +void MainWindow::closeEvent(QCloseEvent *event) +{ + if (DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING || + information()->isEditing()) { + on_actionQuit_triggered(); + event->ignore(); + return; + } + +#ifndef NO_USERMANUAL + if (helpView && helpView->isVisible()) { + helpView->close(); + helpView->deleteLater(); + } +#endif + + if (survey && survey->isVisible()) { + survey->close(); + survey->deleteLater(); + } + + if (unsaved_changes() && (askSaveChanges() == false)) { + event->ignore(); + return; + } + event->accept(); + writeSettings(); + QApplication::closeAllWindows(); +} + +DiveListView *MainWindow::dive_list() +{ + return qobject_cast<DiveListView*>(applicationState["Default"].bottomLeft); +} + +MainTab *MainWindow::information() +{ + return qobject_cast<MainTab*>(applicationState["Default"].topLeft); +} + +void MainWindow::loadRecentFiles(QSettings *s) +{ + QStringList files; + bool modified = false; + + s->beginGroup("Recent_Files"); + for (int c = 1; c <= 4; c++) { + QString key = QString("File_%1").arg(c); + if (s->contains(key)) { + QString file = s->value(key).toString(); + + if (QFile::exists(file)) { + files.append(file); + } else { + modified = true; + } + } else { + break; + } + } + + if (modified) { + for (int c = 0; c < 4; c++) { + QString key = QString("File_%1").arg(c + 1); + + if (files.count() > c) { + s->setValue(key, files.at(c)); + } else { + if (s->contains(key)) { + s->remove(key); + } + } + } + + s->sync(); + } + s->endGroup(); + + for (int c = 0; c < 4; c++) { + QAction *action = this->findChild<QAction *>(QString("actionRecent%1").arg(c + 1)); + + if (files.count() > c) { + QFileInfo fi(files.at(c)); + action->setText(fi.fileName()); + action->setToolTip(fi.absoluteFilePath()); + action->setVisible(true); + } else { + action->setVisible(false); + } + } +} + +void MainWindow::addRecentFile(const QStringList &newFiles) +{ + QStringList files; + QSettings s; + + if (newFiles.isEmpty()) + return; + + s.beginGroup("Recent_Files"); + + for (int c = 1; c <= 4; c++) { + QString key = QString("File_%1").arg(c); + if (s.contains(key)) { + QString file = s.value(key).toString(); + + files.append(file); + } else { + break; + } + } + + foreach (const QString &file, newFiles) { + int index = files.indexOf(QDir::toNativeSeparators(file)); + + if (index >= 0) { + files.removeAt(index); + } + } + + foreach (const QString &file, newFiles) { + if (QFile::exists(file)) { + files.prepend(QDir::toNativeSeparators(file)); + } + } + + while (files.count() > 4) { + files.removeLast(); + } + + for (int c = 1; c <= 4; c++) { + QString key = QString("File_%1").arg(c); + + if (files.count() >= c) { + s.setValue(key, files.at(c - 1)); + } else { + if (s.contains(key)) { + s.remove(key); + } + } + } + s.endGroup(); + s.sync(); + + loadRecentFiles(&s); +} + +void MainWindow::removeRecentFile(QStringList failedFiles) +{ + QStringList files; + QSettings s; + + if (failedFiles.isEmpty()) + return; + + s.beginGroup("Recent_Files"); + + for (int c = 1; c <= 4; c++) { + QString key = QString("File_%1").arg(c); + + if (s.contains(key)) { + QString file = s.value(key).toString(); + files.append(file); + } else { + break; + } + } + + foreach (const QString &file, failedFiles) + files.removeAll(file); + + for (int c = 1; c <= 4; c++) { + QString key = QString("File_%1").arg(c); + + if (files.count() >= c) + s.setValue(key, files.at(c - 1)); + else if (s.contains(key)) + s.remove(key); + } + + s.endGroup(); + s.sync(); + + loadRecentFiles(&s); +} + +void MainWindow::recentFileTriggered(bool checked) +{ + Q_UNUSED(checked); + + if (!okToClose(tr("Please save or cancel the current dive edit before opening a new file."))) + return; + + QAction *actionRecent = (QAction *)sender(); + + const QString &filename = actionRecent->toolTip(); + + updateLastUsedDir(QFileInfo(filename).dir().path()); + closeCurrentFile(); + loadFiles(QStringList() << filename); +} + +int MainWindow::file_save_as(void) +{ + QString filename; + const char *default_filename = existing_filename; + + // if the default is to save to cloud storage, pick something that will work as local file: + // simply extract the branch name which should be the users email address + if (default_filename && strstr(default_filename, prefs.cloud_git_url)) { + QString filename(default_filename); + filename.remove(prefs.cloud_git_url); + filename.remove(0, filename.indexOf("[") + 1); + filename.replace("]", ".ssrf"); + default_filename = strdup(qPrintable(filename)); + } + // create a file dialog that allows us to save to a new file + QFileDialog selection_dialog(this, tr("Save file as"), default_filename, + tr("Subsurface XML files (*.ssrf *.xml *.XML)")); + selection_dialog.setAcceptMode(QFileDialog::AcceptSave); + selection_dialog.setFileMode(QFileDialog::AnyFile); + selection_dialog.setDefaultSuffix(""); + if (same_string(default_filename, "")) { + QFileInfo defaultFile(system_default_filename()); + selection_dialog.setDirectory(qPrintable(defaultFile.absolutePath())); + } + /* if the exit/cancel button is pressed return */ + if (!selection_dialog.exec()) + return 0; + + /* get the first selected file */ + filename = selection_dialog.selectedFiles().at(0); + + /* now for reasons I don't understand we appear to add a .ssrf to + * git style filenames <path>/directory[branch] + * so let's remove that */ + QRegularExpression reg(".*\\[[^]]+]\\.ssrf", QRegularExpression::CaseInsensitiveOption); + if (reg.match(filename).hasMatch()) + filename.remove(QRegularExpression("\\.ssrf$", QRegularExpression::CaseInsensitiveOption)); + if (filename.isNull() || filename.isEmpty()) + return report_error("No filename to save into"); + + if (information()->isEditing()) + information()->acceptChanges(); + + if (save_dives(filename.toUtf8().data())) { + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + return -1; + } + + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + set_filename(filename.toUtf8().data(), true); + setTitle(MWTF_FILENAME); + mark_divelist_changed(false); + addRecentFile(QStringList() << filename); + return 0; +} + +int MainWindow::file_save(void) +{ + const char *current_default; + bool is_cloud = false; + + if (!existing_filename) + return file_save_as(); + + is_cloud = (strncmp(existing_filename, "http", 4) == 0); + + if (information()->isEditing()) + information()->acceptChanges(); + + current_default = prefs.default_filename; + if (strcmp(existing_filename, current_default) == 0) { + /* if we are using the default filename the directory + * that we are creating the file in may not exist */ + QDir current_def_dir = QFileInfo(current_default).absoluteDir(); + if (!current_def_dir.exists()) + current_def_dir.mkpath(current_def_dir.absolutePath()); + } + if (is_cloud) + showProgressBar(); + if (save_dives(existing_filename)) { + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + if (is_cloud) + hideProgressBar(); + return -1; + } + if (is_cloud) + hideProgressBar(); + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + mark_divelist_changed(false); + addRecentFile(QStringList() << QString(existing_filename)); + return 0; +} + +NotificationWidget *MainWindow::getNotificationWidget() +{ + return ui.mainErrorMessage; +} + +void MainWindow::showError() +{ + getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); +} + +QString MainWindow::displayedFilename(QString fullFilename) +{ + QFile f(fullFilename); + QFileInfo fileInfo(f); + QString fileName(fileInfo.fileName()); + + if (fullFilename.contains(prefs.cloud_git_url)) + return tr("[cloud storage for] %1").arg(fileName.left(fileName.indexOf('['))); + else + return fileName; +} + +void MainWindow::setAutomaticTitle() +{ + setTitle(); +} + +void MainWindow::setTitle(enum MainWindowTitleFormat format) +{ + switch (format) { + case MWTF_DEFAULT: + setWindowTitle("Subsurface"); + break; + case MWTF_FILENAME: + if (!existing_filename) { + setTitle(MWTF_DEFAULT); + return; + } + QString unsaved = (unsaved_changes() ? " *" : ""); + setWindowTitle("Subsurface: " + displayedFilename(existing_filename) + unsaved); + break; + } +} + +void MainWindow::importFiles(const QStringList fileNames) +{ + if (fileNames.isEmpty()) + return; + + QByteArray fileNamePtr; + + for (int i = 0; i < fileNames.size(); ++i) { + fileNamePtr = QFile::encodeName(fileNames.at(i)); + parse_file(fileNamePtr.data()); + } + process_dives(true, false); + refreshDisplay(); +} + +void MainWindow::importTxtFiles(const QStringList fileNames) +{ + if (fileNames.isEmpty()) + return; + + QByteArray fileNamePtr, csv; + + for (int i = 0; i < fileNames.size(); ++i) { + fileNamePtr = QFile::encodeName(fileNames.at(i)); + csv = fileNamePtr.data(); + csv.replace(strlen(csv.data()) - 3, 3, "csv"); + parse_txt_file(fileNamePtr.data(), csv); + } + process_dives(true, false); + refreshDisplay(); +} + +void MainWindow::loadFiles(const QStringList fileNames) +{ + bool showWarning = false; + if (fileNames.isEmpty()) { + refreshDisplay(); + return; + } + QByteArray fileNamePtr; + QStringList failedParses; + + showProgressBar(); + for (int i = 0; i < fileNames.size(); ++i) { + int error; + + fileNamePtr = QFile::encodeName(fileNames.at(i)); + error = parse_file(fileNamePtr.data()); + if (!error) { + set_filename(fileNamePtr.data(), true); + setTitle(MWTF_FILENAME); + // if there were any messages, show them + QString warning = get_error_string(); + if (!warning.isEmpty()) { + showWarning = true; + getNotificationWidget()->showNotification(warning , KMessageWidget::Information); + } + } else { + failedParses.append(fileNames.at(i)); + } + } + hideProgressBar(); + if (!showWarning) + getNotificationWidget()->hideNotification(); + process_dives(false, false); + addRecentFile(fileNames); + removeRecentFile(failedParses); + + refreshDisplay(); + ui.actionAutoGroup->setChecked(autogroup); + + int min_datafile_version = get_min_datafile_version(); + if (min_datafile_version >0 && min_datafile_version < DATAFORMAT_VERSION) { + QMessageBox::warning(this, tr("Opening datafile from older version"), + tr("You opened a data file from an older version of Subsurface. We recommend " + "you read the manual to learn about the changes in the new version, especially " + "about dive site management which has changed significantly.\n" + "Subsurface has already tried to pre-populate the data but it might be worth " + "while taking a look at the new dive site management system and to make " + "sure that everything looks correct.")); + } +} + +void MainWindow::on_actionImportDiveLog_triggered() +{ + QStringList fileNames = QFileDialog::getOpenFileNames(this, tr("Open dive log file"), lastUsedDir(), + tr("Dive log files (*.ssrf *.can *.csv *.db *.sql *.dld *.jlb *.lvd *.sde *.udcf *.uddf *.xml *.txt *.dlf *.apd" + "*.SSRF *.CAN *.CSV *.DB *.SQL *.DLD *.JLB *.LVD *.SDE *.UDCF *.UDDF *.xml *.TXT *.DLF *.APD);;" + "Cochran files (*.can *.CAN);;" + "CSV files (*.csv *.CSV);;" + "DiveLog.de files (*.dld *.DLD);;" + "JDiveLog files (*.jlb *.JLB);;" + "Liquivision files (*.lvd *.LVD);;" + "MkVI files (*.txt *.TXT);;" + "Suunto files (*.sde *.db *.SDE *.DB);;" + "Divesoft files (*.dlf *.DLF);;" + "UDDF/UDCF files (*.uddf *.udcf *.UDDF *.UDCF);;" + "XML files (*.xml *.XML);;" + "APD log viewer (*.apd *.APD);;" + "Datatrak/WLog Files (*.log *.LOG);;" + "OSTCtools Files (*.dive *.DIVE);;" + "All files (*)")); + + if (fileNames.isEmpty()) + return; + updateLastUsedDir(QFileInfo(fileNames[0]).dir().path()); + + QStringList logFiles = fileNames.filter(QRegExp("^.*\\.(?!(csv|txt|apd))", Qt::CaseInsensitive)); + QStringList csvFiles = fileNames.filter(".csv", Qt::CaseInsensitive); + csvFiles += fileNames.filter(".apd", Qt::CaseInsensitive); + QStringList txtFiles = fileNames.filter(".txt", Qt::CaseInsensitive); + + if (logFiles.size()) { + importFiles(logFiles); + } + + if (csvFiles.size()) { + DiveLogImportDialog *diveLogImport = new DiveLogImportDialog(csvFiles, this); + diveLogImport->show(); + process_dives(true, false); + refreshDisplay(); + } + + if (txtFiles.size()) { + importTxtFiles(txtFiles); + } +} + +void MainWindow::editCurrentDive() +{ + if (information()->isEditing() || DivePlannerPointsModel::instance()->currentMode() != DivePlannerPointsModel::NOTHING) { + QMessageBox::warning(this, tr("Warning"), tr("Please, first finish the current edition before trying to do another.")); + return; + } + + struct dive *d = current_dive; + QString defaultDC(d->dc.model); + DivePlannerPointsModel::instance()->clear(); + if (defaultDC == "manually added dive") { + disableShortcuts(); + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::ADD); + graphics()->setAddState(); + setApplicationState("EditDive"); + DivePlannerPointsModel::instance()->loadFromDive(d); + information()->enableEdition(MainTab::MANUALLY_ADDED_DIVE); + } else if (defaultDC == "planned dive") { + disableShortcuts(); + DivePlannerPointsModel::instance()->setPlanMode(DivePlannerPointsModel::PLAN); + setApplicationState("EditPlannedDive"); + DivePlannerPointsModel::instance()->loadFromDive(d); + information()->enableEdition(MainTab::MANUALLY_ADDED_DIVE); + } +} + +#define PREF_PROFILE(QT_PREFS) \ + QSettings s; \ + s.beginGroup("TecDetails"); \ + s.setValue(#QT_PREFS, triggered); \ + PreferencesDialog::instance()->emitSettingsChanged(); + +#define TOOLBOX_PREF_PROFILE(METHOD, INTERNAL_PREFS, QT_PREFS) \ + void MainWindow::on_##METHOD##_triggered(bool triggered) \ + { \ + prefs.INTERNAL_PREFS = triggered; \ + PREF_PROFILE(QT_PREFS); \ + } + +// note: why doesn't the list of 17 buttons match the order in the gui? or the order above? (line 1136) +TOOLBOX_PREF_PROFILE(profCalcAllTissues, calcalltissues, calcalltissues); +TOOLBOX_PREF_PROFILE(profCalcCeiling, calcceiling, calcceiling); +TOOLBOX_PREF_PROFILE(profDcCeiling, dcceiling, dcceiling); +TOOLBOX_PREF_PROFILE(profEad, ead, ead); +TOOLBOX_PREF_PROFILE(profIncrement3m, calcceiling3m, calcceiling3m); +TOOLBOX_PREF_PROFILE(profMod, mod, mod); +TOOLBOX_PREF_PROFILE(profNdl_tts, calcndltts, calcndltts); +TOOLBOX_PREF_PROFILE(profPhe, pp_graphs.phe, phegraph); +TOOLBOX_PREF_PROFILE(profPn2, pp_graphs.pn2, pn2graph); +TOOLBOX_PREF_PROFILE(profPO2, pp_graphs.po2, po2graph); +TOOLBOX_PREF_PROFILE(profHR, hrgraph, hrgraph); +TOOLBOX_PREF_PROFILE(profRuler, rulergraph, rulergraph); +TOOLBOX_PREF_PROFILE(profSAC, show_sac, show_sac); +TOOLBOX_PREF_PROFILE(profScaled, zoomed_plot, zoomed_plot); +TOOLBOX_PREF_PROFILE(profTogglePicture, show_pictures_in_profile, show_pictures_in_profile); +TOOLBOX_PREF_PROFILE(profTankbar, tankbar, tankbar); +TOOLBOX_PREF_PROFILE(profTissues, percentagegraph, percentagegraph); +// couldn't the args to TOOLBOX_PREF_PROFILE be made to go in the same sequence as TOOLBOX_PREF_BUTTON? + +void MainWindow::turnOffNdlTts() +{ + const bool triggered = false; + prefs.calcndltts = triggered; + PREF_PROFILE(calcndltts); +} + +#undef TOOLBOX_PREF_PROFILE +#undef PERF_PROFILE + +void MainWindow::on_actionExport_triggered() +{ + DiveLogExportDialog diveLogExport; + diveLogExport.exec(); +} + +void MainWindow::on_actionConfigure_Dive_Computer_triggered() +{ + ConfigureDiveComputerDialog *dcConfig = new ConfigureDiveComputerDialog(this); + dcConfig->show(); +} + +void MainWindow::setEnabledToolbar(bool arg1) +{ + Q_FOREACH (QAction *b, profileToolbarActions) + b->setEnabled(arg1); +} + +void MainWindow::on_copy_triggered() +{ + // open dialog to select what gets copied + // copy the displayed dive + DiveComponentSelection dialog(this, ©PasteDive, &what); + dialog.exec(); +} + +void MainWindow::on_paste_triggered() +{ + // take the data in our copyPasteDive and apply it to selected dives + selective_copy_dive(©PasteDive, &displayed_dive, what, false); + information()->showAndTriggerEditSelective(what); +} + +void MainWindow::on_actionFilterTags_triggered() +{ + if (ui.multiFilter->isVisible()) + ui.multiFilter->closeFilter(); + else + ui.multiFilter->setVisible(true); +} + +void MainWindow::registerApplicationState(const QByteArray& state, QWidget *topLeft, QWidget *topRight, QWidget *bottomLeft, QWidget *bottomRight) +{ + applicationState[state] = WidgetForQuadrant(topLeft, topRight, bottomLeft, bottomRight); + if (ui.topLeft->indexOf(topLeft) == -1 && topLeft) { + ui.topLeft->addWidget(topLeft); + } + if (ui.topRight->indexOf(topRight) == -1 && topRight) { + ui.topRight->addWidget(topRight); + } + if (ui.bottomLeft->indexOf(bottomLeft) == -1 && bottomLeft) { + ui.bottomLeft->addWidget(bottomLeft); + } + if(ui.bottomRight->indexOf(bottomRight) == -1 && bottomRight) { + ui.bottomRight->addWidget(bottomRight); + } +} + +void MainWindow::setApplicationState(const QByteArray& state) { + if (!applicationState.keys().contains(state)) + return; + + if (getCurrentAppState() == state) + return; + + setCurrentAppState(state); + +#define SET_CURRENT_INDEX( X ) \ + if (applicationState[state].X) { \ + ui.X->setCurrentWidget( applicationState[state].X); \ + ui.X->show(); \ + } else { \ + ui.X->hide(); \ + } + + SET_CURRENT_INDEX( topLeft ) + Q_FOREACH(const WidgetProperty& p, stateProperties[state].topLeft) { + ui.topLeft->currentWidget()->setProperty( p.first.data(), p.second); + } + SET_CURRENT_INDEX( topRight ) + Q_FOREACH(const WidgetProperty& p, stateProperties[state].topRight) { + ui.topRight->currentWidget()->setProperty( p.first.data(), p.second); + } + SET_CURRENT_INDEX( bottomLeft ) + Q_FOREACH(const WidgetProperty& p, stateProperties[state].bottomLeft) { + ui.bottomLeft->currentWidget()->setProperty( p.first.data(), p.second); + } + SET_CURRENT_INDEX( bottomRight ) + Q_FOREACH(const WidgetProperty& p, stateProperties[state].bottomRight) { + ui.bottomRight->currentWidget()->setProperty( p.first.data(), p.second); + } +#undef SET_CURRENT_INDEX +} + +void MainWindow::showProgressBar() +{ + if (progressDialog) + delete progressDialog; + + progressDialog = new QProgressDialog(tr("Contacting cloud service..."), tr("Cancel"), 0, 100, this); + progressDialog->setWindowModality(Qt::WindowModal); + progressDialog->setMinimumDuration(200); + progressDialogCanceled = false; + connect(progressDialog, SIGNAL(canceled()), this, SLOT(cancelCloudStorageOperation())); +} + +void MainWindow::cancelCloudStorageOperation() +{ + progressDialogCanceled = true; +} + +void MainWindow::hideProgressBar() +{ + if (progressDialog) { + progressDialog->setValue(100); + progressDialog->deleteLater(); + progressDialog = NULL; + } +} diff --git a/desktop-widgets/mainwindow.h b/desktop-widgets/mainwindow.h new file mode 100644 index 000000000..02ec2478c --- /dev/null +++ b/desktop-widgets/mainwindow.h @@ -0,0 +1,258 @@ +/* + * mainwindow.h + * + * header file for the main window of Subsurface + * + */ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include <QMainWindow> +#include <QAction> +#include <QUrl> +#include <QUuid> +#include <QProgressDialog> + +#include "ui_mainwindow.h" +#include "notificationwidget.h" +#include "windowtitleupdate.h" + +struct DiveList; +class QSortFilterProxyModel; +class DiveTripModel; + +class DiveInfo; +class DiveNotes; +class Stats; +class Equipment; +class QItemSelection; +class DiveListView; +class MainTab; +class ProfileGraphicsView; +class QWebView; +class QSettings; +class UpdateManager; +class UserManual; +class DivePlannerWidget; +class ProfileWidget2; +class PlannerDetails; +class PlannerSettingsWidget; +class QUndoStack; +class LocationInformationWidget; + +typedef std::pair<QByteArray, QVariant> WidgetProperty; +typedef QVector<WidgetProperty> PropertyList; + +enum MainWindowTitleFormat { + MWTF_DEFAULT, + MWTF_FILENAME +}; + +class MainWindow : public QMainWindow { + Q_OBJECT +public: + enum { + COLLAPSED, + EXPANDED + }; + + enum CurrentState { + VIEWALL, + GLOBE_MAXIMIZED, + INFO_MAXIMIZED, + PROFILE_MAXIMIZED, + LIST_MAXIMIZED + }; + + MainWindow(); + virtual ~MainWindow(); + static MainWindow *instance(); + MainTab *information(); + void loadRecentFiles(QSettings *s); + void addRecentFile(const QStringList &newFiles); + void removeRecentFile(QStringList failedFiles); + DiveListView *dive_list(); + DivePlannerWidget *divePlannerWidget(); + PlannerSettingsWidget *divePlannerSettingsWidget(); + LocationInformationWidget *locationInformationWidget(); + void setTitle(enum MainWindowTitleFormat format = MWTF_FILENAME); + + // Some shortcuts like "change DC" or "copy/paste dive components" + // should only be enabled when the profile's visible. + void disableShortcuts(bool disablePaste = true); + void enableShortcuts(); + void loadFiles(const QStringList files); + void importFiles(const QStringList importFiles); + void importTxtFiles(const QStringList fileNames); + void cleanUpEmpty(); + void setToolButtonsEnabled(bool enabled); + ProfileWidget2 *graphics() const; + PlannerDetails *plannerDetails() const; + void setLoadedWithFiles(bool filesFromCommandLine); + bool filesFromCommandLine() const; + void printPlan(); + void checkSurvey(QSettings *s); + void setApplicationState(const QByteArray& state); + void setStateProperties(const QByteArray& state, const PropertyList& tl, const PropertyList& tr, const PropertyList& bl,const PropertyList& br); + bool inPlanner(); + QUndoStack *undoStack; + NotificationWidget *getNotificationWidget(); + void enableDisableCloudActions(); + void showError(); +private +slots: + /* file menu action */ + void recentFileTriggered(bool checked); + void on_actionNew_triggered(); + void on_actionOpen_triggered(); + void on_actionSave_triggered(); + void on_actionSaveAs_triggered(); + void on_actionClose_triggered(); + void on_actionCloudstorageopen_triggered(); + void on_actionCloudstoragesave_triggered(); + void on_actionPrint_triggered(); + void on_actionPreferences_triggered(); + void on_actionQuit_triggered(); + void on_actionHash_images_triggered(); + + /* log menu actions */ + void on_actionDownloadDC_triggered(); + void on_actionDownloadWeb_triggered(); + void on_actionDivelogs_de_triggered(); + void on_actionEditDeviceNames_triggered(); + void on_actionAddDive_triggered(); + void on_actionEditDive_triggered(); + void on_actionRenumber_triggered(); + void on_actionAutoGroup_triggered(); + void on_actionYearlyStatistics_triggered(); + + /* view menu actions */ + void on_actionViewList_triggered(); + void on_actionViewProfile_triggered(); + void on_actionViewInfo_triggered(); + void on_actionViewGlobe_triggered(); + void on_actionViewAll_triggered(); + void on_actionPreviousDC_triggered(); + void on_actionNextDC_triggered(); + void on_actionFullScreen_triggered(bool checked); + + /* other menu actions */ + void on_actionAboutSubsurface_triggered(); + void on_actionUserManual_triggered(); + void on_actionUserSurvey_triggered(); + void on_actionDivePlanner_triggered(); + void on_actionReplanDive_triggered(); + void on_action_Check_for_Updates_triggered(); + + void on_actionDiveSiteEdit_triggered(); + void current_dive_changed(int divenr); + void initialUiSetup(); + + void on_actionImportDiveLog_triggered(); + + /* TODO: Move those slots below to it's own class */ + void on_profCalcAllTissues_triggered(bool triggered); + void on_profCalcCeiling_triggered(bool triggered); + void on_profDcCeiling_triggered(bool triggered); + void on_profEad_triggered(bool triggered); + void on_profIncrement3m_triggered(bool triggered); + void on_profMod_triggered(bool triggered); + void on_profNdl_tts_triggered(bool triggered); + void on_profPO2_triggered(bool triggered); + void on_profPhe_triggered(bool triggered); + void on_profPn2_triggered(bool triggered); + void on_profHR_triggered(bool triggered); + void on_profRuler_triggered(bool triggered); + void on_profSAC_triggered(bool triggered); + void on_profScaled_triggered(bool triggered); + void on_profTogglePicture_triggered(bool triggered); + void on_profTankbar_triggered(bool triggered); + void on_profTissues_triggered(bool triggered); + void on_actionExport_triggered(); + void on_copy_triggered(); + void on_paste_triggered(); + void on_actionFilterTags_triggered(); + void on_actionConfigure_Dive_Computer_triggered(); + void setDefaultState(); + void setAutomaticTitle(); + void cancelCloudStorageOperation(); + +protected: + void closeEvent(QCloseEvent *); + +signals: + void startDiveSiteEdit(); + +public +slots: + void turnOffNdlTts(); + void readSettings(); + void refreshDisplay(bool doRecreateDiveList = true); + void recreateDiveList(); + void showProfile(); + void refreshProfile(); + void editCurrentDive(); + void planCanceled(); + void planCreated(); + void setEnabledToolbar(bool arg1); + void setPlanNotes(); + +private: + Ui::MainWindow ui; + QAction *actionNextDive; + QAction *actionPreviousDive; + UserManual *helpView; + CurrentState state; + QString filter(); + static MainWindow *m_Instance; + QString displayedFilename(QString fullFilename); + bool askSaveChanges(); + bool okToClose(QString message); + void closeCurrentFile(); + void showProgressBar(); + void hideProgressBar(); + void writeSettings(); + int file_save(); + int file_save_as(); + void beginChangeState(CurrentState s); + void saveSplitterSizes(); + QString lastUsedDir(); + void updateLastUsedDir(const QString &s); + void registerApplicationState(const QByteArray& state, QWidget *topLeft, QWidget *topRight, QWidget *bottomLeft, QWidget *bottomRight); + bool filesAsArguments; + UpdateManager *updateManager; + + bool plannerStateClean(); + void setupForAddAndPlan(const char *model); + void configureToolbar(); + QDialog *survey; + struct dive copyPasteDive; + struct dive_components what; + QList<QAction *> profileToolbarActions; + + struct WidgetForQuadrant { + WidgetForQuadrant(QWidget *tl = 0, QWidget *tr = 0, QWidget *bl = 0, QWidget *br = 0) : + topLeft(tl), topRight(tr), bottomLeft(bl), bottomRight(br) {} + QWidget *topLeft; + QWidget *topRight; + QWidget *bottomLeft; + QWidget *bottomRight; + }; + + struct PropertiesForQuadrant { + PropertiesForQuadrant(){} + PropertiesForQuadrant(const PropertyList& tl, const PropertyList& tr,const PropertyList& bl,const PropertyList& br) : + topLeft(tl), topRight(tr), bottomLeft(bl), bottomRight(br) {} + PropertyList topLeft; + PropertyList topRight; + PropertyList bottomLeft; + PropertyList bottomRight; + }; + + QHash<QByteArray, WidgetForQuadrant> applicationState; + QHash<QByteArray, PropertiesForQuadrant> stateProperties; + + WindowTitleUpdate *wtu; +}; + +#endif // MAINWINDOW_H diff --git a/desktop-widgets/mainwindow.ui b/desktop-widgets/mainwindow.ui new file mode 100644 index 000000000..5e3200cfc --- /dev/null +++ b/desktop-widgets/mainwindow.ui @@ -0,0 +1,761 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>861</width> + <height>800</height> + </rect> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QVBoxLayout" name="fullWindowLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="margin"> + <number>0</number> + </property> + <item> + <widget class="MultiFilter" name="multiFilter" native="true"/> + </item> + <item> + <widget class="QSplitter" name="mainSplitter"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <widget class="QSplitter" name="topSplitter"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <widget class="QStackedWidget" name="topLeft"/> + <widget class="QStackedWidget" name="topRight"/> + </widget> + <widget class="QSplitter" name="bottomSplitter"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <widget class="QStackedWidget" name="bottomLeft"/> + <widget class="QStackedWidget" name="bottomRight"/> + </widget> + </widget> + </item> + <item> + <widget class="NotificationWidget" name="mainErrorMessage" native="true"/> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menubar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>861</width> + <height>23</height> + </rect> + </property> + <widget class="QMenu" name="menuFile"> + <property name="title"> + <string>&File</string> + </property> + <addaction name="actionNew"/> + <addaction name="actionOpen"/> + <addaction name="actionCloudstorageopen"/> + <addaction name="actionSave"/> + <addaction name="actionCloudstoragesave"/> + <addaction name="actionSaveAs"/> + <addaction name="separator"/> + <addaction name="actionClose"/> + <addaction name="actionExport"/> + <addaction name="actionPrint"/> + <addaction name="actionPreferences"/> + <addaction name="separator"/> + <addaction name="actionHash_images"/> + <addaction name="actionConfigure_Dive_Computer"/> + <addaction name="actionRecent1"/> + <addaction name="actionRecent2"/> + <addaction name="actionRecent3"/> + <addaction name="actionRecent4"/> + <addaction name="separator"/> + <addaction name="actionQuit"/> + </widget> + <widget class="QMenu" name="menuLog"> + <property name="title"> + <string>&Log</string> + </property> + <addaction name="actionAddDive"/> + <addaction name="actionEditDive"/> + <addaction name="actionDivePlanner"/> + <addaction name="actionReplanDive"/> + <addaction name="copy"/> + <addaction name="paste"/> + <addaction name="separator"/> + <addaction name="actionRenumber"/> + <addaction name="actionAutoGroup"/> + <addaction name="separator"/> + <addaction name="actionEditDeviceNames"/> + <addaction name="actionFilterTags"/> + </widget> + <widget class="QMenu" name="menuView"> + <property name="title"> + <string>&View</string> + </property> + <addaction name="actionViewAll"/> + <addaction name="actionViewList"/> + <addaction name="actionViewProfile"/> + <addaction name="actionViewInfo"/> + <addaction name="actionViewGlobe"/> + <addaction name="separator"/> + <addaction name="actionYearlyStatistics"/> + <addaction name="actionPreviousDC"/> + <addaction name="actionNextDC"/> + <addaction name="separator"/> + <addaction name="actionFullScreen"/> + </widget> + <widget class="QMenu" name="menuHelp"> + <property name="title"> + <string>&Help</string> + </property> + <addaction name="actionAboutSubsurface"/> + <addaction name="action_Check_for_Updates"/> + <addaction name="actionUserSurvey"/> + <addaction name="actionUserManual"/> + </widget> + <widget class="QMenu" name="menuImport"> + <property name="title"> + <string>&Import</string> + </property> + <addaction name="actionDownloadDC"/> + <addaction name="actionImportDiveLog"/> + <addaction name="actionDownloadWeb"/> + <addaction name="actionDivelogs_de"/> + </widget> + <widget class="QMenu" name="menu_Edit"> + <property name="title"> + <string>&Edit</string> + </property> + </widget> + <widget class="QMenu" name="menuShare_on"> + <property name="title"> + <string>Share on</string> + </property> + <addaction name="separator"/> + <addaction name="actionFacebook"/> + </widget> + <addaction name="menuFile"/> + <addaction name="menu_Edit"/> + <addaction name="menuImport"/> + <addaction name="menuLog"/> + <addaction name="menuView"/> + <addaction name="menuShare_on"/> + <addaction name="menuHelp"/> + </widget> + <action name="actionNew"> + <property name="text"> + <string>&New logbook</string> + </property> + <property name="toolTip"> + <string>New</string> + </property> + <property name="shortcut"> + <string>Ctrl+N</string> + </property> + </action> + <action name="actionOpen"> + <property name="text"> + <string>&Open logbook</string> + </property> + <property name="toolTip"> + <string>Open</string> + </property> + <property name="shortcut"> + <string>Ctrl+O</string> + </property> + </action> + <action name="actionSave"> + <property name="text"> + <string>&Save</string> + </property> + <property name="toolTip"> + <string>Save</string> + </property> + <property name="shortcut"> + <string>Ctrl+S</string> + </property> + </action> + <action name="actionSaveAs"> + <property name="text"> + <string>Sa&ve as</string> + </property> + <property name="toolTip"> + <string>Save as</string> + </property> + <property name="shortcut"> + <string>Ctrl+Shift+S</string> + </property> + </action> + <action name="actionClose"> + <property name="text"> + <string>&Close</string> + </property> + <property name="toolTip"> + <string>Close</string> + </property> + <property name="shortcut"> + <string>Ctrl+W</string> + </property> + </action> + <action name="actionPrint"> + <property name="text"> + <string>&Print</string> + </property> + <property name="shortcut"> + <string>Ctrl+P</string> + </property> + </action> + <action name="actionPreferences"> + <property name="text"> + <string>P&references</string> + </property> + <property name="shortcut"> + <string>Ctrl+,</string> + </property> + <property name="menuRole"> + <enum>QAction::PreferencesRole</enum> + </property> + </action> + <action name="actionQuit"> + <property name="text"> + <string>&Quit</string> + </property> + <property name="shortcut"> + <string>Ctrl+Q</string> + </property> + <property name="menuRole"> + <enum>QAction::QuitRole</enum> + </property> + </action> + <action name="actionDownloadDC"> + <property name="text"> + <string>Import from &dive computer</string> + </property> + <property name="shortcut"> + <string>Ctrl+D</string> + </property> + </action> + <action name="actionDownloadWeb"> + <property name="text"> + <string>Import &GPS data from Subsurface web service</string> + </property> + <property name="shortcut"> + <string>Ctrl+G</string> + </property> + </action> + <action name="actionEditDeviceNames"> + <property name="text"> + <string>Edit device &names</string> + </property> + </action> + <action name="actionAddDive"> + <property name="text"> + <string>&Add dive</string> + </property> + <property name="shortcut"> + <string>Ctrl++</string> + </property> + </action> + <action name="actionEditDive"> + <property name="text"> + <string>&Edit dive</string> + </property> + </action> + <action name="copy"> + <property name="text"> + <string>&Copy dive components</string> + </property> + <property name="shortcut"> + <string>Ctrl+C</string> + </property> + </action> + <action name="paste"> + <property name="text"> + <string>&Paste dive components</string> + </property> + <property name="shortcut"> + <string>Ctrl+V</string> + </property> + </action> + <action name="actionRenumber"> + <property name="text"> + <string>&Renumber</string> + </property> + <property name="shortcut"> + <string>Ctrl+R</string> + </property> + </action> + <action name="actionAutoGroup"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="text"> + <string>Auto &group</string> + </property> + </action> + <action name="actionYearlyStatistics"> + <property name="text"> + <string>&Yearly statistics</string> + </property> + <property name="shortcut"> + <string>Ctrl+Y</string> + </property> + </action> + <action name="actionViewList"> + <property name="text"> + <string>&Dive list</string> + </property> + <property name="shortcut"> + <string>Ctrl+2</string> + </property> + </action> + <action name="actionViewProfile"> + <property name="text"> + <string>&Profile</string> + </property> + <property name="shortcut"> + <string>Ctrl+3</string> + </property> + </action> + <action name="actionViewInfo"> + <property name="text"> + <string>&Info</string> + </property> + <property name="shortcut"> + <string>Ctrl+4</string> + </property> + </action> + <action name="actionViewAll"> + <property name="text"> + <string>&All</string> + </property> + <property name="shortcut"> + <string>Ctrl+1</string> + </property> + </action> + <action name="actionPreviousDC"> + <property name="text"> + <string>P&revious DC</string> + </property> + <property name="shortcut"> + <string>Left</string> + </property> + </action> + <action name="actionNextDC"> + <property name="text"> + <string>&Next DC</string> + </property> + <property name="shortcut"> + <string>Right</string> + </property> + </action> + <action name="actionAboutSubsurface"> + <property name="text"> + <string>&About Subsurface</string> + </property> + <property name="menuRole"> + <enum>QAction::AboutRole</enum> + </property> + </action> + <action name="actionUserManual"> + <property name="text"> + <string>User &manual</string> + </property> + <property name="shortcut"> + <string>F1</string> + </property> + </action> + <action name="actionViewGlobe"> + <property name="text"> + <string>&Globe</string> + </property> + <property name="shortcut"> + <string>Ctrl+5</string> + </property> + </action> + <action name="actionDivePlanner"> + <property name="text"> + <string>P&lan dive</string> + </property> + <property name="shortcut"> + <string>Ctrl+L</string> + </property> + </action> + <action name="actionImportDiveLog"> + <property name="text"> + <string>&Import log files</string> + </property> + <property name="toolTip"> + <string>Import divelog files from other applications</string> + </property> + <property name="shortcut"> + <string>Ctrl+I</string> + </property> + </action> + <action name="actionDivelogs_de"> + <property name="text"> + <string>Import &from divelogs.de</string> + </property> + </action> + <action name="actionFullScreen"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="text"> + <string>&Full screen</string> + </property> + <property name="toolTip"> + <string>Toggle full screen</string> + </property> + <property name="shortcut"> + <string>F11</string> + </property> + </action> + <action name="actionRecent1"> + <property name="visible"> + <bool>false</bool> + </property> + </action> + <action name="actionRecent2"> + <property name="visible"> + <bool>false</bool> + </property> + </action> + <action name="actionRecent3"> + <property name="visible"> + <bool>false</bool> + </property> + </action> + <action name="actionRecent4"> + <property name="visible"> + <bool>false</bool> + </property> + </action> + <action name="action_Check_for_Updates"> + <property name="text"> + <string>&Check for updates</string> + </property> + </action> + <action name="actionExport"> + <property name="text"> + <string>&Export</string> + </property> + <property name="toolTip"> + <string>Export dive logs</string> + </property> + <property name="shortcut"> + <string>Ctrl+E</string> + </property> + </action> + <action name="actionConfigure_Dive_Computer"> + <property name="text"> + <string>Configure &dive computer</string> + </property> + <property name="shortcut"> + <string>Ctrl+Shift+C</string> + </property> + <property name="menuRole"> + <enum>QAction::NoRole</enum> + </property> + </action> + <action name="actionReplanDive"> + <property name="text"> + <string>Edit &dive in planner</string> + </property> + </action> + <action name="profPO2"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/icon_o2</normaloff>:/icon_o2</iconset> + </property> + <property name="text"> + <string>Toggle pOâ‚‚ graph</string> + </property> + </action> + <action name="profPn2"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/icon_n2</normaloff>:/icon_n2</iconset> + </property> + <property name="text"> + <string>Toggle pNâ‚‚ graph</string> + </property> + </action> + <action name="profPhe"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/icon_he</normaloff>:/icon_he</iconset> + </property> + <property name="text"> + <string>Toggle pHe graph</string> + </property> + </action> + <action name="profDcCeiling"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/icon_ceiling_dc</normaloff>:/icon_ceiling_dc</iconset> + </property> + <property name="text"> + <string>Toggle DC reported ceiling</string> + </property> + </action> + <action name="profCalcCeiling"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/icon_ceiling_calculated</normaloff>:/icon_ceiling_calculated</iconset> + </property> + <property name="text"> + <string>Toggle calculated ceiling</string> + </property> + </action> + <action name="profCalcAllTissues"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/icon_ceiling_alltissues</normaloff>:/icon_ceiling_alltissues</iconset> + </property> + <property name="text"> + <string>Toggle calculating all tissues</string> + </property> + </action> + <action name="profIncrement3m"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/icon_ceiling_3m</normaloff>:/icon_ceiling_3m</iconset> + </property> + <property name="text"> + <string>Toggle calculated ceiling with 3m increments</string> + </property> + </action> + <action name="profHR"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/icon_HR</normaloff>:/icon_HR</iconset> + </property> + <property name="text"> + <string>Toggle heart rate</string> + </property> + </action> + <action name="profMod"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/icon_mod</normaloff>:/icon_mod</iconset> + </property> + <property name="text"> + <string>Toggle MOD</string> + </property> + </action> + <action name="profEad"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/icon_ead</normaloff>:/icon_ead</iconset> + </property> + <property name="text"> + <string>Toggle EAD, END, EADD</string> + </property> + </action> + <action name="profNdl_tts"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/icon_NDLTTS</normaloff>:/icon_NDLTTS</iconset> + </property> + <property name="text"> + <string>Toggle NDL, TTS</string> + </property> + </action> + <action name="profSAC"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/icon_lung</normaloff>:/icon_lung</iconset> + </property> + <property name="text"> + <string>Toggle SAC rate</string> + </property> + </action> + <action name="profRuler"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/ruler</normaloff>:/ruler</iconset> + </property> + <property name="text"> + <string>Toggle ruler</string> + </property> + </action> + <action name="profScaled"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/scale</normaloff>:/scale</iconset> + </property> + <property name="text"> + <string>Scale graph</string> + </property> + </action> + <action name="profTogglePicture"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/pictures</normaloff>:/pictures</iconset> + </property> + <property name="text"> + <string>Toggle pictures</string> + </property> + </action> + <action name="profTankbar"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/gaschange</normaloff>:/gaschange</iconset> + </property> + <property name="text"> + <string>Toggle tank bar</string> + </property> + </action> + <action name="actionFilterTags"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="text"> + <string>&Filter divelist</string> + </property> + <property name="shortcut"> + <string>Ctrl+F</string> + </property> + </action> + <action name="profTissues"> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="icon"> + <iconset resource="../subsurface.qrc"> + <normaloff>:/icon_tissue</normaloff>:/icon_tissue</iconset> + </property> + <property name="text"> + <string>Toggle tissue graph</string> + </property> + </action> + <action name="actionUserSurvey"> + <property name="text"> + <string>User &survey</string> + </property> + </action> + <action name="action_Undo"> + <property name="text"> + <string>&Undo</string> + </property> + <property name="shortcut"> + <string>Ctrl+Z</string> + </property> + </action> + <action name="action_Redo"> + <property name="text"> + <string>&Redo</string> + </property> + <property name="shortcut"> + <string>Ctrl+Shift+Z</string> + </property> + </action> + <action name="actionHash_images"> + <property name="text"> + <string>&Find moved images</string> + </property> + </action> + <action name="actionCloudstorageopen"> + <property name="text"> + <string>Open c&loud storage</string> + </property> + </action> + <action name="actionCloudstoragesave"> + <property name="text"> + <string>Save to clo&ud storage</string> + </property> + </action> + <action name="actionManage_dive_sites"> + <property name="text"> + <string>&Manage dive sites</string> + </property> + </action> + <action name="actionDiveSiteEdit"> + <property name="text"> + <string>Dive Site &Edit</string> + </property> + </action> + <action name="actionFacebook"> + <property name="text"> + <string>Facebook</string> + </property> + </action> + </widget> + <customwidgets> + <customwidget> + <class>NotificationWidget</class> + <extends>QWidget</extends> + <header>notificationwidget.h</header> + <container>1</container> + </customwidget> + <customwidget> + <class>MultiFilter</class> + <extends>QWidget</extends> + <header>simplewidgets.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <resources> + <include location="../subsurface.qrc"/> + </resources> + <connections/> +</ui> diff --git a/desktop-widgets/marble/GeoDataTreeModel.h b/desktop-widgets/marble/GeoDataTreeModel.h new file mode 100644 index 000000000..39eff8388 --- /dev/null +++ b/desktop-widgets/marble/GeoDataTreeModel.h @@ -0,0 +1,118 @@ +// +// This file is part of the Marble Virtual Globe. +// +// This program is free software licensed under the GNU LGPL. You can +// find a copy of this license in LICENSE.txt in the top directory of +// the source code. +// +// Copyright 2010 Thibaut Gridel <tgridel@free.fr> +// + +#ifndef MARBLE_GEODATATREEMODEL_H +#define MARBLE_GEODATATREEMODEL_H + +// -> does not appear to be needed #include "marble_export.h" + +#include <QAbstractItemModel> + +class QItemSelectionModel; + +namespace Marble +{ +class GeoDataObject; +class GeoDataDocument; +class GeoDataFeature; +class GeoDataContainer; + + +/** + * @short The representation of GeoData in a model + * This class represents all available data given by kml-data files. + */ +class MARBLE_EXPORT GeoDataTreeModel : public QAbstractItemModel +{ + Q_OBJECT + + public: + + /** + * Creates a new GeoDataTreeModel. + * + * @param parent The parent object. + */ + explicit GeoDataTreeModel( QObject *parent = 0 ); + + /** + * Destroys the GeoDataModel. + */ + ~GeoDataTreeModel(); + + virtual bool hasChildren( const QModelIndex &parent ) const; + + /** + * Return the number of Items in the Model. + */ + int rowCount( const QModelIndex &parent = QModelIndex() ) const; + + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const; + + QVariant data( const QModelIndex &index, int role ) const; + + QModelIndex index( int row, int column, + const QModelIndex &parent = QModelIndex() ) const; + + QModelIndex index( GeoDataObject* object ); + + QModelIndex parent( const QModelIndex &index ) const; + + int columnCount( const QModelIndex &parent = QModelIndex() ) const; + + Qt::ItemFlags flags ( const QModelIndex & index ) const; + + bool setData ( const QModelIndex & index, const QVariant & value, int role = Qt::EditRole ); + + void reset(); + + QItemSelectionModel *selectionModel(); + +public Q_SLOTS: + + /** + * Sets the root document to use. This replaces previously loaded data, if any. + * @param document The new root document. Ownership retains with the caller, + * i.e. GeoDataTreeModel will not delete the passed document at its destruction. + */ + void setRootDocument( GeoDataDocument *document ); + GeoDataDocument* rootDocument(); + + int addFeature( GeoDataContainer *parent, GeoDataFeature *feature, int row = -1 ); + + bool removeFeature( GeoDataContainer *parent, int index ); + + int removeFeature( const GeoDataFeature *feature ); + + void updateFeature( GeoDataFeature *feature ); + + int addDocument( GeoDataDocument *document ); + + void removeDocument( int index ); + + void removeDocument( GeoDataDocument* document ); + + void update(); + +Q_SIGNALS: + /// insert and remove row don't trigger any signal that proxies forward + /// this signal will refresh geometry layer and placemark layout + void removed( GeoDataObject *object ); + void added( GeoDataObject *object ); + private: + Q_DISABLE_COPY( GeoDataTreeModel ) + class Private; + Private* const d; +}; + +} + +#endif // MARBLE_GEODATATREEMODEL_H diff --git a/desktop-widgets/modeldelegates.cpp b/desktop-widgets/modeldelegates.cpp new file mode 100644 index 000000000..881037a83 --- /dev/null +++ b/desktop-widgets/modeldelegates.cpp @@ -0,0 +1,617 @@ +#include "modeldelegates.h" +#include "dive.h" +#include "gettextfromc.h" +#include "mainwindow.h" +#include "cylindermodel.h" +#include "models.h" +#include "starwidget.h" +#include "profile/profilewidget2.h" +#include "tankinfomodel.h" +#include "weigthsysteminfomodel.h" +#include "weightmodel.h" +#include "divetripmodel.h" +#include "qthelper.h" +#ifndef NO_MARBLE +#include "globe.h" +#endif + +#include <QCompleter> +#include <QKeyEvent> +#include <QTextDocument> +#include <QApplication> +#include <QFont> +#include <QBrush> +#include <QColor> +#include <QAbstractProxyModel> + +QSize DiveListDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + return QSize(50, 22); +} + +// Gets the index of the model in the currentRow and column. +// currCombo is defined below. +#define IDX(_XX) mymodel->index(currCombo.currRow, (_XX)) +static bool keyboardFinished = false; + +StarWidgetsDelegate::StarWidgetsDelegate(QWidget *parent) : QStyledItemDelegate(parent), + parentWidget(parent) +{ + const IconMetrics& metrics = defaultIconMetrics(); + minStarSize = QSize(metrics.sz_small * TOTALSTARS + metrics.spacing * (TOTALSTARS - 1), metrics.sz_small); +} + +void StarWidgetsDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QStyledItemDelegate::paint(painter, option, index); + if (!index.isValid()) + return; + + QVariant value = index.model()->data(index, DiveTripModel::STAR_ROLE); + if (!value.isValid()) + return; + + int rating = value.toInt(); + int deltaY = option.rect.height() / 2 - StarWidget::starActive().height() / 2; + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + const QPixmap active = QPixmap::fromImage(StarWidget::starActive()); + const QPixmap inactive = QPixmap::fromImage(StarWidget::starInactive()); + const IconMetrics& metrics = defaultIconMetrics(); + + for (int i = 0; i < rating; i++) + painter->drawPixmap(option.rect.x() + i * metrics.sz_small + metrics.spacing, option.rect.y() + deltaY, active); + for (int i = rating; i < TOTALSTARS; i++) + painter->drawPixmap(option.rect.x() + i * metrics.sz_small + metrics.spacing, option.rect.y() + deltaY, inactive); + painter->restore(); +} + +QSize StarWidgetsDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + return minStarSize; +} + +const QSize& StarWidgetsDelegate::starSize() const +{ + return minStarSize; +} + +ComboBoxDelegate::ComboBoxDelegate(QAbstractItemModel *model, QObject *parent) : QStyledItemDelegate(parent), model(model) +{ + connect(this, SIGNAL(closeEditor(QWidget *, QAbstractItemDelegate::EndEditHint)), + this, SLOT(revertModelData(QWidget *, QAbstractItemDelegate::EndEditHint))); + connect(this, SIGNAL(closeEditor(QWidget *, QAbstractItemDelegate::EndEditHint)), + this, SLOT(fixTabBehavior())); +} + +void ComboBoxDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + QComboBox *c = qobject_cast<QComboBox *>(editor); + QString data = index.model()->data(index, Qt::DisplayRole).toString(); + int i = c->findText(data); + if (i != -1) + c->setCurrentIndex(i); + else + c->setEditText(data); + c->lineEdit()->setSelection(0, c->lineEdit()->text().length()); +} + +struct CurrSelected { + QComboBox *comboEditor; + int currRow; + QString activeText; + QAbstractItemModel *model; + bool ignoreSelection; +} currCombo; + +QWidget *ComboBoxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + MainWindow *m = MainWindow::instance(); + QComboBox *comboDelegate = new QComboBox(parent); + comboDelegate->setModel(model); + comboDelegate->setEditable(true); + comboDelegate->setAutoCompletion(true); + comboDelegate->setAutoCompletionCaseSensitivity(Qt::CaseInsensitive); + comboDelegate->completer()->setCompletionMode(QCompleter::PopupCompletion); + comboDelegate->view()->setEditTriggers(QAbstractItemView::AllEditTriggers); + comboDelegate->lineEdit()->installEventFilter(const_cast<QObject *>(qobject_cast<const QObject *>(this))); + if ((m->graphics()->currentState != ProfileWidget2::PROFILE)) + comboDelegate->lineEdit()->setEnabled(false); + comboDelegate->view()->installEventFilter(const_cast<QObject *>(qobject_cast<const QObject *>(this))); + QAbstractItemView *comboPopup = comboDelegate->lineEdit()->completer()->popup(); + comboPopup->setMouseTracking(true); + connect(comboDelegate, SIGNAL(highlighted(QString)), this, SLOT(testActivation(QString))); + connect(comboDelegate, SIGNAL(activated(QString)), this, SLOT(fakeActivation())); + connect(comboPopup, SIGNAL(entered(QModelIndex)), this, SLOT(testActivation(QModelIndex))); + connect(comboPopup, SIGNAL(activated(QModelIndex)), this, SLOT(fakeActivation())); + currCombo.comboEditor = comboDelegate; + currCombo.currRow = index.row(); + currCombo.model = const_cast<QAbstractItemModel *>(index.model()); + keyboardFinished = false; + + // Current display of things on Gnome3 looks like shit, so + // let`s fix that. + if (isGnome3Session()) { + QPalette p; + p.setColor(QPalette::Window, QColor(Qt::white)); + p.setColor(QPalette::Base, QColor(Qt::white)); + comboDelegate->lineEdit()->setPalette(p); + comboDelegate->setPalette(p); + } + return comboDelegate; +} + +/* This Method is being called when the user *writes* something and press enter or tab, + * and it`s also called when the mouse walks over the list of choices from the ComboBox, + * One thing is important, if the user writes a *new* cylinder or weight type, it will + * be ADDED to the list, and the user will need to fill the other data. + */ +void ComboBoxDelegate::testActivation(const QString &currText) +{ + currCombo.activeText = currText.isEmpty() ? currCombo.comboEditor->currentText() : currText; + setModelData(currCombo.comboEditor, currCombo.model, QModelIndex()); +} + +void ComboBoxDelegate::testActivation(const QModelIndex &currIndex) +{ + testActivation(currIndex.data().toString()); +} + +// HACK, send a fake event so Qt thinks we hit 'enter' on the line edit. +void ComboBoxDelegate::fakeActivation() +{ + /* this test is needed because as soon as I show the selector, + * the first item gots selected, this sending an activated signal, + * calling this fakeActivation code and setting as the current, + * thig that we don't want. so, let's just set the ignoreSelection + * to false and be happy, because the next activation ( by click + * or keypress) is real. + */ + if (currCombo.ignoreSelection) { + currCombo.ignoreSelection = false; + return; + } + QKeyEvent ev(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier); + QStyledItemDelegate::eventFilter(currCombo.comboEditor, &ev); +} + +// This 'reverts' the model data to what we actually choosed, +// becaus e a TAB is being understood by Qt as 'cancel' while +// we are on a QComboBox ( but not on a QLineEdit. +void ComboBoxDelegate::fixTabBehavior() +{ + if (keyboardFinished) { + setModelData(0, 0, QModelIndex()); + } +} + +bool ComboBoxDelegate::eventFilter(QObject *object, QEvent *event) +{ + // Reacts on Key_UP and Key_DOWN to show the QComboBox - list of choices. + if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) { + if (object == currCombo.comboEditor) { // the 'LineEdit' part + QKeyEvent *ev = static_cast<QKeyEvent *>(event); + if (ev->key() == Qt::Key_Up || ev->key() == Qt::Key_Down) { + currCombo.ignoreSelection = true; + if (!currCombo.comboEditor->completer()->popup()->isVisible()) { + currCombo.comboEditor->showPopup(); + return true; + } + } + if (ev->key() == Qt::Key_Tab || ev->key() == Qt::Key_Enter || ev->key() == Qt::Key_Return) { + currCombo.activeText = currCombo.comboEditor->currentText(); + keyboardFinished = true; + } + } else { // the 'Drop Down Menu' part. + QKeyEvent *ev = static_cast<QKeyEvent *>(event); + if (ev->key() == Qt::Key_Enter || ev->key() == Qt::Key_Return || + ev->key() == Qt::Key_Tab || ev->key() == Qt::Key_Backtab || + ev->key() == Qt::Key_Escape) { + // treat Qt as a silly little boy - pretending that the key_return nwas pressed on the combo, + // instead of the list of choices. this can be extended later for + // other imputs, like tab navigation and esc. + QStyledItemDelegate::eventFilter(currCombo.comboEditor, event); + } + } + } + + return QStyledItemDelegate::eventFilter(object, event); +} + +void ComboBoxDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QRect defaultRect = option.rect; + defaultRect.setX(defaultRect.x() - 1); + defaultRect.setY(defaultRect.y() - 1); + defaultRect.setWidth(defaultRect.width() + 2); + defaultRect.setHeight(defaultRect.height() + 2); + editor->setGeometry(defaultRect); +} + +struct RevertCylinderData { + QString type; + int pressure; + int size; +} currCylinderData; + +void TankInfoDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &thisindex) const +{ + CylindersModel *mymodel = qobject_cast<CylindersModel *>(currCombo.model); + TankInfoModel *tanks = TankInfoModel::instance(); + QModelIndexList matches = tanks->match(tanks->index(0, 0), Qt::DisplayRole, currCombo.activeText); + int row; + QString cylinderName = currCombo.activeText; + if (matches.isEmpty()) { + tanks->insertRows(tanks->rowCount(), 1); + tanks->setData(tanks->index(tanks->rowCount() - 1, 0), currCombo.activeText); + row = tanks->rowCount() - 1; + } else { + row = matches.first().row(); + cylinderName = matches.first().data().toString(); + } + int tankSize = tanks->data(tanks->index(row, TankInfoModel::ML)).toInt(); + int tankPressure = tanks->data(tanks->index(row, TankInfoModel::BAR)).toInt(); + + mymodel->setData(IDX(CylindersModel::TYPE), cylinderName, Qt::EditRole); + mymodel->passInData(IDX(CylindersModel::WORKINGPRESS), tankPressure); + mymodel->passInData(IDX(CylindersModel::SIZE), tankSize); +} + +TankInfoDelegate::TankInfoDelegate(QObject *parent) : ComboBoxDelegate(TankInfoModel::instance(), parent) +{ + connect(this, SIGNAL(closeEditor(QWidget *, QAbstractItemDelegate::EndEditHint)), + this, SLOT(reenableReplot(QWidget *, QAbstractItemDelegate::EndEditHint))); +} + +void TankInfoDelegate::reenableReplot(QWidget *widget, QAbstractItemDelegate::EndEditHint hint) +{ + MainWindow::instance()->graphics()->setReplot(true); + // FIXME: We need to replot after a cylinder is selected but the replot below overwrites + // the newly selected cylinder. + // MainWindow::instance()->graphics()->replot(); +} + +void TankInfoDelegate::revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint) +{ + if (hint == QAbstractItemDelegate::NoHint || + hint == QAbstractItemDelegate::RevertModelCache) { + CylindersModel *mymodel = qobject_cast<CylindersModel *>(currCombo.model); + mymodel->setData(IDX(CylindersModel::TYPE), currCylinderData.type, Qt::EditRole); + mymodel->passInData(IDX(CylindersModel::WORKINGPRESS), currCylinderData.pressure); + mymodel->passInData(IDX(CylindersModel::SIZE), currCylinderData.size); + } +} + +QWidget *TankInfoDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + // ncreate editor needs to be called before because it will populate a few + // things in the currCombo global var. + QWidget *delegate = ComboBoxDelegate::createEditor(parent, option, index); + CylindersModel *mymodel = qobject_cast<CylindersModel *>(currCombo.model); + cylinder_t *cyl = mymodel->cylinderAt(index); + currCylinderData.type = copy_string(cyl->type.description); + currCylinderData.pressure = cyl->type.workingpressure.mbar; + currCylinderData.size = cyl->type.size.mliter; + MainWindow::instance()->graphics()->setReplot(false); + return delegate; +} + +TankUseDelegate::TankUseDelegate(QObject *parent) : QStyledItemDelegate(parent) +{ +} + +QWidget *TankUseDelegate::createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const +{ + QComboBox *comboBox = new QComboBox(parent); + for (int i = 0; i < NUM_GAS_USE; i++) + comboBox->addItem(gettextFromC::instance()->trGettext(cylinderuse_text[i])); + return comboBox; +} + +void TankUseDelegate::setEditorData(QWidget * editor, const QModelIndex & index) const +{ + QComboBox *comboBox = qobject_cast<QComboBox*>(editor); + QString indexString = index.data().toString(); + comboBox->setCurrentIndex(cylinderuse_from_text(indexString.toUtf8().data())); +} + +void TankUseDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const +{ + QComboBox *comboBox = qobject_cast<QComboBox*>(editor); + model->setData(index, comboBox->currentIndex()); +} + +struct RevertWeightData { + QString type; + int weight; +} currWeight; + +void WSInfoDelegate::revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint) +{ + if (hint == QAbstractItemDelegate::NoHint || + hint == QAbstractItemDelegate::RevertModelCache) { + WeightModel *mymodel = qobject_cast<WeightModel *>(currCombo.model); + mymodel->setData(IDX(WeightModel::TYPE), currWeight.type, Qt::EditRole); + mymodel->passInData(IDX(WeightModel::WEIGHT), currWeight.weight); + } +} + +void WSInfoDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &thisindex) const +{ + WeightModel *mymodel = qobject_cast<WeightModel *>(currCombo.model); + WSInfoModel *wsim = WSInfoModel::instance(); + QModelIndexList matches = wsim->match(wsim->index(0, 0), Qt::DisplayRole, currCombo.activeText); + int row; + if (matches.isEmpty()) { + // we need to add this puppy + wsim->insertRows(wsim->rowCount(), 1); + wsim->setData(wsim->index(wsim->rowCount() - 1, 0), currCombo.activeText); + row = wsim->rowCount() - 1; + } else { + row = matches.first().row(); + } + int grams = wsim->data(wsim->index(row, WSInfoModel::GR)).toInt(); + QVariant v = QString(currCombo.activeText); + + mymodel->setData(IDX(WeightModel::TYPE), v, Qt::EditRole); + mymodel->passInData(IDX(WeightModel::WEIGHT), grams); +} + +WSInfoDelegate::WSInfoDelegate(QObject *parent) : ComboBoxDelegate(WSInfoModel::instance(), parent) +{ +} + +QWidget *WSInfoDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + /* First, call the combobox-create editor, it will setup our globals. */ + QWidget *editor = ComboBoxDelegate::createEditor(parent, option, index); + WeightModel *mymodel = qobject_cast<WeightModel *>(currCombo.model); + weightsystem_t *ws = mymodel->weightSystemAt(index); + currWeight.type = copy_string(ws->description); + currWeight.weight = ws->weight.grams; + return editor; +} + +void AirTypesDelegate::revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint) +{ +} + +void AirTypesDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + if (!index.isValid()) + return; + QComboBox *combo = qobject_cast<QComboBox *>(editor); + model->setData(index, QVariant(combo->currentText())); +} + +AirTypesDelegate::AirTypesDelegate(QObject *parent) : ComboBoxDelegate(GasSelectionModel::instance(), parent) +{ +} + +ProfilePrintDelegate::ProfilePrintDelegate(QObject *parent) : QStyledItemDelegate(parent) +{ +} + +static void paintRect(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) +{ + const QRect rect(option.rect); + const int row = index.row(); + const int col = index.column(); + + painter->save(); + // grid color + painter->setPen(QPen(QColor(0xff999999))); + // horizontal lines + if (row == 2 || row == 4 || row == 6) + painter->drawLine(rect.topLeft(), rect.topRight()); + if (row == 7) + painter->drawLine(rect.bottomLeft(), rect.bottomRight()); + // vertical lines + if (row > 1) { + painter->drawLine(rect.topLeft(), rect.bottomLeft()); + if (col == 4 || (col == 0 && row > 5)) + painter->drawLine(rect.topRight(), rect.bottomRight()); + } + painter->restore(); +} + +/* this method overrides the default table drawing method and places grid lines only at certain rows and columns */ +void ProfilePrintDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + paintRect(painter, option, index); + QStyledItemDelegate::paint(painter, option, index); +} + +SpinBoxDelegate::SpinBoxDelegate(int min, int max, int step, QObject *parent): + QStyledItemDelegate(parent), + min(min), + max(max), + step(step) +{ +} + +QWidget *SpinBoxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QSpinBox *w = qobject_cast<QSpinBox*>(QStyledItemDelegate::createEditor(parent, option, index)); + w->setRange(min,max); + w->setSingleStep(step); + return w; +} + +DoubleSpinBoxDelegate::DoubleSpinBoxDelegate(double min, double max, double step, QObject *parent): + QStyledItemDelegate(parent), + min(min), + max(max), + step(step) +{ +} + +QWidget *DoubleSpinBoxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QDoubleSpinBox *w = qobject_cast<QDoubleSpinBox*>(QStyledItemDelegate::createEditor(parent, option, index)); + w->setRange(min,max); + w->setSingleStep(step); + return w; +} + +HTMLDelegate::HTMLDelegate(QObject *parent) : ProfilePrintDelegate(parent) +{ +} + +void HTMLDelegate::paint(QPainter* painter, const QStyleOptionViewItem & option, const QModelIndex &index) const +{ + paintRect(painter, option, index); + QStyleOptionViewItemV4 options = option; + initStyleOption(&options, index); + painter->save(); + QTextDocument doc; + doc.setHtml(options.text); + doc.setTextWidth(options.rect.width()); + doc.setDefaultFont(options.font); + options.text.clear(); + options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter); + painter->translate(options.rect.left(), options.rect.top()); + QRect clip(0, 0, options.rect.width(), options.rect.height()); + doc.drawContents(painter, clip); + painter->restore(); +} + +QSize HTMLDelegate::sizeHint ( const QStyleOptionViewItem & option, const QModelIndex & index ) const +{ + QStyleOptionViewItemV4 options = option; + initStyleOption(&options, index); + QTextDocument doc; + doc.setHtml(options.text); + doc.setTextWidth(options.rect.width()); + return QSize(doc.idealWidth(), doc.size().height()); +} + +LocationFilterDelegate::LocationFilterDelegate(QObject *parent) +{ +} + +void LocationFilterDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &origIdx) const +{ + QFont fontBigger = qApp->font(); + QFont fontSmaller = qApp->font(); + QFontMetrics fmBigger(fontBigger); + QStyleOptionViewItemV4 opt = option; + const QAbstractProxyModel *proxyModel = dynamic_cast<const QAbstractProxyModel*>(origIdx.model()); + QModelIndex index = proxyModel->mapToSource(origIdx); + QStyledItemDelegate::initStyleOption(&opt, index); + QString diveSiteName = index.data().toString(); + QString bottomText; + QIcon icon = index.data(Qt::DecorationRole).value<QIcon>(); + struct dive_site *ds = get_dive_site_by_uuid( + index.model()->data(index.model()->index(index.row(),0)).toInt() + ); + //Special case: do not show name, but instead, show + if (index.row() < 2) { + diveSiteName = index.data().toString(); + bottomText = index.data(Qt::ToolTipRole).toString(); + goto print_part; + } + + if (!ds) + return; + + for (int i = 0; i < 3; i++) { + if (prefs.geocoding.category[i] == TC_NONE) + continue; + int idx = taxonomy_index_for_category(&ds->taxonomy, prefs.geocoding.category[i]); + if (idx == -1) + continue; + if(!bottomText.isEmpty()) + bottomText += " / "; + bottomText += QString(ds->taxonomy.category[idx].value); + } + + if (bottomText.isEmpty()) { + const char *gpsCoords = printGPSCoords(ds->latitude.udeg, ds->longitude.udeg); + bottomText = QString(gpsCoords); + free( (void*) gpsCoords); + } + + if (dive_site_has_gps_location(ds) && dive_site_has_gps_location(&displayed_dive_site)) { + // so we are showing a completion and both the current dive site and the completion + // have a GPS fix... so let's show the distance + if (ds->latitude.udeg == displayed_dive_site.latitude.udeg && + ds->longitude.udeg == displayed_dive_site.longitude.udeg) { + bottomText += tr(" (same GPS fix)"); + } else { + int distanceMeters = get_distance(ds->latitude, ds->longitude, displayed_dive_site.latitude, displayed_dive_site.longitude); + QString distance = distance_string(distanceMeters); + int nr = nr_of_dives_at_dive_site(ds->uuid, false); + bottomText += tr(" (~%1 away").arg(distance); + bottomText += tr(", %n dive(s) here)", "", nr); + } + } + if (bottomText.isEmpty()) { + if (dive_site_has_gps_location(&displayed_dive_site)) + bottomText = tr("(no existing GPS data, add GPS fix from this dive)"); + else + bottomText = tr("(no GPS data)"); + } + bottomText = tr("Pick site: ") + bottomText; + +print_part: + + fontBigger.setPointSize(fontBigger.pointSize() + 1); + fontBigger.setBold(true); + QPen textPen = QPen(option.state & QStyle::State_Selected ? option.palette.highlightedText().color() : option.palette.text().color(), 1); + + initStyleOption(&opt, index); + opt.text = QString(); + opt.icon = QIcon(); + painter->setClipRect(option.rect); + + painter->save(); + if (option.state & QStyle::State_Selected) { + painter->setPen(QPen(opt.palette.highlight().color().darker())); + painter->setBrush(opt.palette.highlight()); + const qreal pad = 1.0; + const qreal pad2 = pad * 2.0; + const qreal rounding = 5.0; + painter->drawRoundedRect(option.rect.x() + pad, + option.rect.y() + pad, + option.rect.width() - pad2, + option.rect.height() - pad2, + rounding, rounding); + } + painter->setPen(textPen); + painter->setFont(fontBigger); + const qreal textPad = 5.0; + painter->drawText(option.rect.x() + textPad, option.rect.y() + fmBigger.boundingRect("YH").height(), diveSiteName); + double pointSize = fontSmaller.pointSizeF(); + fontSmaller.setPointSizeF(0.9 * pointSize); + painter->setFont(fontSmaller); + painter->drawText(option.rect.x() + textPad, option.rect.y() + fmBigger.boundingRect("YH").height() * 2, bottomText); + painter->restore(); + + if (!icon.isNull()) { + painter->save(); + painter->drawPixmap( + option.rect.x() + option.rect.width() - 24, + option.rect.y() + option.rect.height() - 24, icon.pixmap(20,20)); + painter->restore(); + } +} + +QSize LocationFilterDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QFont fontBigger = qApp->font(); + fontBigger.setPointSize(fontBigger.pointSize()); + fontBigger.setBold(true); + + QFontMetrics fmBigger(fontBigger); + + QFont fontSmaller = qApp->font(); + QFontMetrics fmSmaller(fontSmaller); + + QSize retSize = QStyledItemDelegate::sizeHint(option, index); + retSize.setHeight( + fmBigger.boundingRect("Yellow House").height() + 5 /*spacing*/ + + fmSmaller.boundingRect("Yellow House").height()); + + return retSize; +} diff --git a/desktop-widgets/modeldelegates.h b/desktop-widgets/modeldelegates.h new file mode 100644 index 000000000..95701775a --- /dev/null +++ b/desktop-widgets/modeldelegates.h @@ -0,0 +1,141 @@ +#ifndef MODELDELEGATES_H +#define MODELDELEGATES_H + +#include <QStyledItemDelegate> +#include <QComboBox> +class QPainter; + +class DiveListDelegate : public QStyledItemDelegate { +public: + explicit DiveListDelegate(QObject *parent = 0) + : QStyledItemDelegate(parent) + { + } + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; +}; + +class StarWidgetsDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + explicit StarWidgetsDelegate(QWidget *parent = 0); + virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; + virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; + const QSize& starSize() const; + +private: + QWidget *parentWidget; + QSize minStarSize; +}; + +class ComboBoxDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + explicit ComboBoxDelegate(QAbstractItemModel *model, QObject *parent = 0); + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; + virtual void setEditorData(QWidget *editor, const QModelIndex &index) const; + virtual void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const; + virtual bool eventFilter(QObject *object, QEvent *event); +public +slots: + void testActivation(const QString &currString = QString()); + void testActivation(const QModelIndex &currIndex); + //HACK: try to get rid of this in the future. + void fakeActivation(); + void fixTabBehavior(); + virtual void revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint) = 0; + +protected: + QAbstractItemModel *model; +}; + +class TankInfoDelegate : public ComboBoxDelegate { + Q_OBJECT +public: + explicit TankInfoDelegate(QObject *parent = 0); + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; +public +slots: + void revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint); + void reenableReplot(QWidget *widget, QAbstractItemDelegate::EndEditHint hint); +}; + +class TankUseDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + explicit TankUseDelegate(QObject *parent = 0); + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; + virtual void setEditorData(QWidget * editor, const QModelIndex & index) const; +}; + +class WSInfoDelegate : public ComboBoxDelegate { + Q_OBJECT +public: + explicit WSInfoDelegate(QObject *parent = 0); + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; +public +slots: + void revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint); +}; + +class AirTypesDelegate : public ComboBoxDelegate { + Q_OBJECT +public: + explicit AirTypesDelegate(QObject *parent = 0); + virtual void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; +public +slots: + void revertModelData(QWidget *widget, QAbstractItemDelegate::EndEditHint hint); +}; + +/* ProfilePrintDelagate: + * this delegate is used to modify the look of the table that is printed + * bellow profiles. + */ +class ProfilePrintDelegate : public QStyledItemDelegate { +public: + explicit ProfilePrintDelegate(QObject *parent = 0); + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; +}; + +class SpinBoxDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + SpinBoxDelegate(int min, int max, int step, QObject *parent = 0); + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; +private: + int min; + int max; + int step; +}; + +class DoubleSpinBoxDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + DoubleSpinBoxDelegate(double min, double max, double step, QObject *parent = 0); + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; +private: + double min; + double max; + double step; +}; + +class HTMLDelegate : public ProfilePrintDelegate { + Q_OBJECT +public: + explicit HTMLDelegate(QObject *parent = 0); + virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; + virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; +}; + +class LocationFilterDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + LocationFilterDelegate(QObject *parent = 0); + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE; +}; + +#endif // MODELDELEGATES_H diff --git a/desktop-widgets/notificationwidget.cpp b/desktop-widgets/notificationwidget.cpp new file mode 100644 index 000000000..103c0d068 --- /dev/null +++ b/desktop-widgets/notificationwidget.cpp @@ -0,0 +1,42 @@ +#include "notificationwidget.h" + +NotificationWidget::NotificationWidget(QWidget *parent) : KMessageWidget(parent) +{ + future_watcher = new QFutureWatcher<void>(); + connect(future_watcher, SIGNAL(finished()), this, SLOT(finish())); +} + +void NotificationWidget::showNotification(QString message, KMessageWidget::MessageType type) +{ + if (message.isEmpty()) + return; + setText(message); + setCloseButtonVisible(true); + setMessageType(type); + animatedShow(); +} + +void NotificationWidget::hideNotification() +{ + animatedHide(); +} + +QString NotificationWidget::getNotificationText() +{ + return text(); +} + +void NotificationWidget::setFuture(const QFuture<void> &future) +{ + future_watcher->setFuture(future); +} + +void NotificationWidget::finish() +{ + hideNotification(); +} + +NotificationWidget::~NotificationWidget() +{ + delete future_watcher; +} diff --git a/desktop-widgets/notificationwidget.h b/desktop-widgets/notificationwidget.h new file mode 100644 index 000000000..8a551a0b3 --- /dev/null +++ b/desktop-widgets/notificationwidget.h @@ -0,0 +1,32 @@ +#ifndef NOTIFICATIONWIDGET_H +#define NOTIFICATIONWIDGET_H + +#include <QWidget> +#include <QFutureWatcher> + +#include <kmessagewidget.h> + +namespace Ui { + class NotificationWidget; +} + +class NotificationWidget : public KMessageWidget { + Q_OBJECT + +public: + explicit NotificationWidget(QWidget *parent = 0); + void setFuture(const QFuture<void> &future); + void showNotification(QString message, KMessageWidget::MessageType type); + void hideNotification(); + QString getNotificationText(); + ~NotificationWidget(); + +private: + QFutureWatcher<void> *future_watcher; + +private +slots: + void finish(); +}; + +#endif // NOTIFICATIONWIDGET_H diff --git a/desktop-widgets/plannerDetails.ui b/desktop-widgets/plannerDetails.ui new file mode 100644 index 000000000..1f2790d85 --- /dev/null +++ b/desktop-widgets/plannerDetails.ui @@ -0,0 +1,104 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>plannerDetails</class> + <widget class="QWidget" name="plannerDetails"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="spacing"> + <number>5</number> + </property> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="topMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>5</number> + </property> + <property name="bottomMargin"> + <number>5</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="divePlanOutputLabel"> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>20</height> + </size> + </property> + <property name="text"> + <string><html><head/><body><p><span style=" font-weight:600;">Dive plan details</span></p></body></html></string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="printPlan"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Print</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + <property name="default"> + <bool>false</bool> + </property> + <property name="flat"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QTextEdit" name="divePlanOutput"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="styleSheet"> + <string notr="true">font: 13pt "Courier";</string> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="html"> + <string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Courier'; font-size:13pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'.Curier New';"><br /></p></body></html></string> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/desktop-widgets/plannerSettings.ui b/desktop-widgets/plannerSettings.ui new file mode 100644 index 000000000..4db69f883 --- /dev/null +++ b/desktop-widgets/plannerSettings.ui @@ -0,0 +1,749 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>plannerSettingsWidget</class> + <widget class="QWidget" name="plannerSettingsWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>1102</width> + <height>442</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="topMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>5</number> + </property> + <property name="bottomMargin"> + <number>5</number> + </property> + <item> + <widget class="QScrollArea" name="scrollArea"> + <property name="frameShape"> + <enum>QFrame::NoFrame</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Plain</enum> + </property> + <property name="widgetResizable"> + <bool>true</bool> + </property> + <widget class="QWidget" name="scrollAreaWidgetContents"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>1092</width> + <height>432</height> + </rect> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QGroupBox" name="Rates"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Rates</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <property name="leftMargin"> + <number>2</number> + </property> + <property name="topMargin"> + <number>2</number> + </property> + <property name="rightMargin"> + <number>2</number> + </property> + <property name="bottomMargin"> + <number>2</number> + </property> + <property name="spacing"> + <number>2</number> + </property> + <item row="0" column="0"> + <widget class="QGroupBox" name="Ascent"> + <property name="title"> + <string>Ascent</string> + </property> + <layout class="QGridLayout" name="gridLayout_3"> + <property name="leftMargin"> + <number>12</number> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="ascBelow75"> + <property name="text"> + <string>below 75% avg. depth</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QSpinBox" name="ascRate75"> + <property name="suffix"> + <string>m/min</string> + </property> + <property name="minimum"> + <number>1</number> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="asc75to50"> + <property name="text"> + <string>75% to 50% avg. depth</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QSpinBox" name="ascRate50"> + <property name="suffix"> + <string>m/min</string> + </property> + <property name="minimum"> + <number>1</number> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="asc50to6"> + <property name="text"> + <string>50% avg. depth to 6m</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QSpinBox" name="ascRateStops"> + <property name="suffix"> + <string>m/min</string> + </property> + <property name="minimum"> + <number>1</number> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="asc6toSurf"> + <property name="text"> + <string>6m to surface</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QSpinBox" name="ascRateLast6m"> + <property name="suffix"> + <string>m/min</string> + </property> + <property name="minimum"> + <number>1</number> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="1" column="0"> + <widget class="QGroupBox" name="Descent"> + <property name="title"> + <string>Descent</string> + </property> + <layout class="QGridLayout" name="gridLayout_4"> + <item row="0" column="0"> + <widget class="QLabel" name="descent"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>surface to the bottom</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QSpinBox" name="descRate"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>16777215</height> + </size> + </property> + <property name="suffix"> + <string>m/min</string> + </property> + <property name="minimum"> + <number>1</number> + </property> + <property name="value"> + <number>18</number> + </property> + </widget> + </item> + </layout> + <zorder>descRate</zorder> + <zorder>descent</zorder> + </widget> + </item> + <item row="2" column="0"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="Planning"> + <property name="title"> + <string>Planning</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <property name="leftMargin"> + <number>2</number> + </property> + <property name="topMargin"> + <number>2</number> + </property> + <property name="rightMargin"> + <number>2</number> + </property> + <property name="bottomMargin"> + <number>2</number> + </property> + <property name="spacing"> + <number>2</number> + </property> + <item row="11" column="1"> + <widget class="QRadioButton" name="vpmb_deco"> + <property name="text"> + <string>VPM-B deco</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QRadioButton" name="buehlmann_deco"> + <property name="text"> + <string>Bühlmann deco</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Reserve gas</string> + </property> + <property name="indent"> + <number>26</number> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QSpinBox" name="reserve_gas"> + <property name="suffix"> + <string>bar</string> + </property> + <property name="prefix"> + <string/> + </property> + <property name="minimum"> + <number>10</number> + </property> + <property name="maximum"> + <number>99</number> + </property> + <property name="value"> + <number>40</number> + </property> + </widget> + </item> + <item row="9" column="2"> + <widget class="QSpinBox" name="gfhigh"> + <property name="suffix"> + <string>%</string> + </property> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>150</number> + </property> + </widget> + </item> + <item row="17" column="1" colspan="2"> + <widget class="QCheckBox" name="switch_at_req_stop"> + <property name="toolTip"> + <string>Postpone gas change if a stop is not required</string> + </property> + <property name="text"> + <string>Only switch at required stops</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <spacer name="verticalSpacer_6"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="8" column="1"> + <widget class="QLabel" name="label_15"> + <property name="text"> + <string>GF low</string> + </property> + <property name="indent"> + <number>26</number> + </property> + </widget> + </item> + <item row="16" column="1" colspan="2"> + <widget class="QCheckBox" name="backgasBreaks"> + <property name="text"> + <string>Plan backgas breaks</string> + </property> + </widget> + </item> + <item row="9" column="1"> + <widget class="QLabel" name="label_16"> + <property name="text"> + <string>GF high</string> + </property> + <property name="indent"> + <number>25</number> + </property> + </widget> + </item> + <item row="18" column="2"> + <widget class="QSpinBox" name="min_switch_duration"> + <property name="suffix"> + <string>min</string> + </property> + <property name="prefix"> + <string/> + </property> + <property name="minimum"> + <number>0</number> + </property> + <property name="maximum"> + <number>9</number> + </property> + <property name="value"> + <number>1</number> + </property> + </widget> + </item> + <item row="15" column="1" colspan="2"> + <widget class="QCheckBox" name="lastStop"> + <property name="text"> + <string>Last stop at 6m</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QRadioButton" name="recreational_deco"> + <property name="toolTip"> + <string>Maximize bottom time allowed by gas and no decompression limits</string> + </property> + <property name="text"> + <string>Recreational mode</string> + </property> + </widget> + </item> + <item row="19" column="1"> + <widget class="QComboBox" name="rebreathermode"> + <property name="currentText" stdset="0"> + <string/> + </property> + <property name="maxVisibleItems"> + <number>6</number> + </property> + </widget> + </item> + <item row="8" column="2"> + <widget class="QSpinBox" name="gflow"> + <property name="suffix"> + <string>%</string> + </property> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>150</number> + </property> + </widget> + </item> + <item row="14" column="1" colspan="2"> + <widget class="QCheckBox" name="drop_stone_mode"> + <property name="text"> + <string>Drop to first depth</string> + </property> + </widget> + </item> + <item row="18" column="1"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Min. switch duration</string> + </property> + </widget> + </item> + <item row="20" column="1"> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="1" alignment="Qt::AlignHCenter"> + <widget class="QCheckBox" name="safetystop"> + <property name="layoutDirection"> + <enum>Qt::LeftToRight</enum> + </property> + <property name="text"> + <string>Safety stop</string> + </property> + <property name="tristate"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="10" column="1"> + <spacer name="verticalSpacer_7"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="13" column="1"> + <spacer name="verticalSpacer_5"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="12" column="1"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Conservatism level</string> + </property> + <property name="indent"> + <number>25</number> + </property> + </widget> + </item> + <item row="12" column="2"> + <widget class="QSpinBox" name="conservatism_lvl"> + <property name="maximum"> + <number>4</number> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="Gas"> + <property name="title"> + <string>Gas options</string> + </property> + <layout class="QGridLayout" name="gridLayout_5"> + <property name="leftMargin"> + <number>2</number> + </property> + <property name="topMargin"> + <number>2</number> + </property> + <property name="rightMargin"> + <number>2</number> + </property> + <property name="bottomMargin"> + <number>2</number> + </property> + <property name="spacing"> + <number>2</number> + </property> + <item row="4" column="0"> + <spacer name="verticalSpacer_3"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="1"> + <widget class="QDoubleSpinBox" name="bottompo2"> + <property name="suffix"> + <string>bar</string> + </property> + <property name="maximum"> + <double>2.000000000000000</double> + </property> + <property name="singleStep"> + <double>0.100000000000000</double> + </property> + <property name="value"> + <double>1.400000000000000</double> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QDoubleSpinBox" name="bottomSAC"> + <property name="suffix"> + <string>â„“/min</string> + </property> + <property name="decimals"> + <number>0</number> + </property> + <property name="maximum"> + <double>99.000000000000000</double> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QDoubleSpinBox" name="decopo2"> + <property name="suffix"> + <string>bar</string> + </property> + <property name="maximum"> + <double>2.000000000000000</double> + </property> + <property name="singleStep"> + <double>0.100000000000000</double> + </property> + <property name="value"> + <double>1.600000000000000</double> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="bottomsac"> + <property name="text"> + <string>Bottom SAC</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="bottompO2"> + <property name="text"> + <string>Bottom pOâ‚‚</string> + </property> + </widget> + </item> + <item row="5" column="0" colspan="2"> + <widget class="QGroupBox" name="Notes"> + <property name="title"> + <string>Notes</string> + </property> + <layout class="QGridLayout" name="gridLayout_6"> + <property name="leftMargin"> + <number>2</number> + </property> + <property name="topMargin"> + <number>2</number> + </property> + <property name="rightMargin"> + <number>2</number> + </property> + <property name="bottomMargin"> + <number>2</number> + </property> + <property name="spacing"> + <number>2</number> + </property> + <item row="0" column="0"> + <widget class="QCheckBox" name="display_runtime"> + <property name="toolTip"> + <string>In dive plan, show runtime (absolute time) of stops</string> + </property> + <property name="text"> + <string>Display runtime</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QCheckBox" name="display_duration"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="toolTip"> + <string>In dive plan, show duration (relative time) of stops</string> + </property> + <property name="text"> + <string>Display segment duration</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QCheckBox" name="display_transitions"> + <property name="toolTip"> + <string>In diveplan, list transitions or treat them as implicit</string> + </property> + <property name="text"> + <string>Display transitions in deco</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QCheckBox" name="verbatim_plan"> + <property name="text"> + <string>Verbatim dive plan</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="1" column="1"> + <widget class="QDoubleSpinBox" name="decoStopSAC"> + <property name="suffix"> + <string>â„“/min</string> + </property> + <property name="decimals"> + <number>0</number> + </property> + <property name="maximum"> + <double>99.000000000000000</double> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="decopO2"> + <property name="text"> + <string>Deco pOâ‚‚</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="decosac"> + <property name="text"> + <string>Deco SAC</string> + </property> + </widget> + </item> + <item row="6" column="0"> + <spacer name="verticalSpacer_4"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>scrollArea</tabstop> + <tabstop>ascRate75</tabstop> + <tabstop>ascRate50</tabstop> + <tabstop>ascRateStops</tabstop> + <tabstop>ascRateLast6m</tabstop> + <tabstop>descRate</tabstop> + <tabstop>recreational_deco</tabstop> + <tabstop>reserve_gas</tabstop> + <tabstop>safetystop</tabstop> + <tabstop>buehlmann_deco</tabstop> + <tabstop>gflow</tabstop> + <tabstop>gfhigh</tabstop> + <tabstop>vpmb_deco</tabstop> + <tabstop>drop_stone_mode</tabstop> + <tabstop>lastStop</tabstop> + <tabstop>backgasBreaks</tabstop> + <tabstop>switch_at_req_stop</tabstop> + <tabstop>min_switch_duration</tabstop> + <tabstop>rebreathermode</tabstop> + <tabstop>bottomSAC</tabstop> + <tabstop>decoStopSAC</tabstop> + <tabstop>bottompo2</tabstop> + <tabstop>decopo2</tabstop> + <tabstop>display_runtime</tabstop> + <tabstop>display_duration</tabstop> + <tabstop>display_transitions</tabstop> + <tabstop>verbatim_plan</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/desktop-widgets/preferences.cpp b/desktop-widgets/preferences.cpp new file mode 100644 index 000000000..6450c41cb --- /dev/null +++ b/desktop-widgets/preferences.cpp @@ -0,0 +1,559 @@ +#include "preferences.h" +#include "mainwindow.h" +#include "models.h" +#include "divelocationmodel.h" +#include "prefs-macros.h" +#include "qthelper.h" +#include "subsurfacestartup.h" + +#include <QSettings> +#include <QFileDialog> +#include <QMessageBox> +#include <QShortcut> +#include <QNetworkProxy> +#include <QNetworkCookieJar> + +#include "subsurfacewebservices.h" + +#if !defined(Q_OS_ANDROID) && defined(FBSUPPORT) +#include "socialnetworks.h" +#include <QWebView> +#endif + +PreferencesDialog *PreferencesDialog::instance() +{ + static PreferencesDialog *dialog = new PreferencesDialog(MainWindow::instance()); + return dialog; +} + +PreferencesDialog::PreferencesDialog(QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f) +{ + ui.setupUi(this); + setAttribute(Qt::WA_QuitOnClose, false); + +#if defined(Q_OS_ANDROID) || !defined(FBSUPPORT) + for (int i = 0; i < ui.listWidget->count(); i++) { + if (ui.listWidget->item(i)->text() == "Facebook") { + delete ui.listWidget->item(i); + QWidget *fbpage = ui.stackedWidget->widget(i); + ui.stackedWidget->removeWidget(fbpage); + } + } +#endif + + ui.proxyType->clear(); + ui.proxyType->addItem(tr("No proxy"), QNetworkProxy::NoProxy); + ui.proxyType->addItem(tr("System proxy"), QNetworkProxy::DefaultProxy); + ui.proxyType->addItem(tr("HTTP proxy"), QNetworkProxy::HttpProxy); + ui.proxyType->addItem(tr("SOCKS proxy"), QNetworkProxy::Socks5Proxy); + ui.proxyType->setCurrentIndex(-1); + + ui.first_item->setModel(GeoReferencingOptionsModel::instance()); + ui.second_item->setModel(GeoReferencingOptionsModel::instance()); + ui.third_item->setModel(GeoReferencingOptionsModel::instance()); + // Facebook stuff: +#if !defined(Q_OS_ANDROID) && defined(FBSUPPORT) + FacebookManager *fb = FacebookManager::instance(); + facebookWebView = new QWebView(this); + ui.fbWebviewContainer->layout()->addWidget(facebookWebView); + if (fb->loggedIn()) { + facebookLoggedIn(); + } else { + facebookDisconnect(); + } + connect(facebookWebView, &QWebView::urlChanged, fb, &FacebookManager::tryLogin); + connect(fb, &FacebookManager::justLoggedIn, this, &PreferencesDialog::facebookLoggedIn); + connect(ui.fbDisconnect, &QPushButton::clicked, fb, &FacebookManager::logout); + connect(fb, &FacebookManager::justLoggedOut, this, &PreferencesDialog::facebookDisconnect); +#endif + connect(ui.proxyType, SIGNAL(currentIndexChanged(int)), this, SLOT(proxyType_changed(int))); + connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); + connect(ui.gflow, SIGNAL(valueChanged(int)), this, SLOT(gflowChanged(int))); + connect(ui.gfhigh, SIGNAL(valueChanged(int)), this, SLOT(gfhighChanged(int))); + // connect(ui.defaultSetpoint, SIGNAL(valueChanged(double)), this, SLOT(defaultSetpointChanged(double))); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); + loadSettings(); + setUiFromPrefs(); + rememberPrefs(); +} + +void PreferencesDialog::facebookLoggedIn() +{ +#if !defined(Q_OS_ANDROID) && defined(FBSUPPORT) + ui.fbDisconnect->show(); + ui.fbWebviewContainer->hide(); + ui.fbWebviewContainer->setEnabled(false); + ui.FBLabel->setText(tr("To disconnect Subsurface from your Facebook account, use the button below")); +#endif +} + +void PreferencesDialog::facebookDisconnect() +{ +#if !defined(Q_OS_ANDROID) && defined(FBSUPPORT) + // remove the connect/disconnect button + // and instead add the login view + ui.fbDisconnect->hide(); + ui.fbWebviewContainer->show(); + ui.fbWebviewContainer->setEnabled(true); + ui.FBLabel->setText(tr("To connect to Facebook, please log in. This enables Subsurface to publish dives to your timeline")); + if (facebookWebView) { + facebookWebView->page()->networkAccessManager()->setCookieJar(new QNetworkCookieJar()); + facebookWebView->setUrl(FacebookManager::instance()->connectUrl()); + } +#endif +} + +void PreferencesDialog::cloudPinNeeded() +{ + ui.cloud_storage_pin->setEnabled(prefs.cloud_verification_status == CS_NEED_TO_VERIFY); + ui.cloud_storage_pin->setVisible(prefs.cloud_verification_status == CS_NEED_TO_VERIFY); + ui.cloud_storage_pin_label->setEnabled(prefs.cloud_verification_status == CS_NEED_TO_VERIFY); + ui.cloud_storage_pin_label->setVisible(prefs.cloud_verification_status == CS_NEED_TO_VERIFY); + ui.cloud_storage_new_passwd->setEnabled(prefs.cloud_verification_status == CS_VERIFIED); + ui.cloud_storage_new_passwd->setVisible(prefs.cloud_verification_status == CS_VERIFIED); + ui.cloud_storage_new_passwd_label->setEnabled(prefs.cloud_verification_status == CS_VERIFIED); + ui.cloud_storage_new_passwd_label->setVisible(prefs.cloud_verification_status == CS_VERIFIED); + if (prefs.cloud_verification_status == CS_VERIFIED) { + ui.cloudStorageGroupBox->setTitle(tr("Subsurface cloud storage (credentials verified)")); + ui.cloudDefaultFile->setEnabled(true); + } else { + ui.cloudStorageGroupBox->setTitle(tr("Subsurface cloud storage")); + if (ui.cloudDefaultFile->isChecked()) + ui.noDefaultFile->setChecked(true); + ui.cloudDefaultFile->setEnabled(false); + } + MainWindow::instance()->enableDisableCloudActions(); +} + +#define DANGER_GF (gf > 100) ? "* { color: red; }" : "" +void PreferencesDialog::gflowChanged(int gf) +{ + ui.gflow->setStyleSheet(DANGER_GF); +} +void PreferencesDialog::gfhighChanged(int gf) +{ + ui.gfhigh->setStyleSheet(DANGER_GF); +} +#undef DANGER_GF + +void PreferencesDialog::showEvent(QShowEvent *event) +{ + setUiFromPrefs(); + rememberPrefs(); + QDialog::showEvent(event); +} + +void PreferencesDialog::setUiFromPrefs() +{ + // graphs + ui.pheThreshold->setValue(prefs.pp_graphs.phe_threshold); + ui.po2Threshold->setValue(prefs.pp_graphs.po2_threshold); + ui.pn2Threshold->setValue(prefs.pp_graphs.pn2_threshold); + ui.maxpo2->setValue(prefs.modpO2); + ui.red_ceiling->setChecked(prefs.redceiling); + ui.units_group->setEnabled(ui.personalize->isChecked()); + + ui.gflow->setValue(prefs.gflow); + ui.gfhigh->setValue(prefs.gfhigh); + ui.gf_low_at_maxdepth->setChecked(prefs.gf_low_at_maxdepth); + ui.show_ccr_setpoint->setChecked(prefs.show_ccr_setpoint); + ui.show_ccr_sensors->setChecked(prefs.show_ccr_sensors); + ui.defaultSetpoint->setValue((double)prefs.defaultsetpoint / 1000.0); + ui.psro2rate->setValue(prefs.o2consumption / 1000.0); + ui.pscrfactor->setValue(rint(1000.0 / prefs.pscr_ratio)); + + // units + if (prefs.unit_system == METRIC) + ui.metric->setChecked(true); + else if (prefs.unit_system == IMPERIAL) + ui.imperial->setChecked(true); + else + ui.personalize->setChecked(true); + ui.gpsTraditional->setChecked(prefs.coordinates_traditional); + ui.gpsDecimal->setChecked(!prefs.coordinates_traditional); + + ui.celsius->setChecked(prefs.units.temperature == units::CELSIUS); + ui.fahrenheit->setChecked(prefs.units.temperature == units::FAHRENHEIT); + ui.meter->setChecked(prefs.units.length == units::METERS); + ui.feet->setChecked(prefs.units.length == units::FEET); + ui.bar->setChecked(prefs.units.pressure == units::BAR); + ui.psi->setChecked(prefs.units.pressure == units::PSI); + ui.liter->setChecked(prefs.units.volume == units::LITER); + ui.cuft->setChecked(prefs.units.volume == units::CUFT); + ui.kg->setChecked(prefs.units.weight == units::KG); + ui.lbs->setChecked(prefs.units.weight == units::LBS); + + ui.font->setCurrentFont(QString(prefs.divelist_font)); + ui.fontsize->setValue(prefs.font_size); + ui.defaultfilename->setText(prefs.default_filename); + ui.noDefaultFile->setChecked(prefs.default_file_behavior == NO_DEFAULT_FILE); + ui.cloudDefaultFile->setChecked(prefs.default_file_behavior == CLOUD_DEFAULT_FILE); + ui.localDefaultFile->setChecked(prefs.default_file_behavior == LOCAL_DEFAULT_FILE); + ui.default_cylinder->clear(); + for (int i = 0; tank_info[i].name != NULL; i++) { + ui.default_cylinder->addItem(tank_info[i].name); + if (prefs.default_cylinder && strcmp(tank_info[i].name, prefs.default_cylinder) == 0) + ui.default_cylinder->setCurrentIndex(i); + } + ui.displayinvalid->setChecked(prefs.display_invalid_dives); + ui.display_unused_tanks->setChecked(prefs.display_unused_tanks); + ui.show_average_depth->setChecked(prefs.show_average_depth); + ui.vertical_speed_minutes->setChecked(prefs.units.vertical_speed_time == units::MINUTES); + ui.vertical_speed_seconds->setChecked(prefs.units.vertical_speed_time == units::SECONDS); + ui.velocitySlider->setValue(prefs.animation_speed); + + QSortFilterProxyModel *filterModel = new QSortFilterProxyModel(); + filterModel->setSourceModel(LanguageModel::instance()); + filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + ui.languageView->setModel(filterModel); + filterModel->sort(0); + connect(ui.languageFilter, SIGNAL(textChanged(QString)), filterModel, SLOT(setFilterFixedString(QString))); + + QSettings s; + + ui.save_uid_local->setChecked(s.value("save_uid_local").toBool()); + ui.default_uid->setText(s.value("subsurface_webservice_uid").toString().toUpper()); + + s.beginGroup("Language"); + ui.languageSystemDefault->setChecked(s.value("UseSystemLanguage", true).toBool()); + QAbstractItemModel *m = ui.languageView->model(); + QModelIndexList languages = m->match(m->index(0, 0), Qt::UserRole, s.value("UiLanguage").toString()); + if (languages.count()) + ui.languageView->setCurrentIndex(languages.first()); + + s.endGroup(); + + ui.proxyHost->setText(prefs.proxy_host); + ui.proxyPort->setValue(prefs.proxy_port); + ui.proxyAuthRequired->setChecked(prefs.proxy_auth); + ui.proxyUsername->setText(prefs.proxy_user); + ui.proxyPassword->setText(prefs.proxy_pass); + ui.proxyType->setCurrentIndex(ui.proxyType->findData(prefs.proxy_type)); + ui.btnUseDefaultFile->setChecked(prefs.use_default_file); + + ui.cloud_storage_email->setText(prefs.cloud_storage_email); + ui.cloud_storage_password->setText(prefs.cloud_storage_password); + ui.save_password_local->setChecked(prefs.save_password_local); + cloudPinNeeded(); + ui.cloud_background_sync->setChecked(prefs.cloud_background_sync); + ui.default_uid->setText(prefs.userid); + + // GeoManagement +#ifdef DISABLED + ui.enable_geocoding->setChecked( prefs.geocoding.enable_geocoding ); + ui.parse_without_gps->setChecked(prefs.geocoding.parse_dive_without_gps); + ui.tag_existing_dives->setChecked(prefs.geocoding.tag_existing_dives); +#endif + ui.first_item->setCurrentIndex(prefs.geocoding.category[0]); + ui.second_item->setCurrentIndex(prefs.geocoding.category[1]); + ui.third_item->setCurrentIndex(prefs.geocoding.category[2]); +} + +void PreferencesDialog::restorePrefs() +{ + prefs = oldPrefs; + setUiFromPrefs(); +} + +void PreferencesDialog::rememberPrefs() +{ + oldPrefs = prefs; +} + +void PreferencesDialog::syncSettings() +{ + QSettings s; + + s.setValue("subsurface_webservice_uid", ui.default_uid->text().toUpper()); + set_save_userid_local(ui.save_uid_local->checkState()); + + // Graph + s.beginGroup("TecDetails"); + SAVE_OR_REMOVE("phethreshold", default_prefs.pp_graphs.phe_threshold, ui.pheThreshold->value()); + SAVE_OR_REMOVE("po2threshold", default_prefs.pp_graphs.po2_threshold, ui.po2Threshold->value()); + SAVE_OR_REMOVE("pn2threshold", default_prefs.pp_graphs.pn2_threshold, ui.pn2Threshold->value()); + SAVE_OR_REMOVE("modpO2", default_prefs.modpO2, ui.maxpo2->value()); + SAVE_OR_REMOVE("redceiling", default_prefs.redceiling, ui.red_ceiling->isChecked()); + SAVE_OR_REMOVE("gflow", default_prefs.gflow, ui.gflow->value()); + SAVE_OR_REMOVE("gfhigh", default_prefs.gfhigh, ui.gfhigh->value()); + SAVE_OR_REMOVE("gf_low_at_maxdepth", default_prefs.gf_low_at_maxdepth, ui.gf_low_at_maxdepth->isChecked()); + SAVE_OR_REMOVE("show_ccr_setpoint", default_prefs.show_ccr_setpoint, ui.show_ccr_setpoint->isChecked()); + SAVE_OR_REMOVE("show_ccr_sensors", default_prefs.show_ccr_sensors, ui.show_ccr_sensors->isChecked()); + SAVE_OR_REMOVE("display_unused_tanks", default_prefs.display_unused_tanks, ui.display_unused_tanks->isChecked()); + SAVE_OR_REMOVE("show_average_depth", default_prefs.show_average_depth, ui.show_average_depth->isChecked()); + s.endGroup(); + + // Units + s.beginGroup("Units"); + QString unitSystem[] = {"metric", "imperial", "personal"}; + short unitValue = ui.metric->isChecked() ? METRIC : (ui.imperial->isChecked() ? IMPERIAL : PERSONALIZE); + SAVE_OR_REMOVE_SPECIAL("unit_system", default_prefs.unit_system, unitValue, unitSystem[unitValue]); + s.setValue("temperature", ui.fahrenheit->isChecked() ? units::FAHRENHEIT : units::CELSIUS); + s.setValue("length", ui.feet->isChecked() ? units::FEET : units::METERS); + s.setValue("pressure", ui.psi->isChecked() ? units::PSI : units::BAR); + s.setValue("volume", ui.cuft->isChecked() ? units::CUFT : units::LITER); + s.setValue("weight", ui.lbs->isChecked() ? units::LBS : units::KG); + s.setValue("vertical_speed_time", ui.vertical_speed_minutes->isChecked() ? units::MINUTES : units::SECONDS); + s.setValue("coordinates", ui.gpsTraditional->isChecked()); + s.endGroup(); + + // Defaults + s.beginGroup("GeneralSettings"); + s.setValue("default_filename", ui.defaultfilename->text()); + s.setValue("default_cylinder", ui.default_cylinder->currentText()); + s.setValue("use_default_file", ui.btnUseDefaultFile->isChecked()); + if (ui.noDefaultFile->isChecked()) + s.setValue("default_file_behavior", NO_DEFAULT_FILE); + else if (ui.localDefaultFile->isChecked()) + s.setValue("default_file_behavior", LOCAL_DEFAULT_FILE); + else if (ui.cloudDefaultFile->isChecked()) + s.setValue("default_file_behavior", CLOUD_DEFAULT_FILE); + s.setValue("defaultsetpoint", rint(ui.defaultSetpoint->value() * 1000.0)); + s.setValue("o2consumption", rint(ui.psro2rate->value() *1000.0)); + s.setValue("pscr_ratio", rint(1000.0 / ui.pscrfactor->value())); + s.endGroup(); + + s.beginGroup("Display"); + SAVE_OR_REMOVE_SPECIAL("divelist_font", system_divelist_default_font, ui.font->currentFont().toString(), ui.font->currentFont()); + SAVE_OR_REMOVE("font_size", system_divelist_default_font_size, ui.fontsize->value()); + s.setValue("displayinvalid", ui.displayinvalid->isChecked()); + s.endGroup(); + s.sync(); + + // Locale + QLocale loc; + s.beginGroup("Language"); + bool useSystemLang = s.value("UseSystemLanguage", true).toBool(); + if (useSystemLang != ui.languageSystemDefault->isChecked() || + (!useSystemLang && s.value("UiLanguage").toString() != ui.languageView->currentIndex().data(Qt::UserRole))) { + QMessageBox::warning(MainWindow::instance(), tr("Restart required"), + tr("To correctly load a new language you must restart Subsurface.")); + } + s.setValue("UseSystemLanguage", ui.languageSystemDefault->isChecked()); + s.setValue("UiLanguage", ui.languageView->currentIndex().data(Qt::UserRole)); + s.endGroup(); + + // Animation + s.beginGroup("Animations"); + s.setValue("animation_speed", ui.velocitySlider->value()); + s.endGroup(); + + s.beginGroup("Network"); + s.setValue("proxy_type", ui.proxyType->itemData(ui.proxyType->currentIndex()).toInt()); + s.setValue("proxy_host", ui.proxyHost->text()); + s.setValue("proxy_port", ui.proxyPort->value()); + SB("proxy_auth", ui.proxyAuthRequired); + s.setValue("proxy_user", ui.proxyUsername->text()); + s.setValue("proxy_pass", ui.proxyPassword->text()); + s.endGroup(); + + s.beginGroup("CloudStorage"); + QString email = ui.cloud_storage_email->text(); + QString password = ui.cloud_storage_password->text(); + QString newpassword = ui.cloud_storage_new_passwd->text(); + if (prefs.cloud_verification_status == CS_VERIFIED && !newpassword.isEmpty()) { + // deal with password change + if (!email.isEmpty() && !password.isEmpty()) { + // connect to backend server to check / create credentials + QRegularExpression reg("^[a-zA-Z0-9@.+_-]+$"); + if (!reg.match(email).hasMatch() || (!password.isEmpty() && !reg.match(password).hasMatch())) { + report_error(qPrintable(tr("Cloud storage email and password can only consist of letters, numbers, and '.', '-', '_', and '+'."))); + } else { + CloudStorageAuthenticate *cloudAuth = new CloudStorageAuthenticate(this); + connect(cloudAuth, SIGNAL(finishedAuthenticate()), this, SLOT(cloudPinNeeded())); + connect(cloudAuth, SIGNAL(passwordChangeSuccessful()), this, SLOT(passwordUpdateSuccessfull())); + QNetworkReply *reply = cloudAuth->backend(email, password, "", newpassword); + ui.cloud_storage_new_passwd->setText(""); + free(prefs.cloud_storage_newpassword); + prefs.cloud_storage_newpassword = strdup(qPrintable(newpassword)); + } + } + } else if (prefs.cloud_verification_status == CS_UNKNOWN || + prefs.cloud_verification_status == CS_INCORRECT_USER_PASSWD || + email != prefs.cloud_storage_email || + password != prefs.cloud_storage_password) { + + // different credentials - reset verification status + prefs.cloud_verification_status = CS_UNKNOWN; + if (!email.isEmpty() && !password.isEmpty()) { + // connect to backend server to check / create credentials + QRegularExpression reg("^[a-zA-Z0-9@.+_-]+$"); + if (!reg.match(email).hasMatch() || (!password.isEmpty() && !reg.match(password).hasMatch())) { + report_error(qPrintable(tr("Cloud storage email and password can only consist of letters, numbers, and '.', '-', '_', and '+'."))); + } else { + CloudStorageAuthenticate *cloudAuth = new CloudStorageAuthenticate(this); + connect(cloudAuth, SIGNAL(finishedAuthenticate()), this, SLOT(cloudPinNeeded())); + QNetworkReply *reply = cloudAuth->backend(email, password); + } + } + } else if (prefs.cloud_verification_status == CS_NEED_TO_VERIFY) { + QString pin = ui.cloud_storage_pin->text(); + if (!pin.isEmpty()) { + // connect to backend server to check / create credentials + QRegularExpression reg("^[a-zA-Z0-9@.+_-]+$"); + if (!reg.match(email).hasMatch() || !reg.match(password).hasMatch()) { + report_error(qPrintable(tr("Cloud storage email and password can only consist of letters, numbers, and '.', '-', '_', and '+'."))); + } + CloudStorageAuthenticate *cloudAuth = new CloudStorageAuthenticate(this); + connect(cloudAuth, SIGNAL(finishedAuthenticate()), this, SLOT(cloudPinNeeded())); + QNetworkReply *reply = cloudAuth->backend(email, password, pin); + } + } + SAVE_OR_REMOVE("email", default_prefs.cloud_storage_email, email); + SAVE_OR_REMOVE("save_password_local", default_prefs.save_password_local, ui.save_password_local->isChecked()); + if (ui.save_password_local->isChecked()) { + SAVE_OR_REMOVE("password", default_prefs.cloud_storage_password, password); + } else { + s.remove("password"); + free(prefs.cloud_storage_password); + prefs.cloud_storage_password = strdup(qPrintable(password)); + } + SAVE_OR_REMOVE("cloud_verification_status", default_prefs.cloud_verification_status, prefs.cloud_verification_status); + SAVE_OR_REMOVE("cloud_background_sync", default_prefs.cloud_background_sync, ui.cloud_background_sync->isChecked()); + + // at this point we intentionally do not have a UI for changing this + // it could go into some sort of "advanced setup" or something + SAVE_OR_REMOVE("cloud_base_url", default_prefs.cloud_base_url, prefs.cloud_base_url); + s.endGroup(); + + s.beginGroup("geocoding"); +#ifdef DISABLED + s.setValue("enable_geocoding", ui.enable_geocoding->isChecked()); + s.setValue("parse_dive_without_gps", ui.parse_without_gps->isChecked()); + s.setValue("tag_existing_dives", ui.tag_existing_dives->isChecked()); +#endif + s.setValue("cat0", ui.first_item->currentIndex()); + s.setValue("cat1", ui.second_item->currentIndex()); + s.setValue("cat2", ui.third_item->currentIndex()); + s.endGroup(); + + loadSettings(); + emit settingsChanged(); +} + +void PreferencesDialog::loadSettings() +{ + // This code was on the mainwindow, it should belong nowhere, but since we didn't + // correctly fixed this code yet ( too much stuff on the code calling preferences ) + // force this here. + loadPreferences(); + QSettings s; + QVariant v; + + ui.save_uid_local->setChecked(s.value("save_uid_local").toBool()); + ui.default_uid->setText(s.value("subsurface_webservice_uid").toString().toUpper()); + + ui.defaultfilename->setEnabled(prefs.default_file_behavior == LOCAL_DEFAULT_FILE); + ui.btnUseDefaultFile->setEnabled(prefs.default_file_behavior == LOCAL_DEFAULT_FILE); + ui.chooseFile->setEnabled(prefs.default_file_behavior == LOCAL_DEFAULT_FILE); +} + +void PreferencesDialog::buttonClicked(QAbstractButton *button) +{ + switch (ui.buttonBox->standardButton(button)) { + case QDialogButtonBox::Discard: + restorePrefs(); + syncSettings(); + close(); + break; + case QDialogButtonBox::Apply: + syncSettings(); + break; + case QDialogButtonBox::FirstButton: + syncSettings(); + close(); + break; + default: + break; // ignore warnings. + } +} +#undef SB + +void PreferencesDialog::on_chooseFile_clicked() +{ + QFileInfo fi(system_default_filename()); + QString choosenFileName = QFileDialog::getOpenFileName(this, tr("Open default log file"), fi.absolutePath(), tr("Subsurface XML files (*.ssrf *.xml *.XML)")); + + if (!choosenFileName.isEmpty()) + ui.defaultfilename->setText(choosenFileName); +} + +void PreferencesDialog::on_resetSettings_clicked() +{ + QSettings s; + + QMessageBox response(this); + response.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + response.setDefaultButton(QMessageBox::Cancel); + response.setWindowTitle(tr("Warning")); + response.setText(tr("If you click OK, all settings of Subsurface will be reset to their default values. This will be applied immediately.")); + response.setWindowModality(Qt::WindowModal); + + int result = response.exec(); + if (result == QMessageBox::Ok) { + copy_prefs(&default_prefs, &prefs); + setUiFromPrefs(); + Q_FOREACH (QString key, s.allKeys()) { + s.remove(key); + } + syncSettings(); + close(); + } +} + +void PreferencesDialog::passwordUpdateSuccessfull() +{ + ui.cloud_storage_password->setText(prefs.cloud_storage_password); +} + +void PreferencesDialog::emitSettingsChanged() +{ + emit settingsChanged(); +} + +void PreferencesDialog::proxyType_changed(int idx) +{ + if (idx == -1) { + return; + } + + int proxyType = ui.proxyType->itemData(idx).toInt(); + bool hpEnabled = (proxyType == QNetworkProxy::Socks5Proxy || proxyType == QNetworkProxy::HttpProxy); + ui.proxyHost->setEnabled(hpEnabled); + ui.proxyPort->setEnabled(hpEnabled); + ui.proxyAuthRequired->setEnabled(hpEnabled); + ui.proxyUsername->setEnabled(hpEnabled & ui.proxyAuthRequired->isChecked()); + ui.proxyPassword->setEnabled(hpEnabled & ui.proxyAuthRequired->isChecked()); + ui.proxyAuthRequired->setChecked(ui.proxyAuthRequired->isChecked()); +} + +void PreferencesDialog::on_btnUseDefaultFile_toggled(bool toggle) +{ + if (toggle) { + ui.defaultfilename->setText(system_default_filename()); + ui.defaultfilename->setEnabled(false); + } else { + ui.defaultfilename->setEnabled(true); + } +} + +void PreferencesDialog::on_noDefaultFile_toggled(bool toggle) +{ + prefs.default_file_behavior = NO_DEFAULT_FILE; +} + +void PreferencesDialog::on_localDefaultFile_toggled(bool toggle) +{ + ui.defaultfilename->setEnabled(toggle); + ui.btnUseDefaultFile->setEnabled(toggle); + ui.chooseFile->setEnabled(toggle); + prefs.default_file_behavior = LOCAL_DEFAULT_FILE; +} + +void PreferencesDialog::on_cloudDefaultFile_toggled(bool toggle) +{ + prefs.default_file_behavior = CLOUD_DEFAULT_FILE; +} diff --git a/desktop-widgets/preferences.h b/desktop-widgets/preferences.h new file mode 100644 index 000000000..326b1f964 --- /dev/null +++ b/desktop-widgets/preferences.h @@ -0,0 +1,54 @@ +#ifndef PREFERENCES_H +#define PREFERENCES_H + +#include <QDialog> +#include "pref.h" + +#include "ui_preferences.h" + +#ifndef Q_OS_ANDROID + class QWebView; +#endif + +class QAbstractButton; + +class PreferencesDialog : public QDialog { + Q_OBJECT +public: + static PreferencesDialog *instance(); + void showEvent(QShowEvent *); + void emitSettingsChanged(); + +signals: + void settingsChanged(); +public +slots: + void buttonClicked(QAbstractButton *button); + void on_chooseFile_clicked(); + void on_resetSettings_clicked(); + void syncSettings(); + void loadSettings(); + void restorePrefs(); + void rememberPrefs(); + void gflowChanged(int gf); + void gfhighChanged(int gf); + void proxyType_changed(int idx); + void on_btnUseDefaultFile_toggled(bool toggle); + void on_noDefaultFile_toggled(bool toggle); + void on_localDefaultFile_toggled(bool toggle); + void on_cloudDefaultFile_toggled(bool toggle); + void facebookLoggedIn(); + void facebookDisconnect(); + void cloudPinNeeded(); + void passwordUpdateSuccessfull(); +private: + explicit PreferencesDialog(QWidget *parent = 0, Qt::WindowFlags f = 0); + void setUiFromPrefs(); + Ui::PreferencesDialog ui; + struct preferences oldPrefs; + #ifndef Q_OS_ANDROID + QWebView *facebookWebView; + #endif +}; + +#endif // PREFERENCES_H diff --git a/desktop-widgets/preferences.ui b/desktop-widgets/preferences.ui new file mode 100644 index 000000000..de2d79b91 --- /dev/null +++ b/desktop-widgets/preferences.ui @@ -0,0 +1,1883 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>PreferencesDialog</class> + <widget class="QDialog" name="PreferencesDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>711</width> + <height>662</height> + </rect> + </property> + <property name="windowTitle"> + <string>Preferences</string> + </property> + <property name="windowIcon"> + <iconset> + <normalon>:/subsurface-icon</normalon> + </iconset> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="margin"> + <number>5</number> + </property> + <item> + <layout class="QHBoxLayout" name="mainHorizontalLayout"> + <item> + <widget class="QListWidget" name="listWidget"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>120</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>120</width> + <height>16777215</height> + </size> + </property> + <property name="iconSize"> + <size> + <width>24</width> + <height>24</height> + </size> + </property> + <property name="textElideMode"> + <enum>Qt::ElideNone</enum> + </property> + <property name="movement"> + <enum>QListView::Static</enum> + </property> + <property name="isWrapping" stdset="0"> + <bool>true</bool> + </property> + <property name="layoutMode"> + <enum>QListView::Batched</enum> + </property> + <property name="spacing"> + <number>0</number> + </property> + <property name="gridSize"> + <size> + <width>110</width> + <height>40</height> + </size> + </property> + <property name="viewMode"> + <enum>QListView::ListMode</enum> + </property> + <property name="uniformItemSizes"> + <bool>true</bool> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="currentRow"> + <number>-1</number> + </property> + <item> + <property name="text"> + <string>Defaults</string> + </property> + <property name="icon"> + <iconset> + <normalon>:/subsurface-icon</normalon> + </iconset> + </property> + </item> + <item> + <property name="text"> + <string>Units</string> + </property> + <property name="icon"> + <iconset> + <normalon>:/units</normalon> + </iconset> + </property> + </item> + <item> + <property name="text"> + <string>Graph</string> + </property> + <property name="icon"> + <iconset> + <normalon>:/graph</normalon> + </iconset> + </property> + </item> + <item> + <property name="text"> + <string>Language</string> + </property> + <property name="icon"> + <iconset> + <normalon>:/advanced</normalon> + </iconset> + </property> + </item> + <item> + <property name="text"> + <string>Network</string> + </property> + <property name="icon"> + <iconset> + <normalon>:/network</normalon> + </iconset> + </property> + </item> + <item> + <property name="text"> + <string>Facebook</string> + </property> + <property name="icon"> + <iconset> + <normalon>:/facebook</normalon> + </iconset> + </property> + </item> + <item> + <property name="text"> + <string>Georeference</string> + </property> + <property name="icon"> + <iconset> + <normalon>:/georeference</normalon> + </iconset> + </property> + </item> + </widget> + </item> + <item> + <widget class="QStackedWidget" name="stackedWidget"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="currentIndex"> + <number>4</number> + </property> + <widget class="QWidget" name="defaults_page"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <property name="spacing"> + <number>5</number> + </property> + <property name="margin"> + <number>5</number> + </property> + <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>Lists and tables</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout_11"> + <property name="margin"> + <number>5</number> + </property> + <item> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>Font</string> + </property> + </widget> + </item> + <item> + <widget class="QFontComboBox" name="font"/> + </item> + <item> + <widget class="QLabel" name="label_8"> + <property name="text"> + <string>Font size</string> + </property> + </widget> + </item> + <item> + <widget class="QDoubleSpinBox" name="fontsize"/> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_3"> + <property name="title"> + <string>Dives</string> + </property> + <layout class="QFormLayout" name="formLayout"> + <property name="horizontalSpacing"> + <number>5</number> + </property> + <property name="verticalSpacing"> + <number>5</number> + </property> + <property name="margin"> + <number>5</number> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="defaultDiveLogFileLabel"> + <property name="text"> + <string>Default dive log file</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <layout class="QHBoxLayout" name="defaultFileBehaviorLayout"> + <item> + <widget class="QRadioButton" name="noDefaultFile"> + <property name="text"> + <string>No default file</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">defaultFileGroup</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="localDefaultFile"> + <property name="text"> + <string>&Local default file</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">defaultFileGroup</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="cloudDefaultFile"> + <property name="text"> + <string>Clo&ud storage default file</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">defaultFileGroup</string> + </attribute> + </widget> + </item> + </layout> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_9"> + <property name="text"> + <string>Local dive log file</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_3b"> + <item> + <widget class="QLineEdit" name="defaultfilename"/> + </item> + <item> + <widget class="QToolButton" name="btnUseDefaultFile"> + <property name="text"> + <string>Use default</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="chooseFile"> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_10"> + <property name="text"> + <string>Display invalid</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QCheckBox" name="displayinvalid"> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_6"> + <property name="title"> + <string>Default cylinder</string> + </property> + <layout class="QFormLayout" name="formLayout_6"> + <property name="horizontalSpacing"> + <number>5</number> + </property> + <property name="verticalSpacing"> + <number>5</number> + </property> + <property name="margin"> + <number>5</number> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="label_11"> + <property name="text"> + <string>Use default cylinder</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QComboBox" name="default_cylinder"/> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_7"> + <property name="title"> + <string>Animations</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout_7"> + <property name="margin"> + <number>5</number> + </property> + <item> + <widget class="QLabel" name="label_15"> + <property name="text"> + <string>Speed</string> + </property> + </widget> + </item> + <item> + <widget class="QSlider" name="velocitySlider"> + <property name="maximum"> + <number>500</number> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="velocitySpinBox"> + <property name="maximum"> + <number>500</number> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_9"> + <property name="title"> + <string>Clear all settings</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_7b"> + <property name="spacing"> + <number>5</number> + </property> + <property name="margin"> + <number>5</number> + </property> + <item> + <widget class="QPushButton" name="resetSettings"> + <property name="text"> + <string>Reset all settings to their default value</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="units_page"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="spacing"> + <number>5</number> + </property> + <property name="margin"> + <number>5</number> + </property> + <item> + <widget class="QGroupBox" name="groupBox_units"> + <property name="title"> + <string>Unit system</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>System</string> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="metric"> + <property name="text"> + <string>&Metric</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup_6</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="imperial"> + <property name="text"> + <string>Imperial</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup_6</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="personalize"> + <property name="text"> + <string>Personali&ze</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup_6</string> + </attribute> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="units_group"> + <property name="title"> + <string>Individual settings</string> + </property> + <property name="checkable"> + <bool>false</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Depth</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QRadioButton" name="meter"> + <property name="text"> + <string>meter</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup</string> + </attribute> + </widget> + </item> + <item row="0" column="2"> + <widget class="QRadioButton" name="feet"> + <property name="text"> + <string>feet</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup</string> + </attribute> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Pressure</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QRadioButton" name="bar"> + <property name="text"> + <string>bar</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup_2</string> + </attribute> + </widget> + </item> + <item row="1" column="2"> + <widget class="QRadioButton" name="psi"> + <property name="text"> + <string>psi</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup_2</string> + </attribute> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Volume</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QRadioButton" name="liter"> + <property name="text"> + <string>&liter</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup_3</string> + </attribute> + </widget> + </item> + <item row="2" column="2"> + <widget class="QRadioButton" name="cuft"> + <property name="text"> + <string>cu ft</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup_3</string> + </attribute> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Temperature</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QRadioButton" name="celsius"> + <property name="text"> + <string>celsius</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup_4</string> + </attribute> + </widget> + </item> + <item row="3" column="2"> + <widget class="QRadioButton" name="fahrenheit"> + <property name="text"> + <string>fahrenheit</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup_4</string> + </attribute> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Weight</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QRadioButton" name="kg"> + <property name="text"> + <string>kg</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup_5</string> + </attribute> + </widget> + </item> + <item row="4" column="2"> + <widget class="QRadioButton" name="lbs"> + <property name="text"> + <string>lbs</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup_5</string> + </attribute> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <layout class="QHBoxLayout"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Time units</string> + </property> + <layout class="QGridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label13"> + <property name="text"> + <string>Ascent/descent speed denominator</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QRadioButton" name="vertical_speed_minutes"> + <property name="text"> + <string>Minutes</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">verticalSpeed</string> + </attribute> + </widget> + </item> + <item row="0" column="2"> + <widget class="QRadioButton" name="vertical_speed_seconds"> + <property name="text"> + <string>Seconds</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">verticalSpeed</string> + </attribute> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QGroupBox" name="groupBox_11"> + <property name="title"> + <string>GPS coordinates</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout_12"> + <item> + <widget class="QLabel" name="label_27"> + <property name="text"> + <string>Location Display</string> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="gpsTraditional"> + <property name="text"> + <string>traditional (dms)</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup_7</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="gpsDecimal"> + <property name="text"> + <string>decimal</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup_7</string> + </attribute> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="graph_page"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <property name="spacing"> + <number>5</number> + </property> + <property name="margin"> + <number>5</number> + </property> + <item> + <widget class="QGroupBox" name="groupBox_4"> + <property name="title"> + <string>Show</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <widget class="QLabel" name="label_12"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Threshold when showing pOâ‚‚</string> + </property> + </widget> + </item> + <item> + <widget class="QDoubleSpinBox" name="po2Threshold"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="singleStep"> + <double>0.100000000000000</double> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_5"> + <item> + <widget class="QLabel" name="label_13"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Threshold when showing pNâ‚‚</string> + </property> + </widget> + </item> + <item> + <widget class="QDoubleSpinBox" name="pn2Threshold"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="singleStep"> + <double>0.100000000000000</double> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_8"> + <item> + <widget class="QLabel" name="label_17"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Threshold when showing pHe</string> + </property> + </widget> + </item> + <item> + <widget class="QDoubleSpinBox" name="pheThreshold"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="singleStep"> + <double>0.100000000000000</double> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_9"> + <item> + <widget class="QLabel" name="label_18"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Max pOâ‚‚ when showing MOD</string> + </property> + </widget> + </item> + <item> + <widget class="QDoubleSpinBox" name="maxpo2"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="singleStep"> + <double>0.100000000000000</double> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_10b"> + <item> + <widget class="QCheckBox" name="red_ceiling"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Draw dive computer reported ceiling red</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_11c"> + <item> + <widget class="QCheckBox" name="display_unused_tanks"> + <property name="text"> + <string>Show unused cylinders in Equipment tab</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_11d"> + <item> + <widget class="QCheckBox" name="show_average_depth"> + <property name="text"> + <string>Show average depth</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_5"> + <property name="title"> + <string>Misc</string> + </property> + <layout class="QFormLayout" name="formLayout_3"> + <item row="1" column="0"> + <widget class="QLabel" name="label_19"> + <property name="text"> + <string>GFLow</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QSpinBox" name="gflow"> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>150</number> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_20"> + <property name="text"> + <string>GFHigh</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QSpinBox" name="gfhigh"> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>150</number> + </property> + </widget> + </item> + <item row="4" column="0" colspan="2"> + <widget class="QCheckBox" name="gf_low_at_maxdepth"> + <property name="text"> + <string>GFLow at max depth</string> + </property> + </widget> + </item> + <item row="5" column="0"> + <widget class="QCheckBox" name="show_ccr_setpoint"> + <property name="text"> + <string>CCR: show setpoints when viewing pOâ‚‚</string> + </property> + </widget> + </item> + <item row="6" column="0"> + <widget class="QCheckBox" name="show_ccr_sensors"> + <property name="text"> + <string>CCR: show individual Oâ‚‚ sensor values when viewing pOâ‚‚</string> + </property> + </widget> + </item> + <item row="7" column="0"> + <widget class="QLabel" name="label_26"> + <property name="text"> + <string>Default CCR set-point for dive planning</string> + </property> + </widget> + </item> + <item row="7" column="1"> + <widget class="QDoubleSpinBox" name="defaultSetpoint"> + <property name="suffix"> + <string>bar</string> + </property> + <property name="decimals"> + <number>2</number> + </property> + <property name="maximum"> + <double>10.000000000000000</double> + </property> + <property name="singleStep"> + <double>0.100000000000000</double> + </property> + </widget> + </item> + <item row="8" column="0"> + <widget class="QLabel" name="pSCR"> + <property name="text"> + <string>pSCR Oâ‚‚ metabolism rate</string> + </property> + </widget> + </item> + <item row="9" column="0"> + <widget class="QLabel" name="label_28"> + <property name="text"> + <string>pSCR ratio</string> + </property> + </widget> + </item> + <item row="8" column="1"> + <widget class="QDoubleSpinBox" name="psro2rate"> + <property name="suffix"> + <string>â„“/min</string> + </property> + <property name="decimals"> + <number>3</number> + </property> + </widget> + </item> + <item row="9" column="1"> + <widget class="QSpinBox" name="pscrfactor"> + <property name="suffix"> + <string/> + </property> + <property name="prefix"> + <string>1:</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer_5"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="language_page"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QVBoxLayout" name="verticalLayout_6" stretch="0,1,0"> + <property name="spacing"> + <number>5</number> + </property> + <property name="sizeConstraint"> + <enum>QLayout::SetNoConstraint</enum> + </property> + <property name="margin"> + <number>5</number> + </property> + <item> + <widget class="QGroupBox" name="language_group"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>UI language</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout_6"> + <item> + <widget class="QCheckBox" name="languageSystemDefault"> + <property name="text"> + <string>System default</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="label_14"> + <property name="text"> + <string>Filter</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="languageFilter"/> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QListView" name="languageView"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_7"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="network_page"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QVBoxLayout" name="verticalLayout_15"> + <property name="spacing"> + <number>5</number> + </property> + <property name="margin"> + <number>5</number> + </property> + <item> + <widget class="QGroupBox" name="groupBox_10"> + <property name="title"> + <string>Proxy</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="1" column="2"> + <widget class="QLabel" name="label_23"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Port</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="proxyType"/> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_22"> + <property name="text"> + <string>Host</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QCheckBox" name="proxyAuthRequired"> + <property name="layoutDirection"> + <enum>Qt::LeftToRight</enum> + </property> + <property name="text"> + <string>Requires authentication</string> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label_21"> + <property name="text"> + <string>Proxy type</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_24"> + <property name="text"> + <string>Username</string> + </property> + </widget> + </item> + <item row="1" column="3"> + <widget class="QSpinBox" name="proxyPort"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximum"> + <number>65535</number> + </property> + <property name="value"> + <number>80</number> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QLineEdit" name="proxyUsername"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maxLength"> + <number>32</number> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="proxyHost"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>2</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maxLength"> + <number>64</number> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_25"> + <property name="text"> + <string>Password</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QLineEdit" name="proxyPassword"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maxLength"> + <number>32</number> + </property> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="cloudStorageGroupBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>129</height> + </size> + </property> + <property name="title"> + <string>Subsurface cloud storage</string> + </property> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="0" column="0"> + <widget class="QLabel" name="label_16b"> + <property name="toolTip"> + <string extracomment="Email address used for the Subsurface cloud storage infrastructure"/> + </property> + <property name="text"> + <string>Email address</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="label_16c"> + <property name="text"> + <string>Password</string> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QLabel" name="cloud_storage_pin_label"> + <property name="text"> + <string>Verification PIN</string> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QLabel" name="cloud_storage_new_passwd_label"> + <property name="text"> + <string>New password</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLineEdit" name="cloud_storage_email"> + <property name="toolTip"> + <string extracomment="Email address used for the Subsurface cloud storage infrastructure"/> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="cloud_storage_password"> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QLineEdit" name="cloud_storage_pin"> + <property name="toolTip"> + <string extracomment="One time verification PIN for Subsurface cloud storage infrastructure"/> + </property> + </widget> + </item> + <item row="1" column="3"> + <widget class="QLineEdit" name="cloud_storage_new_passwd"> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QCheckBox" name="cloud_background_sync"> + <property name="text"> + <string>Sync to cloud in the background?</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QCheckBox" name="save_password_local"> + <property name="text"> + <string>Save Password locally?</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_8"> + <property name="title"> + <string>Subsurface web service</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_7"> + <property name="spacing"> + <number>5</number> + </property> + <property name="margin"> + <number>5</number> + </property> + <item> + <widget class="QLabel" name="label_16"> + <property name="text"> + <string>Default user ID</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="default_uid"/> + </item> + <item> + <widget class="QCheckBox" name="save_uid_local"> + <property name="text"> + <string>Save user ID locally?</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer_3"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="facebook_page"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QVBoxLayout" name="fbLayout" stretch="0"> + <property name="spacing"> + <number>5</number> + </property> + <property name="margin"> + <number>5</number> + </property> + <item> + <widget class="QWidget" name="widget" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_9"> + <item> + <widget class="QLabel" name="FBLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Connect to facebook text placeholder</string> + </property> + </widget> + </item> + <item> + <widget class="QWidget" name="fbWebviewContainer" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_10"/> + </widget> + </item> + <item> + <widget class="QPushButton" name="fbDisconnect"> + <property name="text"> + <string>Disconnect</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="geolookup_page"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QVBoxLayout" name="verticalLayout_155"> + <property name="spacing"> + <number>5</number> + </property> + <property name="margin"> + <number>5</number> + </property> + <item> + <widget class="QGroupBox" name="groupBox_12"> + <property name="title"> + <string>Dive Site Layout</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout_10"> + <item> + <widget class="QComboBox" name="first_item"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_29"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>/</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="second_item"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_30"> + <property name="text"> + <string>/</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="third_item"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer_6"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Apply|QDialogButtonBox::Discard|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>PreferencesDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>264</x> + <y>720</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>PreferencesDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>332</x> + <y>720</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>listWidget</sender> + <signal>currentRowChanged(int)</signal> + <receiver>stackedWidget</receiver> + <slot>setCurrentIndex(int)</slot> + <hints> + <hint type="sourcelabel"> + <x>37</x> + <y>97</y> + </hint> + <hint type="destinationlabel"> + <x>282</x> + <y>18</y> + </hint> + </hints> + </connection> + <connection> + <sender>personalize</sender> + <signal>toggled(bool)</signal> + <receiver>units_group</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>185</x> + <y>19</y> + </hint> + <hint type="destinationlabel"> + <x>186</x> + <y>23</y> + </hint> + </hints> + </connection> + <connection> + <sender>languageSystemDefault</sender> + <signal>toggled(bool)</signal> + <receiver>languageView</receiver> + <slot>setDisabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>231</x> + <y>26</y> + </hint> + <hint type="destinationlabel"> + <x>186</x> + <y>30</y> + </hint> + </hints> + </connection> + <connection> + <sender>languageSystemDefault</sender> + <signal>toggled(bool)</signal> + <receiver>languageFilter</receiver> + <slot>setDisabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>231</x> + <y>26</y> + </hint> + <hint type="destinationlabel"> + <x>185</x> + <y>20</y> + </hint> + </hints> + </connection> + <connection> + <sender>imperial</sender> + <signal>toggled(bool)</signal> + <receiver>feet</receiver> + <slot>setChecked(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>164</x> + <y>19</y> + </hint> + <hint type="destinationlabel"> + <x>175</x> + <y>34</y> + </hint> + </hints> + </connection> + <connection> + <sender>metric</sender> + <signal>toggled(bool)</signal> + <receiver>meter</receiver> + <slot>setChecked(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>142</x> + <y>19</y> + </hint> + <hint type="destinationlabel"> + <x>153</x> + <y>34</y> + </hint> + </hints> + </connection> + <connection> + <sender>imperial</sender> + <signal>toggled(bool)</signal> + <receiver>psi</receiver> + <slot>setChecked(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>164</x> + <y>19</y> + </hint> + <hint type="destinationlabel"> + <x>175</x> + <y>33</y> + </hint> + </hints> + </connection> + <connection> + <sender>metric</sender> + <signal>toggled(bool)</signal> + <receiver>bar</receiver> + <slot>setChecked(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>142</x> + <y>19</y> + </hint> + <hint type="destinationlabel"> + <x>153</x> + <y>33</y> + </hint> + </hints> + </connection> + <connection> + <sender>imperial</sender> + <signal>toggled(bool)</signal> + <receiver>cuft</receiver> + <slot>setChecked(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>164</x> + <y>19</y> + </hint> + <hint type="destinationlabel"> + <x>175</x> + <y>31</y> + </hint> + </hints> + </connection> + <connection> + <sender>metric</sender> + <signal>toggled(bool)</signal> + <receiver>liter</receiver> + <slot>setChecked(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>142</x> + <y>19</y> + </hint> + <hint type="destinationlabel"> + <x>153</x> + <y>31</y> + </hint> + </hints> + </connection> + <connection> + <sender>imperial</sender> + <signal>toggled(bool)</signal> + <receiver>fahrenheit</receiver> + <slot>setChecked(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>164</x> + <y>19</y> + </hint> + <hint type="destinationlabel"> + <x>175</x> + <y>29</y> + </hint> + </hints> + </connection> + <connection> + <sender>metric</sender> + <signal>toggled(bool)</signal> + <receiver>celsius</receiver> + <slot>setChecked(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>142</x> + <y>19</y> + </hint> + <hint type="destinationlabel"> + <x>153</x> + <y>29</y> + </hint> + </hints> + </connection> + <connection> + <sender>imperial</sender> + <signal>toggled(bool)</signal> + <receiver>lbs</receiver> + <slot>setChecked(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>164</x> + <y>19</y> + </hint> + <hint type="destinationlabel"> + <x>175</x> + <y>28</y> + </hint> + </hints> + </connection> + <connection> + <sender>metric</sender> + <signal>toggled(bool)</signal> + <receiver>kg</receiver> + <slot>setChecked(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>142</x> + <y>19</y> + </hint> + <hint type="destinationlabel"> + <x>153</x> + <y>28</y> + </hint> + </hints> + </connection> + <connection> + <sender>velocitySlider</sender> + <signal>valueChanged(int)</signal> + <receiver>velocitySpinBox</receiver> + <slot>setValue(int)</slot> + <hints> + <hint type="sourcelabel"> + <x>236</x> + <y>52</y> + </hint> + <hint type="destinationlabel"> + <x>236</x> + <y>52</y> + </hint> + </hints> + </connection> + <connection> + <sender>velocitySpinBox</sender> + <signal>valueChanged(int)</signal> + <receiver>velocitySlider</receiver> + <slot>setValue(int)</slot> + <hints> + <hint type="sourcelabel"> + <x>236</x> + <y>52</y> + </hint> + <hint type="destinationlabel"> + <x>236</x> + <y>52</y> + </hint> + </hints> + </connection> + <connection> + <sender>proxyAuthRequired</sender> + <signal>toggled(bool)</signal> + <receiver>proxyUsername</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>409</x> + <y>123</y> + </hint> + <hint type="destinationlabel"> + <x>409</x> + <y>153</y> + </hint> + </hints> + </connection> + <connection> + <sender>proxyAuthRequired</sender> + <signal>toggled(bool)</signal> + <receiver>proxyPassword</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>409</x> + <y>123</y> + </hint> + <hint type="destinationlabel"> + <x>409</x> + <y>183</y> + </hint> + </hints> + </connection> + <connection> + <sender>btnUseDefaultFile</sender> + <signal>toggled(bool)</signal> + <receiver>chooseFile</receiver> + <slot>setHidden(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>236</x> + <y>44</y> + </hint> + <hint type="destinationlabel"> + <x>236</x> + <y>44</y> + </hint> + </hints> + </connection> + </connections> + <buttongroups> + <buttongroup name="verticalSpeed"/> + <buttongroup name="buttonGroup_2"/> + <buttongroup name="buttonGroup_3"/> + <buttongroup name="buttonGroup_4"/> + <buttongroup name="defaultFileGroup"/> + <buttongroup name="buttonGroup_5"/> + <buttongroup name="buttonGroup_6"/> + <buttongroup name="buttonGroup_7"/> + <buttongroup name="buttonGroup"/> + </buttongroups> +</ui> diff --git a/desktop-widgets/printdialog.cpp b/desktop-widgets/printdialog.cpp new file mode 100644 index 000000000..cf08062d2 --- /dev/null +++ b/desktop-widgets/printdialog.cpp @@ -0,0 +1,194 @@ +#include "printdialog.h" +#include "printoptions.h" +#include "mainwindow.h" + +#ifndef NO_PRINTING +#include <QProgressBar> +#include <QPrintPreviewDialog> +#include <QPrintDialog> +#include <QShortcut> +#include <QSettings> +#include <QMessageBox> + +#define SETTINGS_GROUP "PrintDialog" + +template_options::color_palette_struct ssrf_colors, almond_colors, blueshades_colors, custom_colors; + +PrintDialog::PrintDialog(QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f) +{ + // initialize const colors + ssrf_colors.color1 = QColor::fromRgb(0xff, 0xff, 0xff); + ssrf_colors.color2 = QColor::fromRgb(0xa6, 0xbc, 0xd7); + ssrf_colors.color3 = QColor::fromRgb(0xef, 0xf7, 0xff); + ssrf_colors.color4 = QColor::fromRgb(0x34, 0x65, 0xa4); + ssrf_colors.color5 = QColor::fromRgb(0x20, 0x4a, 0x87); + ssrf_colors.color6 = QColor::fromRgb(0x17, 0x37, 0x64); + almond_colors.color1 = QColor::fromRgb(255, 255, 255); + almond_colors.color2 = QColor::fromRgb(253, 204, 156); + almond_colors.color3 = QColor::fromRgb(243, 234, 207); + almond_colors.color4 = QColor::fromRgb(136, 160, 150); + almond_colors.color5 = QColor::fromRgb(187, 171, 139); + almond_colors.color6 = QColor::fromRgb(0, 0, 0); + blueshades_colors.color1 = QColor::fromRgb(255, 255, 255); + blueshades_colors.color2 = QColor::fromRgb(142, 152, 166); + blueshades_colors.color3 = QColor::fromRgb(182, 192, 206); + blueshades_colors.color4 = QColor::fromRgb(31, 49, 75); + blueshades_colors.color5 = QColor::fromRgb(21, 45, 84); + blueshades_colors.color6 = QColor::fromRgb(0, 0, 0); + + // check if the options were previously stored in the settings; if not use some defaults. + QSettings s; + bool stored = s.childGroups().contains(SETTINGS_GROUP); + if (!stored) { + printOptions.print_selected = true; + printOptions.color_selected = true; + printOptions.landscape = false; + printOptions.p_template = "one_dive.html"; + printOptions.type = print_options::DIVELIST; + templateOptions.font_index = 0; + templateOptions.font_size = 9; + templateOptions.color_palette_index = SSRF_COLORS; + templateOptions.line_spacing = 1; + custom_colors = ssrf_colors; + } else { + s.beginGroup(SETTINGS_GROUP); + printOptions.type = (print_options::print_type)s.value("type").toInt(); + printOptions.print_selected = s.value("print_selected").toBool(); + printOptions.color_selected = s.value("color_selected").toBool(); + printOptions.landscape = s.value("landscape").toBool(); + printOptions.p_template = s.value("template_selected").toString(); + qprinter.setOrientation((QPrinter::Orientation)printOptions.landscape); + templateOptions.font_index = s.value("font").toInt(); + templateOptions.font_size = s.value("font_size").toDouble(); + templateOptions.color_palette_index = s.value("color_palette").toInt(); + templateOptions.line_spacing = s.value("line_spacing").toDouble(); + custom_colors.color1 = QColor(s.value("custom_color_1").toString()); + custom_colors.color2 = QColor(s.value("custom_color_2").toString()); + custom_colors.color3 = QColor(s.value("custom_color_3").toString()); + custom_colors.color4 = QColor(s.value("custom_color_4").toString()); + custom_colors.color5 = QColor(s.value("custom_color_5").toString()); + } + + // handle cases from old QSettings group + if (templateOptions.font_size < 9) { + templateOptions.font_size = 9; + } + if (templateOptions.line_spacing < 1) { + templateOptions.line_spacing = 1; + } + + switch (templateOptions.color_palette_index) { + case SSRF_COLORS: // default Subsurface derived colors + templateOptions.color_palette = ssrf_colors; + break; + case ALMOND: // almond + templateOptions.color_palette = almond_colors; + break; + case BLUESHADES: // blueshades + templateOptions.color_palette = blueshades_colors; + break; + case CUSTOM: // custom + templateOptions.color_palette = custom_colors; + break; + } + + // create a print options object and pass our options struct + optionsWidget = new PrintOptions(this, &printOptions, &templateOptions); + + // create a new printer object + printer = new Printer(&qprinter, &printOptions, &templateOptions, Printer::PRINT); + + QVBoxLayout *layout = new QVBoxLayout(this); + setLayout(layout); + + layout->addWidget(optionsWidget); + + progressBar = new QProgressBar(); + progressBar->setMinimum(0); + progressBar->setMaximum(100); + progressBar->setValue(0); + progressBar->setTextVisible(false); + layout->addWidget(progressBar); + + QHBoxLayout *hLayout = new QHBoxLayout(); + layout->addLayout(hLayout); + + QPushButton *printButton = new QPushButton(tr("P&rint")); + connect(printButton, SIGNAL(clicked(bool)), this, SLOT(printClicked())); + + QPushButton *previewButton = new QPushButton(tr("&Preview")); + connect(previewButton, SIGNAL(clicked(bool)), this, SLOT(previewClicked())); + + QDialogButtonBox *buttonBox = new QDialogButtonBox; + buttonBox->addButton(QDialogButtonBox::Cancel); + buttonBox->addButton(printButton, QDialogButtonBox::AcceptRole); + buttonBox->addButton(previewButton, QDialogButtonBox::ActionRole); + + connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); + + hLayout->addWidget(buttonBox); + + setWindowTitle(tr("Print")); + setWindowIcon(QIcon(":subsurface-icon")); + + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); + + // seems to be the most reliable way to track for all sorts of dialog disposal. + connect(this, SIGNAL(finished(int)), this, SLOT(onFinished())); +} + +void PrintDialog::onFinished() +{ + QSettings s; + s.beginGroup(SETTINGS_GROUP); + + // save print paper settings + s.setValue("type", printOptions.type); + s.setValue("print_selected", printOptions.print_selected); + s.setValue("color_selected", printOptions.color_selected); + s.setValue("template_selected", printOptions.p_template); + + // save template settings + s.setValue("font", templateOptions.font_index); + s.setValue("font_size", templateOptions.font_size); + s.setValue("color_palette", templateOptions.color_palette_index); + s.setValue("line_spacing", templateOptions.line_spacing); + + // save custom colors + s.setValue("custom_color_1", custom_colors.color1.name()); + s.setValue("custom_color_2", custom_colors.color2.name()); + s.setValue("custom_color_3", custom_colors.color3.name()); + s.setValue("custom_color_4", custom_colors.color4.name()); + s.setValue("custom_color_5", custom_colors.color5.name()); +} + +void PrintDialog::previewClicked(void) +{ + QPrintPreviewDialog previewDialog(&qprinter, this, Qt::Window + | Qt::CustomizeWindowHint | Qt::WindowCloseButtonHint + | Qt::WindowTitleHint); + connect(&previewDialog, SIGNAL(paintRequested(QPrinter *)), this, SLOT(onPaintRequested(QPrinter *))); + previewDialog.exec(); +} + +void PrintDialog::printClicked(void) +{ + QPrintDialog printDialog(&qprinter, this); + if (printDialog.exec() == QDialog::Accepted) { + connect(printer, SIGNAL(progessUpdated(int)), progressBar, SLOT(setValue(int))); + printer->print(); + close(); + } +} + +void PrintDialog::onPaintRequested(QPrinter *printerPtr) +{ + connect(printer, SIGNAL(progessUpdated(int)), progressBar, SLOT(setValue(int))); + printer->print(); + progressBar->setValue(0); + disconnect(printer, SIGNAL(progessUpdated(int)), progressBar, SLOT(setValue(int))); +} +#endif diff --git a/desktop-widgets/printdialog.h b/desktop-widgets/printdialog.h new file mode 100644 index 000000000..a00c4c5d9 --- /dev/null +++ b/desktop-widgets/printdialog.h @@ -0,0 +1,38 @@ +#ifndef PRINTDIALOG_H +#define PRINTDIALOG_H + +#ifndef NO_PRINTING +#include <QDialog> +#include <QPrinter> +#include "printoptions.h" +#include "printer.h" +#include "templateedit.h" + +class QProgressBar; +class PrintOptions; +class PrintLayout; + +// should be based on a custom QPrintDialog class +class PrintDialog : public QDialog { + Q_OBJECT + +public: + explicit PrintDialog(QWidget *parent = 0, Qt::WindowFlags f = 0); + +private: + PrintOptions *optionsWidget; + QProgressBar *progressBar; + Printer *printer; + QPrinter qprinter; + struct print_options printOptions; + struct template_options templateOptions; + +private +slots: + void onFinished(); + void previewClicked(); + void printClicked(); + void onPaintRequested(QPrinter *); +}; +#endif +#endif // PRINTDIALOG_H diff --git a/desktop-widgets/printer.cpp b/desktop-widgets/printer.cpp new file mode 100644 index 000000000..f0197d446 --- /dev/null +++ b/desktop-widgets/printer.cpp @@ -0,0 +1,273 @@ +#include "printer.h" +#include "templatelayout.h" +#include "statistics.h" +#include "helpers.h" + +#include <algorithm> +#include <QtWebKitWidgets> +#include <QPainter> +#include <QWebElementCollection> +#include <QWebElement> + +Printer::Printer(QPaintDevice *paintDevice, print_options *printOptions, template_options *templateOptions, PrintMode printMode) +{ + this->paintDevice = paintDevice; + this->printOptions = printOptions; + this->templateOptions = templateOptions; + this->printMode = printMode; + dpi = 0; + done = 0; + webView = new QWebView(); +} + +Printer::~Printer() +{ + delete webView; +} + +void Printer::putProfileImage(QRect profilePlaceholder, QRect viewPort, QPainter *painter, struct dive *dive, QPointer<ProfileWidget2> profile) +{ + int x = profilePlaceholder.x() - viewPort.x(); + int y = profilePlaceholder.y() - viewPort.y(); + // use the placeHolder and the viewPort position to calculate the relative position of the dive profile. + QRect pos(x, y, profilePlaceholder.width(), profilePlaceholder.height()); + profile->plotDive(dive, true); + + if (!printOptions->color_selected) { + QImage image(pos.width(), pos.height(), QImage::Format_ARGB32); + QPainter imgPainter(&image); + imgPainter.setRenderHint(QPainter::Antialiasing); + imgPainter.setRenderHint(QPainter::SmoothPixmapTransform); + profile->render(&imgPainter, QRect(0, 0, pos.width(), pos.height())); + imgPainter.end(); + + // convert QImage to grayscale before rendering + for (int i = 0; i < image.height(); i++) { + QRgb *pixel = reinterpret_cast<QRgb *>(image.scanLine(i)); + QRgb *end = pixel + image.width(); + for (; pixel != end; pixel++) { + int gray_val = qGray(*pixel); + *pixel = QColor(gray_val, gray_val, gray_val).rgb(); + } + } + + painter->drawImage(pos, image); + } else { + profile->render(painter, pos); + } +} + +void Printer::flowRender() +{ + // add extra padding at the bottom to pages with height not divisible by view port + int paddingBottom = pageSize.height() - (webView->page()->mainFrame()->contentsSize().height() % pageSize.height()); + QString styleString = QString::fromUtf8("padding-bottom: ") + QString::number(paddingBottom) + "px;"; + webView->page()->mainFrame()->findFirstElement("body").setAttribute("style", styleString); + + // render the Qwebview + QPainter painter; + QRect viewPort(0, 0, 0, 0); + painter.begin(paintDevice); + painter.setRenderHint(QPainter::Antialiasing); + painter.setRenderHint(QPainter::SmoothPixmapTransform); + + // get all references to dontbreak divs + int start = 0, end = 0; + int fullPageResolution = webView->page()->mainFrame()->contentsSize().height(); + QWebElementCollection dontbreak = webView->page()->mainFrame()->findAllElements(".dontbreak"); + foreach (QWebElement dontbreakElement, dontbreak) { + if ((dontbreakElement.geometry().y() + dontbreakElement.geometry().height()) - start < pageSize.height()) { + // One more element can be placed + end = dontbreakElement.geometry().y() + dontbreakElement.geometry().height(); + } else { + // fill the page with background color + QRect fullPage(0, 0, pageSize.width(), pageSize.height()); + QBrush fillBrush(templateOptions->color_palette.color1); + painter.fillRect(fullPage, fillBrush); + QRegion reigon(0, 0, pageSize.width(), end - start); + viewPort.setRect(0, start, pageSize.width(), end - start); + + // render the base Html template + webView->page()->mainFrame()->render(&painter, QWebFrame::ContentsLayer, reigon); + + // scroll the webview to the next page + webView->page()->mainFrame()->scroll(0, dontbreakElement.geometry().y() - start); + + // rendering progress is 4/5 of total work + emit(progessUpdated((end * 80.0 / fullPageResolution) + done)); + + // add new pages only in print mode, while previewing we don't add new pages + if (printMode == Printer::PRINT) + static_cast<QPrinter*>(paintDevice)->newPage(); + else { + painter.end(); + return; + } + start = dontbreakElement.geometry().y(); + } + } + // render the remianing page + QRect fullPage(0, 0, pageSize.width(), pageSize.height()); + QBrush fillBrush(templateOptions->color_palette.color1); + painter.fillRect(fullPage, fillBrush); + QRegion reigon(0, 0, pageSize.width(), end - start); + webView->page()->mainFrame()->render(&painter, QWebFrame::ContentsLayer, reigon); + + painter.end(); +} + +void Printer::render(int Pages = 0) +{ + // keep original preferences + QPointer<ProfileWidget2> profile = MainWindow::instance()->graphics(); + int profileFrameStyle = profile->frameStyle(); + int animationOriginal = prefs.animation_speed; + double fontScale = profile->getFontPrintScale(); + double printFontScale = 1.0; + + // apply printing settings to profile + profile->setFrameStyle(QFrame::NoFrame); + profile->setPrintMode(true, !printOptions->color_selected); + profile->setToolTipVisibile(false); + prefs.animation_speed = 0; + + // render the Qwebview + QPainter painter; + QRect viewPort(0, 0, pageSize.width(), pageSize.height()); + painter.begin(paintDevice); + painter.setRenderHint(QPainter::Antialiasing); + painter.setRenderHint(QPainter::SmoothPixmapTransform); + + // get all refereces to diveprofile class in the Html template + QWebElementCollection collection = webView->page()->mainFrame()->findAllElements(".diveprofile"); + + QSize originalSize = profile->size(); + if (collection.count() > 0) { + printFontScale = (double)collection.at(0).geometry().size().height() / (double)profile->size().height(); + profile->resize(collection.at(0).geometry().size()); + } + profile->setFontPrintScale(printFontScale); + + int elemNo = 0; + for (int i = 0; i < Pages; i++) { + // render the base Html template + webView->page()->mainFrame()->render(&painter, QWebFrame::ContentsLayer); + + // render all the dive profiles in the current page + while (elemNo < collection.count() && collection.at(elemNo).geometry().y() < viewPort.y() + viewPort.height()) { + // dive id field should be dive_{{dive_no}} se we remove the first 5 characters + QString diveIdString = collection.at(elemNo).attribute("id"); + int diveId = diveIdString.remove(0, 5).toInt(0, 10); + putProfileImage(collection.at(elemNo).geometry(), viewPort, &painter, get_dive_by_uniq_id(diveId), profile); + elemNo++; + } + + // scroll the webview to the next page + webView->page()->mainFrame()->scroll(0, pageSize.height()); + viewPort.adjust(0, pageSize.height(), 0, pageSize.height()); + + // rendering progress is 4/5 of total work + emit(progessUpdated((i * 80.0 / Pages) + done)); + if (i < Pages - 1 && printMode == Printer::PRINT) + static_cast<QPrinter*>(paintDevice)->newPage(); + } + painter.end(); + + // return profle settings + profile->setFrameStyle(profileFrameStyle); + profile->setPrintMode(false); + profile->setFontPrintScale(fontScale); + profile->setToolTipVisibile(true); + profile->resize(originalSize); + prefs.animation_speed = animationOriginal; + + //replot the dive after returning the settings + profile->plotDive(0, true); +} + +//value: ranges from 0 : 100 and shows the progress of the templating engine +void Printer::templateProgessUpdated(int value) +{ + done = value / 5; //template progess if 1/5 of total work + emit progessUpdated(done); +} + +void Printer::print() +{ + // we can only print if "PRINT" mode is selected + if (printMode != Printer::PRINT) { + return; + } + + + QPrinter *printerPtr; + printerPtr = static_cast<QPrinter*>(paintDevice); + + TemplateLayout t(printOptions, templateOptions); + connect(&t, SIGNAL(progressUpdated(int)), this, SLOT(templateProgessUpdated(int))); + dpi = printerPtr->resolution(); + //rendering resolution = selected paper size in inchs * printer dpi + pageSize.setHeight(qCeil(printerPtr->pageRect(QPrinter::Inch).height() * dpi)); + pageSize.setWidth(qCeil(printerPtr->pageRect(QPrinter::Inch).width() * dpi)); + webView->page()->setViewportSize(pageSize); + webView->page()->mainFrame()->setScrollBarPolicy(Qt::Vertical, Qt::ScrollBarAlwaysOff); + // export border width with at least 1 pixel + templateOptions->border_width = std::max(1, pageSize.width() / 1000); + if (printOptions->type == print_options::DIVELIST) { + webView->setHtml(t.generate()); + } else if (printOptions->type == print_options::STATISTICS ) { + webView->setHtml(t.generateStatistics()); + } + if (printOptions->color_selected && printerPtr->colorMode()) { + printerPtr->setColorMode(QPrinter::Color); + } else { + printerPtr->setColorMode(QPrinter::GrayScale); + } + // apply user settings + int divesPerPage; + + // get number of dives per page from data-numberofdives attribute in the body of the selected template + bool ok; + divesPerPage = webView->page()->mainFrame()->findFirstElement("body").attribute("data-numberofdives").toInt(&ok); + if (!ok) { + divesPerPage = 1; // print each dive in a single page if the attribute is missing or malformed + //TODO: show warning + } + int Pages; + if (divesPerPage == 0) { + flowRender(); + } else { + Pages = qCeil(getTotalWork(printOptions) / (float)divesPerPage); + render(Pages); + } +} + +void Printer::previewOnePage() +{ + if (printMode == PREVIEW) { + TemplateLayout t(printOptions, templateOptions); + + pageSize.setHeight(paintDevice->height()); + pageSize.setWidth(paintDevice->width()); + webView->page()->setViewportSize(pageSize); + // initialize the border settings + templateOptions->border_width = std::max(1, pageSize.width() / 1000); + if (printOptions->type == print_options::DIVELIST) { + webView->setHtml(t.generate()); + } else if (printOptions->type == print_options::STATISTICS ) { + webView->setHtml(t.generateStatistics()); + } + + bool ok; + int divesPerPage = webView->page()->mainFrame()->findFirstElement("body").attribute("data-numberofdives").toInt(&ok); + if (!ok) { + divesPerPage = 1; // print each dive in a single page if the attribute is missing or malformed + //TODO: show warning + } + if (divesPerPage == 0) { + flowRender(); + } else { + render(1); + } + } +} diff --git a/desktop-widgets/printer.h b/desktop-widgets/printer.h new file mode 100644 index 000000000..979cacd6a --- /dev/null +++ b/desktop-widgets/printer.h @@ -0,0 +1,48 @@ +#ifndef PRINTER_H +#define PRINTER_H + +#include <QPrinter> +#include <QWebView> +#include <QRect> +#include <QPainter> + +#include "profile/profilewidget2.h" +#include "printoptions.h" +#include "templateedit.h" + +class Printer : public QObject { + Q_OBJECT + +public: + enum PrintMode { + PRINT, + PREVIEW + }; + +private: + QPaintDevice *paintDevice; + QWebView *webView; + print_options *printOptions; + template_options *templateOptions; + QSize pageSize; + PrintMode printMode; + int done; + int dpi; + void render(int Pages); + void flowRender(); + void putProfileImage(QRect box, QRect viewPort, QPainter *painter, struct dive *dive, QPointer<ProfileWidget2> profile); + +private slots: + void templateProgessUpdated(int value); + +public: + Printer(QPaintDevice *paintDevice, print_options *printOptions, template_options *templateOptions, PrintMode printMode); + ~Printer(); + void print(); + void previewOnePage(); + +signals: + void progessUpdated(int value); +}; + +#endif //PRINTER_H diff --git a/desktop-widgets/printoptions.cpp b/desktop-widgets/printoptions.cpp new file mode 100644 index 000000000..769c89ff4 --- /dev/null +++ b/desktop-widgets/printoptions.cpp @@ -0,0 +1,189 @@ +#include "printoptions.h" +#include "templateedit.h" +#include "helpers.h" + +#include <QDebug> +#include <QFileDialog> +#include <QMessageBox> + +PrintOptions::PrintOptions(QWidget *parent, struct print_options *printOpt, struct template_options *templateOpt) +{ + hasSetupSlots = false; + ui.setupUi(this); + if (parent) + setParent(parent); + if (!printOpt || !templateOpt) + return; + templateOptions = templateOpt; + printOptions = printOpt; + setup(); +} + +void PrintOptions::setup() +{ + // print type radio buttons + switch (printOptions->type) { + case print_options::DIVELIST: + ui.radioDiveListPrint->setChecked(true); + break; + case print_options::STATISTICS: + ui.radioStatisticsPrint->setChecked(true); + break; + } + + setupTemplates(); + + // general print option checkboxes + if (printOptions->color_selected) + ui.printInColor->setChecked(true); + if (printOptions->print_selected) + ui.printSelected->setChecked(true); + + // connect slots only once + if (hasSetupSlots) + return; + + connect(ui.printInColor, SIGNAL(clicked(bool)), this, SLOT(printInColorClicked(bool))); + connect(ui.printSelected, SIGNAL(clicked(bool)), this, SLOT(printSelectedClicked(bool))); + + hasSetupSlots = true; +} + +void PrintOptions::setupTemplates() +{ + if (printOptions->type == print_options::DIVELIST) { + // insert dive list templates in the UI and select the current template + qSort(grantlee_templates); + int current_index = 0, index = 0; + for (QList<QString>::iterator i = grantlee_templates.begin(); i != grantlee_templates.end(); ++i) { + if ((*i).compare(printOptions->p_template) == 0) { + current_index = index; + break; + } + index++; + } + ui.printTemplate->clear(); + for (QList<QString>::iterator i = grantlee_templates.begin(); i != grantlee_templates.end(); ++i) { + ui.printTemplate->addItem((*i).split('.')[0], QVariant::fromValue(*i)); + } + ui.printTemplate->setCurrentIndex(current_index); + } else if (printOptions->type == print_options::STATISTICS) { + // insert statistics templates in the UI and select the current template + qSort(grantlee_statistics_templates); + int current_index = 0, index = 0; + for (QList<QString>::iterator i = grantlee_statistics_templates.begin(); i != grantlee_statistics_templates.end(); ++i) { + if ((*i).compare(printOptions->p_template) == 0) { + current_index = index; + break; + } + index++; + } + ui.printTemplate->clear(); + for (QList<QString>::iterator i = grantlee_statistics_templates.begin(); i != grantlee_statistics_templates.end(); ++i) { + ui.printTemplate->addItem((*i).split('.')[0], QVariant::fromValue(*i)); + } + ui.printTemplate->setCurrentIndex(current_index); + } +} + +// print type radio buttons +void PrintOptions::on_radioDiveListPrint_toggled(bool check) +{ + if (check) { + printOptions->type = print_options::DIVELIST; + + // print options + ui.printSelected->setEnabled(true); + + // print template + ui.deleteButton->setEnabled(true); + ui.exportButton->setEnabled(true); + ui.importButton->setEnabled(true); + + setupTemplates(); + } +} + +void PrintOptions::on_radioStatisticsPrint_toggled(bool check) +{ + if (check) { + printOptions->type = print_options::STATISTICS; + + // print options + ui.printSelected->setEnabled(false); + + // print template + ui.deleteButton->setEnabled(false); + ui.exportButton->setEnabled(false); + ui.importButton->setEnabled(false); + + setupTemplates(); + } +} + +// general print option checkboxes +void PrintOptions::printInColorClicked(bool check) +{ + printOptions->color_selected = check; +} + +void PrintOptions::printSelectedClicked(bool check) +{ + printOptions->print_selected = check; +} + + +void PrintOptions::on_printTemplate_currentIndexChanged(int index) +{ + printOptions->p_template = ui.printTemplate->itemData(index).toString(); +} + +void PrintOptions::on_editButton_clicked() +{ + TemplateEdit te(this, printOptions, templateOptions); + te.exec(); + setup(); +} + +void PrintOptions::on_importButton_clicked() +{ + QString filename = QFileDialog::getOpenFileName(this, tr("Import template file"), "", + tr("HTML files (*.html)")); + if (filename.isEmpty()) + return; + QFileInfo fileInfo(filename); + QFile::copy(filename, getPrintingTemplatePathUser() + QDir::separator() + fileInfo.fileName()); + printOptions->p_template = fileInfo.fileName(); + find_all_templates(); + setup(); +} + +void PrintOptions::on_exportButton_clicked() +{ + QString filename = QFileDialog::getSaveFileName(this, tr("Export template files as"), "", + tr("HTML files (*.html)")); + if (filename.isEmpty()) + return; + QFile::copy(getPrintingTemplatePathUser() + QDir::separator() + getSelectedTemplate(), filename); +} + +void PrintOptions::on_deleteButton_clicked() +{ + QString templateName = getSelectedTemplate(); + QMessageBox msgBox; + msgBox.setText(tr("This action cannot be undone!")); + msgBox.setInformativeText(tr("Delete template: %1?").arg(templateName)); + msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Cancel); + if (msgBox.exec() == QMessageBox::Ok) { + QFile f(getPrintingTemplatePathUser() + QDir::separator() + templateName); + f.remove(); + find_all_templates(); + setup(); + } +} + +QString PrintOptions::getSelectedTemplate() +{ + return ui.printTemplate->currentData().toString(); +} diff --git a/desktop-widgets/printoptions.h b/desktop-widgets/printoptions.h new file mode 100644 index 000000000..9c50b10f3 --- /dev/null +++ b/desktop-widgets/printoptions.h @@ -0,0 +1,88 @@ +#ifndef PRINTOPTIONS_H +#define PRINTOPTIONS_H + +#include <QWidget> + +#include "ui_printoptions.h" + +struct print_options { + enum print_type { + DIVELIST, + STATISTICS + } type; + QString p_template; + bool print_selected; + bool color_selected; + bool landscape; +}; + +struct template_options { + int font_index; + int color_palette_index; + int border_width; + double font_size; + double line_spacing; + struct color_palette_struct { + QColor color1; + QColor color2; + QColor color3; + QColor color4; + QColor color5; + QColor color6; + bool operator!=(const color_palette_struct &other) const { + return other.color1 != color1 + || other.color2 != color2 + || other.color3 != color3 + || other.color4 != color4 + || other.color5 != color5 + || other.color6 != color6; + } + } color_palette; + bool operator!=(const template_options &other) const { + return other.font_index != font_index + || other.color_palette_index != color_palette_index + || other.font_size != font_size + || other.line_spacing != line_spacing + || other.color_palette != color_palette; + } + }; + +extern template_options::color_palette_struct ssrf_colors, almond_colors, blueshades_colors, custom_colors; + +enum color_palette { + SSRF_COLORS, + ALMOND, + BLUESHADES, + CUSTOM +}; + +// should be based on a custom QPrintDialog class +class PrintOptions : public QWidget { + Q_OBJECT + +public: + explicit PrintOptions(QWidget *parent, struct print_options *printOpt, struct template_options *templateOpt); + void setup(); + QString getSelectedTemplate(); + +private: + Ui::PrintOptions ui; + struct print_options *printOptions; + struct template_options *templateOptions; + bool hasSetupSlots; + void setupTemplates(); + +private +slots: + void printInColorClicked(bool check); + void printSelectedClicked(bool check); + void on_radioStatisticsPrint_toggled(bool check); + void on_radioDiveListPrint_toggled(bool check); + void on_printTemplate_currentIndexChanged(int index); + void on_editButton_clicked(); + void on_importButton_clicked(); + void on_exportButton_clicked(); + void on_deleteButton_clicked(); +}; + +#endif // PRINTOPTIONS_H diff --git a/desktop-widgets/printoptions.ui b/desktop-widgets/printoptions.ui new file mode 100644 index 000000000..1c2523d39 --- /dev/null +++ b/desktop-widgets/printoptions.ui @@ -0,0 +1,168 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>PrintOptions</class> + <widget class="QWidget" name="PrintOptions"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>367</width> + <height>433</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QGroupBox" name="printType"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="baseSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="title"> + <string>Print type</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" colspan="2"> + <widget class="QRadioButton" name="radioDiveListPrint"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>&Dive list print</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QRadioButton" name="radioStatisticsPrint"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>&Statistics print</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="printOptions"> + <property name="title"> + <string>Print options</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QCheckBox" name="printSelected"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Print only selected dives</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="printInColor"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Print in color</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="template_2"> + <property name="title"> + <string>Template</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QComboBox" name="printTemplate"/> + </item> + <item> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="1" column="0"> + <widget class="QPushButton" name="editButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>16777215</height> + </size> + </property> + <property name="text"> + <string>Edit</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QPushButton" name="deleteButton"> + <property name="text"> + <string>Delete</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QPushButton" name="exportButton"> + <property name="text"> + <string>Export</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QPushButton" name="importButton"> + <property name="text"> + <string>Import</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>radioDiveListPrint</tabstop> + <tabstop>printSelected</tabstop> + <tabstop>printInColor</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/desktop-widgets/profile/animationfunctions.cpp b/desktop-widgets/profile/animationfunctions.cpp new file mode 100644 index 000000000..a19d50c9d --- /dev/null +++ b/desktop-widgets/profile/animationfunctions.cpp @@ -0,0 +1,75 @@ +#include "animationfunctions.h" +#include "pref.h" +#include <QPropertyAnimation> + +namespace Animations { + + void hide(QObject *obj) + { + if (prefs.animation_speed != 0) { + QPropertyAnimation *animation = new QPropertyAnimation(obj, "opacity"); + animation->setStartValue(1); + animation->setEndValue(0); + animation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + obj->setProperty("opacity", 0); + } + } + + void show(QObject *obj) + { + if (prefs.animation_speed != 0) { + QPropertyAnimation *animation = new QPropertyAnimation(obj, "opacity"); + animation->setStartValue(0); + animation->setEndValue(1); + animation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + obj->setProperty("opacity", 1); + } + } + + void animDelete(QObject *obj) + { + if (prefs.animation_speed != 0) { + QPropertyAnimation *animation = new QPropertyAnimation(obj, "opacity"); + obj->connect(animation, SIGNAL(finished()), SLOT(deleteLater())); + animation->setStartValue(1); + animation->setEndValue(0); + animation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + obj->setProperty("opacity", 0); + } + } + + void moveTo(QObject *obj, qreal x, qreal y) + { + if (prefs.animation_speed != 0) { + QPropertyAnimation *animation = new QPropertyAnimation(obj, "pos"); + animation->setDuration(prefs.animation_speed); + animation->setStartValue(obj->property("pos").toPointF()); + animation->setEndValue(QPointF(x, y)); + animation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + obj->setProperty("pos", QPointF(x, y)); + } + } + + void scaleTo(QObject *obj, qreal scale) + { + if (prefs.animation_speed != 0) { + QPropertyAnimation *animation = new QPropertyAnimation(obj, "scale"); + animation->setDuration(prefs.animation_speed); + animation->setStartValue(obj->property("scale").toReal()); + animation->setEndValue(QVariant::fromValue(scale)); + animation->setEasingCurve(QEasingCurve::InCubic); + animation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + obj->setProperty("scale", QVariant::fromValue(scale)); + } + } + + void moveTo(QObject *obj, const QPointF &pos) + { + moveTo(obj, pos.x(), pos.y()); + } +} diff --git a/desktop-widgets/profile/animationfunctions.h b/desktop-widgets/profile/animationfunctions.h new file mode 100644 index 000000000..3cfcff563 --- /dev/null +++ b/desktop-widgets/profile/animationfunctions.h @@ -0,0 +1,18 @@ +#ifndef ANIMATIONFUNCTIONS_H +#define ANIMATIONFUNCTIONS_H + +#include <QtGlobal> +#include <QPointF> + +class QObject; + +namespace Animations { + void hide(QObject *obj); + void show(QObject *obj); + void moveTo(QObject *obj, qreal x, qreal y); + void moveTo(QObject *obj, const QPointF &pos); + void animDelete(QObject *obj); + void scaleTo(QObject *obj, qreal scale); +} + +#endif // ANIMATIONFUNCTIONS_H diff --git a/desktop-widgets/profile/divecartesianaxis.cpp b/desktop-widgets/profile/divecartesianaxis.cpp new file mode 100644 index 000000000..bf5a5380c --- /dev/null +++ b/desktop-widgets/profile/divecartesianaxis.cpp @@ -0,0 +1,459 @@ +#include "divecartesianaxis.h" +#include "divetextitem.h" +#include "helpers.h" +#include "preferences.h" +#include "diveplotdatamodel.h" +#include "animationfunctions.h" +#include "mainwindow.h" +#include "divelineitem.h" +#include "profilewidget2.h" + +QPen DiveCartesianAxis::gridPen() +{ + QPen pen; + pen.setColor(getColor(TIME_GRID)); + /* cosmetic width() == 0 for lines in printMode + * having setCosmetic(true) and width() > 0 does not work when + * printing on OSX and Linux */ + pen.setWidth(DiveCartesianAxis::printMode ? 0 : 2); + pen.setCosmetic(true); + return pen; +} + +double DiveCartesianAxis::tickInterval() const +{ + return interval; +} + +double DiveCartesianAxis::tickSize() const +{ + return tick_size; +} + +void DiveCartesianAxis::setFontLabelScale(qreal scale) +{ + labelScale = scale; + changed = true; +} + +void DiveCartesianAxis::setPrintMode(bool mode) +{ + printMode = mode; + // update the QPen of all lines depending on printMode + QPen newPen = gridPen(); + QColor oldColor = pen().brush().color(); + newPen.setBrush(oldColor); + setPen(newPen); + Q_FOREACH (DiveLineItem *item, lines) + item->setPen(pen()); +} + +void DiveCartesianAxis::setMaximum(double maximum) +{ + if (IS_FP_SAME(max, maximum)) + return; + max = maximum; + changed = true; + emit maxChanged(); +} + +void DiveCartesianAxis::setMinimum(double minimum) +{ + if (IS_FP_SAME(min, minimum)) + return; + min = minimum; + changed = true; +} + +void DiveCartesianAxis::setTextColor(const QColor &color) +{ + textColor = color; +} + +DiveCartesianAxis::DiveCartesianAxis() : QObject(), + QGraphicsLineItem(), + printMode(false), + unitSystem(0), + orientation(LeftToRight), + min(0), + max(0), + interval(1), + tick_size(0), + textVisibility(true), + lineVisibility(true), + labelScale(1.0), + line_size(1), + changed(true) +{ + setPen(gridPen()); +} + +DiveCartesianAxis::~DiveCartesianAxis() +{ +} + +void DiveCartesianAxis::setLineSize(qreal lineSize) +{ + line_size = lineSize; + changed = true; +} + +void DiveCartesianAxis::setOrientation(Orientation o) +{ + orientation = o; + changed = true; +} + +QColor DiveCartesianAxis::colorForValue(double value) +{ + return QColor(Qt::black); +} + +void DiveCartesianAxis::setTextVisible(bool arg1) +{ + if (textVisibility == arg1) { + return; + } + textVisibility = arg1; + Q_FOREACH (DiveTextItem *item, labels) { + item->setVisible(textVisibility); + } +} + +void DiveCartesianAxis::setLinesVisible(bool arg1) +{ + if (lineVisibility == arg1) { + return; + } + lineVisibility = arg1; + Q_FOREACH (DiveLineItem *item, lines) { + item->setVisible(lineVisibility); + } +} + +template <typename T> +void emptyList(QList<T *> &list, double steps) +{ + if (!list.isEmpty() && list.size() > steps) { + while (list.size() > steps) { + T *removedItem = list.takeLast(); + Animations::animDelete(removedItem); + } + } +} + +void DiveCartesianAxis::updateTicks(color_indice_t color) +{ + if (!scene() || (!changed && !MainWindow::instance()->graphics()->getPrintMode())) + return; + QLineF m = line(); + // unused so far: + // QGraphicsView *view = scene()->views().first(); + double steps = (max - min) / interval; + double currValueText = min; + double currValueLine = min; + + if (steps < 1) + return; + + emptyList(labels, steps); + emptyList(lines, steps); + + // Move the remaining Ticks / Text to it's corerct position + // Regartind the possibly new values for the Axis + qreal begin, stepSize; + if (orientation == TopToBottom) { + begin = m.y1(); + stepSize = (m.y2() - m.y1()); + } else if (orientation == BottomToTop) { + begin = m.y2(); + stepSize = (m.y2() - m.y1()); + } else if (orientation == LeftToRight) { + begin = m.x1(); + stepSize = (m.x2() - m.x1()); + } else /* if (orientation == RightToLeft) */ { + begin = m.x2(); + stepSize = (m.x2() - m.x1()); + } + stepSize = stepSize / steps; + + for (int i = 0, count = labels.size(); i < count; i++, currValueText += interval) { + qreal childPos = (orientation == TopToBottom || orientation == LeftToRight) ? + begin + i * stepSize : + begin - i * stepSize; + + labels[i]->setText(textForValue(currValueText)); + if (orientation == LeftToRight || orientation == RightToLeft) { + Animations::moveTo(labels[i],childPos, m.y1() + tick_size); + } else { + Animations::moveTo(labels[i],m.x1() - tick_size, childPos); + } + } + + for (int i = 0, count = lines.size(); i < count; i++, currValueLine += interval) { + qreal childPos = (orientation == TopToBottom || orientation == LeftToRight) ? + begin + i * stepSize : + begin - i * stepSize; + + if (orientation == LeftToRight || orientation == RightToLeft) { + Animations::moveTo(lines[i],childPos, m.y1()); + } else { + Animations::moveTo(lines[i],m.x1(), childPos); + } + } + + // Add's the rest of the needed Ticks / Text. + for (int i = labels.size(); i < steps; i++, currValueText += interval) { + qreal childPos; + if (orientation == TopToBottom || orientation == LeftToRight) { + childPos = begin + i * stepSize; + } else { + childPos = begin - i * stepSize; + } + DiveTextItem *label = new DiveTextItem(this); + label->setText(textForValue(currValueText)); + label->setBrush(colorForValue(currValueText)); + label->setScale(fontLabelScale()); + label->setZValue(1); + labels.push_back(label); + if (orientation == RightToLeft || orientation == LeftToRight) { + label->setAlignment(Qt::AlignBottom | Qt::AlignHCenter); + label->setPos(scene()->sceneRect().width() + 10, m.y1() + tick_size); // position it outside of the scene); + Animations::moveTo(label,childPos, m.y1() + tick_size); + } else { + label->setAlignment(Qt::AlignVCenter | Qt::AlignLeft); + label->setPos(m.x1() - tick_size, scene()->sceneRect().height() + 10); + Animations::moveTo(label,m.x1() - tick_size, childPos); + } + } + + // Add's the rest of the needed Ticks / Text. + for (int i = lines.size(); i < steps; i++, currValueText += interval) { + qreal childPos; + if (orientation == TopToBottom || orientation == LeftToRight) { + childPos = begin + i * stepSize; + } else { + childPos = begin - i * stepSize; + } + DiveLineItem *line = new DiveLineItem(this); + QPen pen = gridPen(); + pen.setBrush(getColor(color)); + line->setPen(pen); + line->setZValue(0); + lines.push_back(line); + if (orientation == RightToLeft || orientation == LeftToRight) { + line->setLine(0, -line_size, 0, 0); + line->setPos(scene()->sceneRect().width() + 10, m.y1()); // position it outside of the scene); + Animations::moveTo(line,childPos, m.y1()); + } else { + QPointF p1 = mapFromScene(3, 0); + QPointF p2 = mapFromScene(line_size, 0); + line->setLine(p1.x(), 0, p2.x(), 0); + line->setPos(m.x1(), scene()->sceneRect().height() + 10); + Animations::moveTo(line,m.x1(), childPos); + } + } + + Q_FOREACH (DiveTextItem *item, labels) + item->setVisible(textVisibility); + Q_FOREACH (DiveLineItem *item, lines) + item->setVisible(lineVisibility); + changed = false; +} + +void DiveCartesianAxis::setLine(const QLineF &line) +{ + QGraphicsLineItem::setLine(line); + changed = true; +} + +void DiveCartesianAxis::animateChangeLine(const QLineF &newLine) +{ + setLine(newLine); + updateTicks(); + sizeChanged(); +} + +QString DiveCartesianAxis::textForValue(double value) +{ + return QString::number(value); +} + +void DiveCartesianAxis::setTickSize(qreal size) +{ + tick_size = size; +} + +void DiveCartesianAxis::setTickInterval(double i) +{ + interval = i; +} + +qreal DiveCartesianAxis::valueAt(const QPointF &p) const +{ + QLineF m = line(); + QPointF relativePosition = p; + relativePosition -= pos(); // normalize p based on the axis' offset on screen + + double retValue = (orientation == LeftToRight || orientation == RightToLeft) ? + max * (relativePosition.x() - m.x1()) / (m.x2() - m.x1()) : + max * (relativePosition.y() - m.y1()) / (m.y2() - m.y1()); + return retValue; +} + +qreal DiveCartesianAxis::posAtValue(qreal value) +{ + QLineF m = line(); + QPointF p = pos(); + + double size = max - min; + // unused for now: + // double distanceFromOrigin = value - min; + double percent = IS_FP_SAME(min, max) ? 0.0 : (value - min) / size; + + + double realSize = orientation == LeftToRight || orientation == RightToLeft ? + m.x2() - m.x1() : + m.y2() - m.y1(); + + // Inverted axis, just invert the percentage. + if (orientation == RightToLeft || orientation == BottomToTop) + percent = 1 - percent; + + double retValue = realSize * percent; + double adjusted = + orientation == LeftToRight ? retValue + m.x1() + p.x() : + orientation == RightToLeft ? retValue + m.x1() + p.x() : + orientation == TopToBottom ? retValue + m.y1() + p.y() : + /* entation == BottomToTop */ retValue + m.y1() + p.y(); + return adjusted; +} + +qreal DiveCartesianAxis::percentAt(const QPointF &p) +{ + qreal value = valueAt(p); + double size = max - min; + double percent = value / size; + return percent; +} + +double DiveCartesianAxis::maximum() const +{ + return max; +} + +double DiveCartesianAxis::minimum() const +{ + return min; +} + +double DiveCartesianAxis::fontLabelScale() const +{ + return labelScale; +} + +void DiveCartesianAxis::setColor(const QColor &color) +{ + QPen defaultPen = gridPen(); + defaultPen.setColor(color); + defaultPen.setJoinStyle(Qt::RoundJoin); + defaultPen.setCapStyle(Qt::RoundCap); + setPen(defaultPen); +} + +QString DepthAxis::textForValue(double value) +{ + if (value == 0) + return QString(); + return get_depth_string(value, false, false); +} + +QColor DepthAxis::colorForValue(double value) +{ + Q_UNUSED(value); + return QColor(Qt::red); +} + +DepthAxis::DepthAxis() +{ + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); + changed = true; + settingsChanged(); +} + +void DepthAxis::settingsChanged() +{ + static int unitSystem = prefs.units.length; + if ( unitSystem == prefs.units.length ) + return; + changed = true; + updateTicks(); + unitSystem = prefs.units.length; +} + +QColor TimeAxis::colorForValue(double value) +{ + Q_UNUSED(value); + return QColor(Qt::blue); +} + +QString TimeAxis::textForValue(double value) +{ + int nr = value / 60; + if (maximum() < 600) + return QString("%1:%2").arg(nr).arg((int)value % 60, 2, 10, QChar('0')); + return QString::number(nr); +} + +void TimeAxis::updateTicks() +{ + DiveCartesianAxis::updateTicks(); + if (maximum() > 600) { + for (int i = 0; i < labels.count(); i++) { + labels[i]->setVisible(i % 2); + } + } +} + +QString TemperatureAxis::textForValue(double value) +{ + return QString::number(mkelvin_to_C((int)value)); +} + +PartialGasPressureAxis::PartialGasPressureAxis() : + DiveCartesianAxis(), + model(NULL) +{ + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); +} + +void PartialGasPressureAxis::setModel(DivePlotDataModel *m) +{ + model = m; + connect(model, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(settingsChanged())); + settingsChanged(); +} + +void PartialGasPressureAxis::settingsChanged() +{ + bool showPhe = prefs.pp_graphs.phe; + bool showPn2 = prefs.pp_graphs.pn2; + bool showPo2 = prefs.pp_graphs.po2; + setVisible(showPhe || showPn2 || showPo2); + if (!model->rowCount()) + return; + + double max = showPhe ? model->pheMax() : -1; + if (showPn2 && model->pn2Max() > max) + max = model->pn2Max(); + if (showPo2 && model->po2Max() > max) + max = model->po2Max(); + + qreal pp = floor(max * 10.0) / 10.0 + 0.2; + if (IS_FP_SAME(maximum(), pp)) + return; + + setMaximum(pp); + setTickInterval(pp > 4 ? 0.5 : 0.25); + updateTicks(); +} diff --git a/desktop-widgets/profile/divecartesianaxis.h b/desktop-widgets/profile/divecartesianaxis.h new file mode 100644 index 000000000..cc7d0bcf7 --- /dev/null +++ b/desktop-widgets/profile/divecartesianaxis.h @@ -0,0 +1,122 @@ +#ifndef DIVECARTESIANAXIS_H +#define DIVECARTESIANAXIS_H + +#include <QObject> +#include <QGraphicsLineItem> +#include "subsurface-core/color.h" + +class QPropertyAnimation; +class DiveTextItem; +class DiveLineItem; +class DivePlotDataModel; + +class DiveCartesianAxis : public QObject, public QGraphicsLineItem { + Q_OBJECT + Q_PROPERTY(QLineF line WRITE setLine READ line) + Q_PROPERTY(QPointF pos WRITE setPos READ pos) + Q_PROPERTY(qreal x WRITE setX READ x) + Q_PROPERTY(qreal y WRITE setY READ y) +private: + bool printMode; + QPen gridPen(); +public: + enum Orientation { + TopToBottom, + BottomToTop, + LeftToRight, + RightToLeft + }; + DiveCartesianAxis(); + virtual ~DiveCartesianAxis(); + void setPrintMode(bool mode); + void setMinimum(double minimum); + void setMaximum(double maximum); + void setTickInterval(double interval); + void setOrientation(Orientation orientation); + void setTickSize(qreal size); + void setFontLabelScale(qreal scale); + double minimum() const; + double maximum() const; + double tickInterval() const; + double tickSize() const; + double fontLabelScale() const; + qreal valueAt(const QPointF &p) const; + qreal percentAt(const QPointF &p); + qreal posAtValue(qreal value); + void setColor(const QColor &color); + void setTextColor(const QColor &color); + void animateChangeLine(const QLineF &newLine); + void setTextVisible(bool arg1); + void setLinesVisible(bool arg1); + void setLineSize(qreal lineSize); + void setLine(const QLineF& line); + int unitSystem; +public +slots: + virtual void updateTicks(color_indice_t color = TIME_GRID); + +signals: + void sizeChanged(); + void maxChanged(); + +protected: + virtual QString textForValue(double value); + virtual QColor colorForValue(double value); + Orientation orientation; + QList<DiveTextItem *> labels; + QList<DiveLineItem *> lines; + double min; + double max; + double interval; + double tick_size; + QColor textColor; + bool textVisibility; + bool lineVisibility; + double labelScale; + qreal line_size; + bool changed; +}; + +class DepthAxis : public DiveCartesianAxis { + Q_OBJECT +public: + DepthAxis(); + +protected: + QString textForValue(double value); + QColor colorForValue(double value); +private +slots: + void settingsChanged(); +}; + +class TimeAxis : public DiveCartesianAxis { + Q_OBJECT +public: + virtual void updateTicks(); + +protected: + QString textForValue(double value); + QColor colorForValue(double value); +}; + +class TemperatureAxis : public DiveCartesianAxis { + Q_OBJECT +protected: + QString textForValue(double value); +}; + +class PartialGasPressureAxis : public DiveCartesianAxis { + Q_OBJECT +public: + PartialGasPressureAxis(); + void setModel(DivePlotDataModel *model); +public +slots: + void settingsChanged(); + +private: + DivePlotDataModel *model; +}; + +#endif // DIVECARTESIANAXIS_H diff --git a/desktop-widgets/profile/diveeventitem.cpp b/desktop-widgets/profile/diveeventitem.cpp new file mode 100644 index 000000000..0bbc84267 --- /dev/null +++ b/desktop-widgets/profile/diveeventitem.cpp @@ -0,0 +1,172 @@ +#include "diveeventitem.h" +#include "diveplotdatamodel.h" +#include "divecartesianaxis.h" +#include "animationfunctions.h" +#include "libdivecomputer.h" +#include "profile.h" +#include "gettextfromc.h" +#include "metrics.h" + +extern struct ev_select *ev_namelist; +extern int evn_used; + +DiveEventItem::DiveEventItem(QObject *parent) : DivePixmapItem(parent), + vAxis(NULL), + hAxis(NULL), + dataModel(NULL), + internalEvent(NULL) +{ + setFlag(ItemIgnoresTransformations); +} + + +void DiveEventItem::setHorizontalAxis(DiveCartesianAxis *axis) +{ + hAxis = axis; + recalculatePos(true); +} + +void DiveEventItem::setModel(DivePlotDataModel *model) +{ + dataModel = model; + recalculatePos(true); +} + +void DiveEventItem::setVerticalAxis(DiveCartesianAxis *axis) +{ + vAxis = axis; + recalculatePos(true); + connect(vAxis, SIGNAL(sizeChanged()), this, SLOT(recalculatePos())); +} + +struct event *DiveEventItem::getEvent() +{ + return internalEvent; +} + +void DiveEventItem::setEvent(struct event *ev) +{ + if (!ev) + return; + internalEvent = ev; + setupPixmap(); + setupToolTipString(); + recalculatePos(true); +} + +void DiveEventItem::setupPixmap() +{ + const IconMetrics& metrics = defaultIconMetrics(); + int sz_bigger = metrics.sz_med + metrics.sz_small; // ex 40px + int sz_pix = sz_bigger/2; // ex 20px + +#define EVENT_PIXMAP(PIX) QPixmap(QString(PIX)).scaled(sz_pix, sz_pix, Qt::KeepAspectRatio, Qt::SmoothTransformation) +#define EVENT_PIXMAP_BIGGER(PIX) QPixmap(QString(PIX)).scaled(sz_bigger, sz_bigger, Qt::KeepAspectRatio, Qt::SmoothTransformation) + if (same_string(internalEvent->name, "")) { + setPixmap(EVENT_PIXMAP(":warning")); + } else if (internalEvent->type == SAMPLE_EVENT_BOOKMARK) { + setPixmap(EVENT_PIXMAP(":flag")); + } else if (strcmp(internalEvent->name, "heading") == 0 || + (same_string(internalEvent->name, "SP change") && internalEvent->time.seconds == 0)) { + // 2 cases: + // a) some dive computers have heading in every sample + // b) at t=0 we might have an "SP change" to indicate dive type + // in both cases we want to get the right data into the tooltip but don't want the visual clutter + // so set an "almost invisible" pixmap (a narrow but somewhat tall, basically transparent pixmap) + // that allows tooltips to work when we don't want to show a specific + // pixmap for an event, but want to show the event value in the tooltip + QPixmap transparentPixmap(4, 20); + transparentPixmap.fill(QColor::fromRgbF(1.0, 1.0, 1.0, 0.01)); + setPixmap(transparentPixmap); + } else if (event_is_gaschange(internalEvent)) { + if (internalEvent->gas.mix.he.permille) + setPixmap(EVENT_PIXMAP_BIGGER(":gaschangeTrimix")); + else if (gasmix_is_air(&internalEvent->gas.mix)) + setPixmap(EVENT_PIXMAP_BIGGER(":gaschangeAir")); + else + setPixmap(EVENT_PIXMAP_BIGGER(":gaschangeNitrox")); + } else { + setPixmap(EVENT_PIXMAP(":warning")); + } +#undef EVENT_PIXMAP +} + +void DiveEventItem::setupToolTipString() +{ + // we display the event on screen - so translate + QString name = gettextFromC::instance()->tr(internalEvent->name); + int value = internalEvent->value; + int type = internalEvent->type; + if (value) { + if (event_is_gaschange(internalEvent)) { + name += ": "; + name += gasname(&internalEvent->gas.mix); + + /* Do we have an explicit cylinder index? Show it. */ + if (internalEvent->gas.index >= 0) + name += QString(" (cyl %1)").arg(internalEvent->gas.index+1); + } else if (type == SAMPLE_EVENT_PO2 && name == "SP change") { + name += QString(":%1").arg((double)value / 1000); + } else { + name += QString(":%1").arg(value); + } + } else if (type == SAMPLE_EVENT_PO2 && name == "SP change") { + // this is a bad idea - we are abusing an existing event type that is supposed to + // warn of high or low pOâ‚‚ and are turning it into a set point change event + name += "\n" + tr("Manual switch to OC"); + } else { + name += internalEvent->flags == SAMPLE_FLAGS_BEGIN ? tr(" begin", "Starts with space!") : + internalEvent->flags == SAMPLE_FLAGS_END ? tr(" end", "Starts with space!") : ""; + } + // qDebug() << name; + setToolTip(name); +} + +void DiveEventItem::eventVisibilityChanged(const QString &eventName, bool visible) +{ +} + +bool DiveEventItem::shouldBeHidden() +{ + struct event *event = internalEvent; + + /* + * Some gas change events are special. Some dive computers just tell us the initial gas this way. + * Don't bother showing those + */ + struct sample *first_sample = &get_dive_dc(&displayed_dive, dc_number)->sample[0]; + if (!strcmp(event->name, "gaschange") && + (event->time.seconds == 0 || + (first_sample && event->time.seconds == first_sample->time.seconds))) + return true; + + for (int i = 0; i < evn_used; i++) { + if (!strcmp(event->name, ev_namelist[i].ev_name) && ev_namelist[i].plot_ev == false) + return true; + } + return false; +} + +void DiveEventItem::recalculatePos(bool instant) +{ + if (!vAxis || !hAxis || !internalEvent || !dataModel) + return; + + QModelIndexList result = dataModel->match(dataModel->index(0, DivePlotDataModel::TIME), Qt::DisplayRole, internalEvent->time.seconds); + if (result.isEmpty()) { + Q_ASSERT("can't find a spot in the dataModel"); + hide(); + return; + } + if (!isVisible() && !shouldBeHidden()) + show(); + int depth = dataModel->data(dataModel->index(result.first().row(), DivePlotDataModel::DEPTH)).toInt(); + qreal x = hAxis->posAtValue(internalEvent->time.seconds); + qreal y = vAxis->posAtValue(depth); + if (!instant) + Animations::moveTo(this, x, y); + else + setPos(x, y); + if (isVisible() && shouldBeHidden()) + hide(); +} diff --git a/desktop-widgets/profile/diveeventitem.h b/desktop-widgets/profile/diveeventitem.h new file mode 100644 index 000000000..f358fee6d --- /dev/null +++ b/desktop-widgets/profile/diveeventitem.h @@ -0,0 +1,34 @@ +#ifndef DIVEEVENTITEM_H +#define DIVEEVENTITEM_H + +#include "divepixmapitem.h" + +class DiveCartesianAxis; +class DivePlotDataModel; +struct event; + +class DiveEventItem : public DivePixmapItem { + Q_OBJECT +public: + DiveEventItem(QObject *parent = 0); + void setEvent(struct event *ev); + struct event *getEvent(); + void eventVisibilityChanged(const QString &eventName, bool visible); + void setVerticalAxis(DiveCartesianAxis *axis); + void setHorizontalAxis(DiveCartesianAxis *axis); + void setModel(DivePlotDataModel *model); + bool shouldBeHidden(); +public +slots: + void recalculatePos(bool instant = false); + +private: + void setupToolTipString(); + void setupPixmap(); + DiveCartesianAxis *vAxis; + DiveCartesianAxis *hAxis; + DivePlotDataModel *dataModel; + struct event *internalEvent; +}; + +#endif // DIVEEVENTITEM_H diff --git a/desktop-widgets/profile/divelineitem.cpp b/desktop-widgets/profile/divelineitem.cpp new file mode 100644 index 000000000..f9e288a44 --- /dev/null +++ b/desktop-widgets/profile/divelineitem.cpp @@ -0,0 +1,5 @@ +#include "divelineitem.h" + +DiveLineItem::DiveLineItem(QGraphicsItem *parent) : QGraphicsLineItem(parent) +{ +} diff --git a/desktop-widgets/profile/divelineitem.h b/desktop-widgets/profile/divelineitem.h new file mode 100644 index 000000000..ec88e9da5 --- /dev/null +++ b/desktop-widgets/profile/divelineitem.h @@ -0,0 +1,15 @@ +#ifndef DIVELINEITEM_H +#define DIVELINEITEM_H + +#include <QObject> +#include <QGraphicsLineItem> + +class DiveLineItem : public QObject, public QGraphicsLineItem { + Q_OBJECT + Q_PROPERTY(QPointF pos READ pos WRITE setPos) + Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity) +public: + DiveLineItem(QGraphicsItem *parent = 0); +}; + +#endif // DIVELINEITEM_H diff --git a/desktop-widgets/profile/divepixmapitem.cpp b/desktop-widgets/profile/divepixmapitem.cpp new file mode 100644 index 000000000..581f6f9b4 --- /dev/null +++ b/desktop-widgets/profile/divepixmapitem.cpp @@ -0,0 +1,130 @@ +#include "divepixmapitem.h" +#include "animationfunctions.h" +#include "divepicturemodel.h" +#include <preferences.h> + +#include <QDesktopServices> +#include <QGraphicsView> +#include <QUrl> + +DivePixmapItem::DivePixmapItem(QObject *parent) : QObject(parent), QGraphicsPixmapItem() +{ +} + +DiveButtonItem::DiveButtonItem(QObject *parent): DivePixmapItem(parent) +{ +} + +void DiveButtonItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + QGraphicsItem::mousePressEvent(event); + emit clicked(); +} + +// If we have many many pictures on screen, maybe a shared-pixmap would be better to +// paint on screen, but for now, this. +CloseButtonItem::CloseButtonItem(QObject *parent): DiveButtonItem(parent) +{ + static QPixmap p = QPixmap(":trash"); + setPixmap(p); + setFlag(ItemIgnoresTransformations); +} + +void CloseButtonItem::hide() +{ + DiveButtonItem::hide(); +} + +void CloseButtonItem::show() +{ + DiveButtonItem::show(); +} + +DivePictureItem::DivePictureItem(QObject *parent): DivePixmapItem(parent), + canvas(new QGraphicsRectItem(this)), + shadow(new QGraphicsRectItem(this)) +{ + setFlag(ItemIgnoresTransformations); + setAcceptHoverEvents(true); + setScale(0.2); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); + setVisible(prefs.show_pictures_in_profile); + + canvas->setPen(Qt::NoPen); + canvas->setBrush(QColor(Qt::white)); + canvas->setFlag(ItemStacksBehindParent); + canvas->setZValue(-1); + + shadow->setPos(5,5); + shadow->setPen(Qt::NoPen); + shadow->setBrush(QColor(Qt::lightGray)); + shadow->setFlag(ItemStacksBehindParent); + shadow->setZValue(-2); +} + +void DivePictureItem::settingsChanged() +{ + setVisible(prefs.show_pictures_in_profile); +} + +void DivePictureItem::setPixmap(const QPixmap &pix) +{ + DivePixmapItem::setPixmap(pix); + QRectF r = boundingRect(); + canvas->setRect(0 - 10, 0 -10, r.width() + 20, r.height() + 20); + shadow->setRect(canvas->rect()); +} + +CloseButtonItem *button = NULL; +void DivePictureItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event) +{ + Animations::scaleTo(this, 1.0); + setZValue(5); + + if(!button) { + button = new CloseButtonItem(); + button->setScale(0.2); + button->setZValue(7); + scene()->addItem(button); + } + button->setParentItem(this); + button->setPos(boundingRect().width() - button->boundingRect().width() * 0.2, + boundingRect().height() - button->boundingRect().height() * 0.2); + button->setOpacity(0); + button->show(); + Animations::show(button); + button->disconnect(); + connect(button, SIGNAL(clicked()), this, SLOT(removePicture())); +} + +void DivePictureItem::setFileUrl(const QString &s) +{ + fileUrl = s; +} + +void DivePictureItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) +{ + Animations::scaleTo(this, 0.2); + setZValue(0); + if(button){ + button->setParentItem(NULL); + Animations::hide(button); + } +} + +DivePictureItem::~DivePictureItem(){ + if(button){ + button->setParentItem(NULL); + Animations::hide(button); + } +} + +void DivePictureItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + QDesktopServices::openUrl(QUrl::fromLocalFile(fileUrl)); +} + +void DivePictureItem::removePicture() +{ + DivePictureModel::instance()->removePicture(fileUrl); +} diff --git a/desktop-widgets/profile/divepixmapitem.h b/desktop-widgets/profile/divepixmapitem.h new file mode 100644 index 000000000..02c1523f7 --- /dev/null +++ b/desktop-widgets/profile/divepixmapitem.h @@ -0,0 +1,57 @@ +#ifndef DIVEPIXMAPITEM_H +#define DIVEPIXMAPITEM_H + +#include <QObject> +#include <QGraphicsPixmapItem> + +class DivePixmapItem : public QObject, public QGraphicsPixmapItem { + Q_OBJECT + Q_PROPERTY(qreal opacity WRITE setOpacity READ opacity) + Q_PROPERTY(QPointF pos WRITE setPos READ pos) + Q_PROPERTY(qreal x WRITE setX READ x) + Q_PROPERTY(qreal y WRITE setY READ y) +public: + DivePixmapItem(QObject *parent = 0); +}; + +class DivePictureItem : public DivePixmapItem { + Q_OBJECT + Q_PROPERTY(qreal scale WRITE setScale READ scale) +public: + DivePictureItem(QObject *parent = 0); + virtual ~DivePictureItem(); + void setPixmap(const QPixmap& pix); +public slots: + void settingsChanged(); + void removePicture(); + void setFileUrl(const QString& s); +protected: + void hoverEnterEvent(QGraphicsSceneHoverEvent *event); + void hoverLeaveEvent(QGraphicsSceneHoverEvent *event); + void mousePressEvent(QGraphicsSceneMouseEvent *event); +private: + QString fileUrl; + QGraphicsRectItem *canvas; + QGraphicsRectItem *shadow; +}; + +class DiveButtonItem : public DivePixmapItem { + Q_OBJECT +public: + DiveButtonItem(QObject *parent = 0); +protected: + virtual void mousePressEvent(QGraphicsSceneMouseEvent *event); +signals: + void clicked(); +}; + +class CloseButtonItem : public DiveButtonItem { + Q_OBJECT +public: + CloseButtonItem(QObject *parent = 0); +public slots: + void hide(); + void show(); +}; + +#endif // DIVEPIXMAPITEM_H diff --git a/desktop-widgets/profile/diveprofileitem.cpp b/desktop-widgets/profile/diveprofileitem.cpp new file mode 100644 index 000000000..2c814678a --- /dev/null +++ b/desktop-widgets/profile/diveprofileitem.cpp @@ -0,0 +1,979 @@ +#include "diveprofileitem.h" +#include "diveplotdatamodel.h" +#include "divecartesianaxis.h" +#include "divetextitem.h" +#include "animationfunctions.h" +#include "dive.h" +#include "profile.h" +#include "preferences.h" +#include "diveplannermodel.h" +#include "helpers.h" +#include "libdivecomputer/parser.h" +#include "mainwindow.h" +#include "maintab.h" +#include "profile/profilewidget2.h" +#include "diveplanner.h" + +#include <QSettings> + +AbstractProfilePolygonItem::AbstractProfilePolygonItem() : QObject(), QGraphicsPolygonItem(), hAxis(NULL), vAxis(NULL), dataModel(NULL), hDataColumn(-1), vDataColumn(-1) +{ + setCacheMode(DeviceCoordinateCache); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); +} + +void AbstractProfilePolygonItem::settingsChanged() +{ +} + +void AbstractProfilePolygonItem::setHorizontalAxis(DiveCartesianAxis *horizontal) +{ + hAxis = horizontal; + connect(hAxis, SIGNAL(sizeChanged()), this, SLOT(modelDataChanged())); + modelDataChanged(); +} + +void AbstractProfilePolygonItem::setHorizontalDataColumn(int column) +{ + hDataColumn = column; + modelDataChanged(); +} + +void AbstractProfilePolygonItem::setModel(DivePlotDataModel *model) +{ + dataModel = model; + connect(dataModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(modelDataChanged(QModelIndex, QModelIndex))); + connect(dataModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex, int, int)), this, SLOT(modelDataRemoved(QModelIndex, int, int))); + modelDataChanged(); +} + +void AbstractProfilePolygonItem::modelDataRemoved(const QModelIndex &parent, int from, int to) +{ + setPolygon(QPolygonF()); + qDeleteAll(texts); + texts.clear(); +} + +void AbstractProfilePolygonItem::setVerticalAxis(DiveCartesianAxis *vertical) +{ + vAxis = vertical; + connect(vAxis, SIGNAL(sizeChanged()), this, SLOT(modelDataChanged())); + connect(vAxis, SIGNAL(maxChanged()), this, SLOT(modelDataChanged())); + modelDataChanged(); +} + +void AbstractProfilePolygonItem::setVerticalDataColumn(int column) +{ + vDataColumn = column; + modelDataChanged(); +} + +bool AbstractProfilePolygonItem::shouldCalculateStuff(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + if (!hAxis || !vAxis) + return false; + if (!dataModel || dataModel->rowCount() == 0) + return false; + if (hDataColumn == -1 || vDataColumn == -1) + return false; + if (topLeft.isValid() && bottomRight.isValid()) { + if ((topLeft.column() >= vDataColumn || topLeft.column() >= hDataColumn) && + (bottomRight.column() <= vDataColumn || topLeft.column() <= hDataColumn)) { + return true; + } + } + return true; +} + +void AbstractProfilePolygonItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + // We don't have enougth data to calculate things, quit. + + // Calculate the polygon. This is the polygon that will be painted on screen + // on the ::paint method. Here we calculate the correct position of the points + // regarting our cartesian plane ( made by the hAxis and vAxis ), the QPolygonF + // is an array of QPointF's, so we basically get the point from the model, convert + // to our coordinates, store. no painting is done here. + QPolygonF poly; + for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; i++) { + qreal horizontalValue = dataModel->index(i, hDataColumn).data().toReal(); + qreal verticalValue = dataModel->index(i, vDataColumn).data().toReal(); + QPointF point(hAxis->posAtValue(horizontalValue), vAxis->posAtValue(verticalValue)); + poly.append(point); + } + setPolygon(poly); + + qDeleteAll(texts); + texts.clear(); +} + +DiveProfileItem::DiveProfileItem() : show_reported_ceiling(0), reported_ceiling_in_red(0) +{ +} + +void DiveProfileItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + Q_UNUSED(widget); + if (polygon().isEmpty()) + return; + + painter->save(); + // This paints the Polygon + Background. I'm setting the pen to QPen() so we don't get a black line here, + // after all we need to plot the correct velocities colors later. + setPen(Qt::NoPen); + QGraphicsPolygonItem::paint(painter, option, widget); + + // Here we actually paint the boundaries of the Polygon using the colors that the model provides. + // Those are the speed colors of the dives. + QPen pen; + pen.setCosmetic(true); + pen.setWidth(2); + QPolygonF poly = polygon(); + // This paints the colors of the velocities. + for (int i = 1, count = dataModel->rowCount(); i < count; i++) { + QModelIndex colorIndex = dataModel->index(i, DivePlotDataModel::COLOR); + pen.setBrush(QBrush(colorIndex.data(Qt::BackgroundRole).value<QColor>())); + painter->setPen(pen); + painter->drawLine(poly[i - 1], poly[i]); + } + painter->restore(); +} + +int DiveProfileItem::maxCeiling(int row) +{ + int max = -1; + plot_data *entry = dataModel->data().entry + row; + for (int tissue = 0; tissue < 16; tissue++) { + if (max < entry->ceilings[tissue]) + max = entry->ceilings[tissue]; + } + return max; +} + +void DiveProfileItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + bool eventAdded = false; + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + AbstractProfilePolygonItem::modelDataChanged(topLeft, bottomRight); + if (polygon().isEmpty()) + return; + + show_reported_ceiling = prefs.dcceiling; + reported_ceiling_in_red = prefs.redceiling; + profileColor = getColor(DEPTH_BOTTOM); + + int currState = qobject_cast<ProfileWidget2 *>(scene()->views().first())->currentState; + if (currState == ProfileWidget2::PLAN) { + plot_data *entry = dataModel->data().entry; + for (int i = 0; i < dataModel->rowCount(); i++, entry++) { + int max = maxCeiling(i); + // Don't scream if we violate the ceiling by a few cm + if (entry->depth < max - 100 && entry->sec > 0) { + profileColor = QColor(Qt::red); + if (!eventAdded) { + add_event(&displayed_dive.dc, entry->sec, SAMPLE_EVENT_CEILING, -1, max / 1000, "planned waypoint above ceiling"); + eventAdded = true; + } + } + } + } + + /* Show any ceiling we may have encountered */ + if (prefs.dcceiling && !prefs.redceiling) { + QPolygonF p = polygon(); + plot_data *entry = dataModel->data().entry + dataModel->rowCount() - 1; + for (int i = dataModel->rowCount() - 1; i >= 0; i--, entry--) { + if (!entry->in_deco) { + /* not in deco implies this is a safety stop, no ceiling */ + p.append(QPointF(hAxis->posAtValue(entry->sec), vAxis->posAtValue(0))); + } else { + p.append(QPointF(hAxis->posAtValue(entry->sec), vAxis->posAtValue(qMin(entry->stopdepth, entry->depth)))); + } + } + setPolygon(p); + } + + // This is the blueish gradient that the Depth Profile should have. + // It's a simple QLinearGradient with 2 stops, starting from top to bottom. + QLinearGradient pat(0, polygon().boundingRect().top(), 0, polygon().boundingRect().bottom()); + pat.setColorAt(1, profileColor); + pat.setColorAt(0, getColor(DEPTH_TOP)); + setBrush(QBrush(pat)); + + int last = -1; + for (int i = 0, count = dataModel->rowCount(); i < count; i++) { + + struct plot_data *entry = dataModel->data().entry + i; + if (entry->depth < 2000) + continue; + + if ((entry == entry->max[2]) && entry->depth / 100 != last) { + plot_depth_sample(entry, Qt::AlignHCenter | Qt::AlignBottom, getColor(SAMPLE_DEEP)); + last = entry->depth / 100; + } + + if ((entry == entry->min[2]) && entry->depth / 100 != last) { + plot_depth_sample(entry, Qt::AlignHCenter | Qt::AlignTop, getColor(SAMPLE_SHALLOW)); + last = entry->depth / 100; + } + + if (entry->depth != last) + last = -1; + } +} + +void DiveProfileItem::settingsChanged() +{ + //TODO: Only modelDataChanged() here if we need to rebuild the graph ( for instance, + // if the prefs.dcceiling are enabled, but prefs.redceiling is disabled + // and only if it changed something. let's not waste cpu cycles repoloting something we don't need to. + modelDataChanged(); +} + +void DiveProfileItem::plot_depth_sample(struct plot_data *entry, QFlags<Qt::AlignmentFlag> flags, const QColor &color) +{ + int decimals; + double d = get_depth_units(entry->depth, &decimals, NULL); + DiveTextItem *item = new DiveTextItem(this); + item->setPos(hAxis->posAtValue(entry->sec), vAxis->posAtValue(entry->depth)); + item->setText(QString("%1").arg(d, 0, 'f', 1)); + item->setAlignment(flags); + item->setBrush(color); + texts.append(item); +} + +DiveHeartrateItem::DiveHeartrateItem() +{ + QPen pen; + pen.setBrush(QBrush(getColor(::HR_PLOT))); + pen.setCosmetic(true); + pen.setWidth(1); + setPen(pen); + settingsChanged(); +} + +void DiveHeartrateItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + int last = -300, last_printed_hr = 0, sec = 0; + struct { + int sec; + int hr; + } hist[3] = {}; + + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + qDeleteAll(texts); + texts.clear(); + // Ignore empty values. a heartrate of 0 would be a bad sign. + QPolygonF poly; + for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; i++) { + int hr = dataModel->index(i, vDataColumn).data().toInt(); + if (!hr) + continue; + sec = dataModel->index(i, hDataColumn).data().toInt(); + QPointF point(hAxis->posAtValue(sec), vAxis->posAtValue(hr)); + poly.append(point); + if (hr == hist[2].hr) + // same as last one, no point in looking at printing + continue; + hist[0] = hist[1]; + hist[1] = hist[2]; + hist[2].sec = sec; + hist[2].hr = hr; + // don't print a HR + // if it's not a local min / max + // if it's been less than 5min and less than a 20 beats change OR + // if it's been less than 2min OR if the change from the + // last print is less than 10 beats + // to test min / max requires three points, so we now look at the + // previous one + sec = hist[1].sec; + hr = hist[1].hr; + if ((hist[0].hr < hr && hr < hist[2].hr) || + (hist[0].hr > hr && hr > hist[2].hr) || + ((sec < last + 300) && (abs(hr - last_printed_hr) < 20)) || + (sec < last + 120) || + (abs(hr - last_printed_hr) < 10)) + continue; + last = sec; + createTextItem(sec, hr); + last_printed_hr = hr; + } + setPolygon(poly); + + if (texts.count()) + texts.last()->setAlignment(Qt::AlignLeft | Qt::AlignBottom); +} + +void DiveHeartrateItem::createTextItem(int sec, int hr) +{ + DiveTextItem *text = new DiveTextItem(this); + text->setAlignment(Qt::AlignRight | Qt::AlignBottom); + text->setBrush(getColor(HR_TEXT)); + text->setPos(QPointF(hAxis->posAtValue(sec), vAxis->posAtValue(hr))); + text->setScale(0.7); // need to call this BEFORE setText() + text->setText(QString("%1").arg(hr)); + texts.append(text); +} + +void DiveHeartrateItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + painter->save(); + painter->setPen(pen()); + painter->drawPolyline(polygon()); + painter->restore(); +} + +void DiveHeartrateItem::settingsChanged() +{ + setVisible(prefs.hrgraph); +} + +DivePercentageItem::DivePercentageItem(int i) +{ + QPen pen; + QColor color; + color.setHsl(100 + 10 * i, 200, 100); + pen.setBrush(QBrush(color)); + pen.setCosmetic(true); + pen.setWidth(1); + setPen(pen); + settingsChanged(); +} + +void DivePercentageItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + int sec = 0; + + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + // Ignore empty values. a heartrate of 0 would be a bad sign. + QPolygonF poly; + for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; i++) { + int hr = dataModel->index(i, vDataColumn).data().toInt(); + if (!hr) + continue; + sec = dataModel->index(i, hDataColumn).data().toInt(); + QPointF point(hAxis->posAtValue(sec), vAxis->posAtValue(hr)); + poly.append(point); + } + setPolygon(poly); + + if (texts.count()) + texts.last()->setAlignment(Qt::AlignLeft | Qt::AlignBottom); +} + +void DivePercentageItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + painter->save(); + painter->setPen(pen()); + painter->drawPolyline(polygon()); + painter->restore(); +} + +void DivePercentageItem::settingsChanged() +{ + setVisible(prefs.percentagegraph); +} + +DiveAmbPressureItem::DiveAmbPressureItem() +{ + QPen pen; + pen.setBrush(QBrush(getColor(::AMB_PRESSURE_LINE))); + pen.setCosmetic(true); + pen.setWidth(2); + setPen(pen); + settingsChanged(); +} + +void DiveAmbPressureItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + int sec = 0; + + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + // Ignore empty values. a heartrate of 0 would be a bad sign. + QPolygonF poly; + for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; i++) { + int hr = dataModel->index(i, vDataColumn).data().toInt(); + if (!hr) + continue; + sec = dataModel->index(i, hDataColumn).data().toInt(); + QPointF point(hAxis->posAtValue(sec), vAxis->posAtValue(hr)); + poly.append(point); + } + setPolygon(poly); + + if (texts.count()) + texts.last()->setAlignment(Qt::AlignLeft | Qt::AlignBottom); +} + +void DiveAmbPressureItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + painter->save(); + painter->setPen(pen()); + painter->drawPolyline(polygon()); + painter->restore(); +} + +void DiveAmbPressureItem::settingsChanged() +{ + setVisible(prefs.percentagegraph); +} + +DiveGFLineItem::DiveGFLineItem() +{ + QPen pen; + pen.setBrush(QBrush(getColor(::GF_LINE))); + pen.setCosmetic(true); + pen.setWidth(2); + setPen(pen); + settingsChanged(); +} + +void DiveGFLineItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + int sec = 0; + + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + // Ignore empty values. a heartrate of 0 would be a bad sign. + QPolygonF poly; + for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; i++) { + int hr = dataModel->index(i, vDataColumn).data().toInt(); + if (!hr) + continue; + sec = dataModel->index(i, hDataColumn).data().toInt(); + QPointF point(hAxis->posAtValue(sec), vAxis->posAtValue(hr)); + poly.append(point); + } + setPolygon(poly); + + if (texts.count()) + texts.last()->setAlignment(Qt::AlignLeft | Qt::AlignBottom); +} + +void DiveGFLineItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + painter->save(); + painter->setPen(pen()); + painter->drawPolyline(polygon()); + painter->restore(); +} + +void DiveGFLineItem::settingsChanged() +{ + setVisible(prefs.percentagegraph); +} + +DiveTemperatureItem::DiveTemperatureItem() +{ + QPen pen; + pen.setBrush(QBrush(getColor(::TEMP_PLOT))); + pen.setCosmetic(true); + pen.setWidth(2); + setPen(pen); +} + +void DiveTemperatureItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + int last = -300, last_printed_temp = 0, sec = 0, last_valid_temp = 0; + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + qDeleteAll(texts); + texts.clear(); + // Ignore empty values. things do not look good with '0' as temperature in kelvin... + QPolygonF poly; + for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; i++) { + int mkelvin = dataModel->index(i, vDataColumn).data().toInt(); + if (!mkelvin) + continue; + last_valid_temp = mkelvin; + sec = dataModel->index(i, hDataColumn).data().toInt(); + QPointF point(hAxis->posAtValue(sec), vAxis->posAtValue(mkelvin)); + poly.append(point); + + /* don't print a temperature + * if it's been less than 5min and less than a 2K change OR + * if it's been less than 2min OR if the change from the + * last print is less than .4K (and therefore less than 1F) */ + if (((sec < last + 300) && (abs(mkelvin - last_printed_temp) < 2000)) || + (sec < last + 120) || + (abs(mkelvin - last_printed_temp) < 400)) + continue; + last = sec; + if (mkelvin > 200000) + createTextItem(sec, mkelvin); + last_printed_temp = mkelvin; + } + setPolygon(poly); + + /* it would be nice to print the end temperature, if it's + * different or if the last temperature print has been more + * than a quarter of the dive back */ + if (last_valid_temp > 200000 && + ((abs(last_valid_temp - last_printed_temp) > 500) || ((double)last / (double)sec < 0.75))) { + createTextItem(sec, last_valid_temp); + } + if (texts.count()) + texts.last()->setAlignment(Qt::AlignLeft | Qt::AlignBottom); +} + +void DiveTemperatureItem::createTextItem(int sec, int mkelvin) +{ + double deg; + const char *unit; + deg = get_temp_units(mkelvin, &unit); + + DiveTextItem *text = new DiveTextItem(this); + text->setAlignment(Qt::AlignRight | Qt::AlignBottom); + text->setBrush(getColor(TEMP_TEXT)); + text->setPos(QPointF(hAxis->posAtValue(sec), vAxis->posAtValue(mkelvin))); + text->setScale(0.8); // need to call this BEFORE setText() + text->setText(QString("%1%2").arg(deg, 0, 'f', 1).arg(unit)); + texts.append(text); +} + +void DiveTemperatureItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + painter->save(); + painter->setPen(pen()); + painter->drawPolyline(polygon()); + painter->restore(); +} + +DiveMeanDepthItem::DiveMeanDepthItem() +{ + QPen pen; + pen.setBrush(QBrush(getColor(::HR_AXIS))); + pen.setCosmetic(true); + pen.setWidth(2); + setPen(pen); + settingsChanged(); +} + +void DiveMeanDepthItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + double meandepthvalue = 0.0; + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + QPolygonF poly; + plot_data *entry = dataModel->data().entry; + for (int i = 0, modelDataCount = dataModel->rowCount(); i < modelDataCount; i++, entry++) { + // Ignore empty values + if (entry->running_sum == 0 || entry->sec == 0) + continue; + + meandepthvalue = entry->running_sum / entry->sec; + QPointF point(hAxis->posAtValue(entry->sec), vAxis->posAtValue(meandepthvalue)); + poly.append(point); + } + lastRunningSum = meandepthvalue; + setPolygon(poly); + createTextItem(); +} + + +void DiveMeanDepthItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + painter->save(); + painter->setPen(pen()); + painter->drawPolyline(polygon()); + painter->restore(); +} + +void DiveMeanDepthItem::settingsChanged() +{ + setVisible(prefs.show_average_depth); +} + +void DiveMeanDepthItem::createTextItem() { + plot_data *entry = dataModel->data().entry; + int sec = entry[dataModel->rowCount()-1].sec; + qDeleteAll(texts); + texts.clear(); + int decimals; + const char *unitText; + double d = get_depth_units(lastRunningSum, &decimals, &unitText); + DiveTextItem *text = new DiveTextItem(this); + text->setAlignment(Qt::AlignRight | Qt::AlignTop); + text->setBrush(getColor(TEMP_TEXT)); + text->setPos(QPointF(hAxis->posAtValue(sec) + 1, vAxis->posAtValue(lastRunningSum))); + text->setScale(0.8); // need to call this BEFORE setText() + text->setText(QString("%1%2").arg(d, 0, 'f', 1).arg(unitText)); + texts.append(text); +} + +void DiveGasPressureItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + int last_index = -1; + int o2mbar; + QPolygonF boundingPoly, o2Poly; // This is the "Whole Item", but a pressure can be divided in N Polygons. + polygons.clear(); + if (displayed_dive.dc.divemode == CCR) + polygons.append(o2Poly); + + for (int i = 0, count = dataModel->rowCount(); i < count; i++) { + o2mbar = 0; + plot_data *entry = dataModel->data().entry + i; + int mbar = GET_PRESSURE(entry); + if (displayed_dive.dc.divemode == CCR) + o2mbar = GET_O2CYLINDER_PRESSURE(entry); + + if (entry->cylinderindex != last_index) { + polygons.append(QPolygonF()); // this is the polygon that will be actually drawn on screen. + last_index = entry->cylinderindex; + } + if (!mbar) { + continue; + } + if (o2mbar) { + QPointF o2point(hAxis->posAtValue(entry->sec), vAxis->posAtValue(o2mbar)); + boundingPoly.push_back(o2point); + polygons.first().push_back(o2point); + } + + QPointF point(hAxis->posAtValue(entry->sec), vAxis->posAtValue(mbar)); + boundingPoly.push_back(point); // The BoundingRect + polygons.last().push_back(point); // The polygon thta will be plotted. + } + setPolygon(boundingPoly); + qDeleteAll(texts); + texts.clear(); + int mbar, cyl; + int seen_cyl[MAX_CYLINDERS] = { false, }; + int last_pressure[MAX_CYLINDERS] = { 0, }; + int last_time[MAX_CYLINDERS] = { 0, }; + struct plot_data *entry; + + cyl = -1; + o2mbar = 0; + + double print_y_offset[8][2] = { { 0, -0.5 }, { 0, -0.5 }, { 0, -0.5 }, { 0, -0.5 }, { 0, -0.5 } ,{ 0, -0.5 }, { 0, -0.5 }, { 0, -0.5 } }; + // CCR dives: These are offset values used to print the gas lables and pressures on a CCR dive profile at + // appropriate Y-coordinates: One doublet of values for each of 8 cylinders. + // Order of offsets within a doublet: gas lable offset; gas pressure offset. + // The array is initialised with default values that apply to non-CCR dives. + + bool offsets_initialised = false; + int o2cyl = -1, dilcyl = -1; + QFlags<Qt::AlignmentFlag> alignVar= Qt::AlignTop, align_dil = Qt::AlignBottom, align_o2 = Qt::AlignTop; + double axisRange = (vAxis->maximum() - vAxis->minimum())/1000; // Convert axis pressure range to bar + double axisLog = log10(log10(axisRange)); + for (int i = 0, count = dataModel->rowCount(); i < count; i++) { + entry = dataModel->data().entry + i; + mbar = GET_PRESSURE(entry); + if (displayed_dive.dc.divemode == CCR && displayed_dive.oxygen_cylinder_index >= 0) + o2mbar = GET_O2CYLINDER_PRESSURE(entry); + + if (o2mbar) { // If there is an o2mbar value then this is a CCR dive. Then do: + // The first time an o2 value is detected, see if the oxygen cyl pressure graph starts above or below the dil graph + if (!offsets_initialised) { // Initialise the parameters for placing the text correctly near the graph line: + o2cyl = displayed_dive.oxygen_cylinder_index; + dilcyl = displayed_dive.diluent_cylinder_index; + if ((o2mbar > mbar)) { // If above, write o2 start cyl pressure above graph and diluent pressure below graph: + print_y_offset[o2cyl][0] = -7 * axisLog; // y offset for oxygen gas lable (above); pressure offsets=-0.5, already initialised + print_y_offset[dilcyl][0] = 5 * axisLog; // y offset for diluent gas lable (below) + } else { // ... else write o2 start cyl pressure below graph: + print_y_offset[o2cyl][0] = 5 * axisLog; // o2 lable & pressure below graph; pressure offsets=-0.5, already initialised + print_y_offset[dilcyl][0] = -7.8 * axisLog; // and diluent lable above graph. + align_dil = Qt::AlignTop; + align_o2 = Qt::AlignBottom; + } + offsets_initialised = true; + } + + if (!seen_cyl[displayed_dive.oxygen_cylinder_index]) { //For o2, on the left of profile, write lable and pressure + plotPressureValue(o2mbar, entry->sec, align_o2, print_y_offset[o2cyl][1]); + plotGasValue(o2mbar, entry->sec, displayed_dive.cylinder[displayed_dive.oxygen_cylinder_index].gasmix, align_o2, print_y_offset[o2cyl][0]); + seen_cyl[displayed_dive.oxygen_cylinder_index] = true; + } + last_pressure[displayed_dive.oxygen_cylinder_index] = o2mbar; + last_time[displayed_dive.oxygen_cylinder_index] = entry->sec; + alignVar = align_dil; + } + + if (!mbar) + continue; + + if (cyl != entry->cylinderindex) { // Pressure value near the left hand edge of the profile - other cylinders: + cyl = entry->cylinderindex; // For each other cylinder, write the gas lable and pressure + if (!seen_cyl[cyl]) { + plotPressureValue(mbar, entry->sec, alignVar, print_y_offset[cyl][1]); + plotGasValue(mbar, entry->sec, displayed_dive.cylinder[cyl].gasmix, align_dil, print_y_offset[cyl][0]); + seen_cyl[cyl] = true; + } + } + last_pressure[cyl] = mbar; + last_time[cyl] = entry->sec; + } + + for (cyl = 0; cyl < MAX_CYLINDERS; cyl++) { // For each cylinder, on right hand side of profile, write cylinder pressure + alignVar = ((o2cyl >= 0) && (cyl == displayed_dive.oxygen_cylinder_index)) ? align_o2 : align_dil; + if (last_time[cyl]) { + plotPressureValue(last_pressure[cyl], last_time[cyl], (alignVar | Qt::AlignLeft), print_y_offset[cyl][1]); + } + } +} + +void DiveGasPressureItem::plotPressureValue(int mbar, int sec, QFlags<Qt::AlignmentFlag> align, double pressure_offset) +{ + const char *unit; + int pressure = get_pressure_units(mbar, &unit); + DiveTextItem *text = new DiveTextItem(this); + text->setPos(hAxis->posAtValue(sec), vAxis->posAtValue(mbar) + pressure_offset ); + text->setText(QString("%1 %2").arg(pressure).arg(unit)); + text->setAlignment(align); + text->setBrush(getColor(PRESSURE_TEXT)); + texts.push_back(text); +} + +void DiveGasPressureItem::plotGasValue(int mbar, int sec, struct gasmix gasmix, QFlags<Qt::AlignmentFlag> align, double gasname_offset) +{ + QString gas = get_gas_string(gasmix); + DiveTextItem *text = new DiveTextItem(this); + text->setPos(hAxis->posAtValue(sec), vAxis->posAtValue(mbar) + gasname_offset ); + text->setText(gas); + text->setAlignment(align); + text->setBrush(getColor(PRESSURE_TEXT)); + texts.push_back(text); +} + +void DiveGasPressureItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + QPen pen; + pen.setCosmetic(true); + pen.setWidth(2); + painter->save(); + struct plot_data *entry; + Q_FOREACH (const QPolygonF &poly, polygons) { + entry = dataModel->data().entry; + for (int i = 1, count = poly.count(); i < count; i++, entry++) { + if (entry->sac) + pen.setBrush(getSacColor(entry->sac, displayed_dive.sac)); + else + pen.setBrush(MED_GRAY_HIGH_TRANS); + painter->setPen(pen); + painter->drawLine(poly[i - 1], poly[i]); + } + } + painter->restore(); +} + +DiveCalculatedCeiling::DiveCalculatedCeiling() : is3mIncrement(false) +{ + settingsChanged(); +} + +void DiveCalculatedCeiling::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + if (MainWindow::instance()->information()) + connect(MainWindow::instance()->information(), SIGNAL(dateTimeChanged()), this, SLOT(recalc()), Qt::UniqueConnection); + + // We don't have enougth data to calculate things, quit. + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + AbstractProfilePolygonItem::modelDataChanged(topLeft, bottomRight); + // Add 2 points to close the polygon. + QPolygonF poly = polygon(); + if (poly.isEmpty()) + return; + QPointF p1 = poly.first(); + QPointF p2 = poly.last(); + + poly.prepend(QPointF(p1.x(), vAxis->posAtValue(0))); + poly.append(QPointF(p2.x(), vAxis->posAtValue(0))); + setPolygon(poly); + + QLinearGradient pat(0, polygon().boundingRect().top(), 0, polygon().boundingRect().bottom()); + pat.setColorAt(0, getColor(CALC_CEILING_SHALLOW)); + pat.setColorAt(1, getColor(CALC_CEILING_DEEP)); + setPen(QPen(QBrush(Qt::NoBrush), 0)); + setBrush(pat); +} + +void DiveCalculatedCeiling::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + QGraphicsPolygonItem::paint(painter, option, widget); +} + +DiveCalculatedTissue::DiveCalculatedTissue() +{ + settingsChanged(); +} + +void DiveCalculatedTissue::settingsChanged() +{ + setVisible(prefs.calcalltissues && prefs.calcceiling); +} + +void DiveReportedCeiling::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + QPolygonF p; + p.append(QPointF(hAxis->posAtValue(0), vAxis->posAtValue(0))); + plot_data *entry = dataModel->data().entry; + for (int i = 0, count = dataModel->rowCount(); i < count; i++, entry++) { + if (entry->in_deco && entry->stopdepth) { + p.append(QPointF(hAxis->posAtValue(entry->sec), vAxis->posAtValue(qMin(entry->stopdepth, entry->depth)))); + } else { + p.append(QPointF(hAxis->posAtValue(entry->sec), vAxis->posAtValue(0))); + } + } + setPolygon(p); + QLinearGradient pat(0, p.boundingRect().top(), 0, p.boundingRect().bottom()); + // does the user want the ceiling in "surface color" or in red? + if (prefs.redceiling) { + pat.setColorAt(0, getColor(CEILING_SHALLOW)); + pat.setColorAt(1, getColor(CEILING_DEEP)); + } else { + pat.setColorAt(0, getColor(BACKGROUND_TRANS)); + pat.setColorAt(1, getColor(BACKGROUND_TRANS)); + } + setPen(QPen(QBrush(Qt::NoBrush), 0)); + setBrush(pat); +} + +void DiveCalculatedCeiling::recalc() +{ + dataModel->calculateDecompression(); +} + +void DiveCalculatedCeiling::settingsChanged() +{ + if (dataModel && is3mIncrement != prefs.calcceiling3m) { + // recalculate that part. + recalc(); + } + is3mIncrement = prefs.calcceiling3m; + setVisible(prefs.calcceiling); +} + +void DiveReportedCeiling::settingsChanged() +{ + setVisible(prefs.dcceiling); +} + +void DiveReportedCeiling::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (polygon().isEmpty()) + return; + QGraphicsPolygonItem::paint(painter, option, widget); +} + +void PartialPressureGasItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + //AbstractProfilePolygonItem::modelDataChanged(); + if (!shouldCalculateStuff(topLeft, bottomRight)) + return; + + plot_data *entry = dataModel->data().entry; + QPolygonF poly; + QPolygonF alertpoly; + alertPolygons.clear(); + QSettings s; + s.beginGroup("TecDetails"); + double threshold = 0.0; + if (thresholdPtr) + threshold = *thresholdPtr; + bool inAlertFragment = false; + for (int i = 0; i < dataModel->rowCount(); i++, entry++) { + double value = dataModel->index(i, vDataColumn).data().toDouble(); + int time = dataModel->index(i, hDataColumn).data().toInt(); + QPointF point(hAxis->posAtValue(time), vAxis->posAtValue(value)); + poly.push_back(point); + if (value >= threshold) { + if (inAlertFragment) { + alertPolygons.back().push_back(point); + } else { + alertpoly.clear(); + alertpoly.push_back(point); + alertPolygons.append(alertpoly); + inAlertFragment = true; + } + } else { + inAlertFragment = false; + } + } + setPolygon(poly); + /* + createPPLegend(trUtf8("pN" UTF8_SUBSCRIPT_2),getColor(PN2), legendPos); + */ +} + +void PartialPressureGasItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + const qreal pWidth = 0.0; + painter->save(); + painter->setPen(QPen(normalColor, pWidth)); + painter->drawPolyline(polygon()); + + QPolygonF poly; + painter->setPen(QPen(alertColor, pWidth)); + Q_FOREACH (const QPolygonF &poly, alertPolygons) + painter->drawPolyline(poly); + painter->restore(); +} + +void PartialPressureGasItem::setThreshouldSettingsKey(double *prefPointer) +{ + thresholdPtr = prefPointer; +} + +PartialPressureGasItem::PartialPressureGasItem() : + thresholdPtr(NULL) +{ +} + +void PartialPressureGasItem::settingsChanged() +{ + QSettings s; + s.beginGroup("TecDetails"); + setVisible(s.value(visibilityKey).toBool()); +} + +void PartialPressureGasItem::setVisibilitySettingsKey(const QString &key) +{ + visibilityKey = key; +} + +void PartialPressureGasItem::setColors(const QColor &normal, const QColor &alert) +{ + normalColor = normal; + alertColor = alert; +} diff --git a/desktop-widgets/profile/diveprofileitem.h b/desktop-widgets/profile/diveprofileitem.h new file mode 100644 index 000000000..0bba7f7a3 --- /dev/null +++ b/desktop-widgets/profile/diveprofileitem.h @@ -0,0 +1,225 @@ +#ifndef DIVEPROFILEITEM_H +#define DIVEPROFILEITEM_H + +#include <QObject> +#include <QGraphicsPolygonItem> +#include <QModelIndex> + +#include "divelineitem.h" + +/* This is the Profile Item, it should be used for quite a lot of things + on the profile view. The usage should be pretty simple: + + DiveProfileItem *profile = new DiveProfileItem(); + profile->setVerticalAxis( profileYAxis ); + profile->setHorizontalAxis( timeAxis ); + profile->setModel( DiveDataModel ); + profile->setHorizontalDataColumn( DiveDataModel::TIME ); + profile->setVerticalDataColumn( DiveDataModel::DEPTH ); + scene()->addItem(profile); + + This is a generically item and should be used as a base for others, I think... +*/ + +class DivePlotDataModel; +class DiveTextItem; +class DiveCartesianAxis; +class QAbstractTableModel; +struct plot_data; + +class AbstractProfilePolygonItem : public QObject, public QGraphicsPolygonItem { + Q_OBJECT + Q_PROPERTY(QPointF pos WRITE setPos READ pos) + Q_PROPERTY(qreal x WRITE setX READ x) + Q_PROPERTY(qreal y WRITE setY READ y) +public: + AbstractProfilePolygonItem(); + void setVerticalAxis(DiveCartesianAxis *vertical); + void setHorizontalAxis(DiveCartesianAxis *horizontal); + void setModel(DivePlotDataModel *model); + void setHorizontalDataColumn(int column); + void setVerticalDataColumn(int column); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0) = 0; + virtual void clear() + { + } +public +slots: + virtual void settingsChanged(); + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void modelDataRemoved(const QModelIndex &parent, int from, int to); + +protected: + /* when the model emits a 'datachanged' signal, this method below should be used to check if the + * modified data affects this particular item ( for example, when setting the '3m increment' + * the data for Ceiling and tissues will be changed, and only those. so, the topLeft will be the CEILING + * column and the bottomRight will have the TISSUE_16 column. this method takes the vDataColumn and hDataColumn + * into consideration when returning 'true' for "yes, continue the calculation', and 'false' for + * 'do not recalculate, we already have the right data. + */ + bool shouldCalculateStuff(const QModelIndex &topLeft, const QModelIndex &bottomRight); + + DiveCartesianAxis *hAxis; + DiveCartesianAxis *vAxis; + DivePlotDataModel *dataModel; + int hDataColumn; + int vDataColumn; + QList<DiveTextItem *> texts; +}; + +class DiveProfileItem : public AbstractProfilePolygonItem { + Q_OBJECT + +public: + DiveProfileItem(); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void settingsChanged(); + void plot_depth_sample(struct plot_data *entry, QFlags<Qt::AlignmentFlag> flags, const QColor &color); + int maxCeiling(int row); + +private: + unsigned int show_reported_ceiling; + unsigned int reported_ceiling_in_red; + QColor profileColor; +}; + +class DiveMeanDepthItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + DiveMeanDepthItem(); + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + virtual void settingsChanged(); + +private: + void createTextItem(); + double lastRunningSum; + QString visibilityKey; +}; + +class DiveTemperatureItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + DiveTemperatureItem(); + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + +private: + void createTextItem(int seconds, int mkelvin); +}; + +class DiveHeartrateItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + DiveHeartrateItem(); + virtual void modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + virtual void settingsChanged(); + +private: + void createTextItem(int seconds, int hr); + QString visibilityKey; +}; + +class DivePercentageItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + DivePercentageItem(int i); + virtual void modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + virtual void settingsChanged(); + +private: + QString visibilityKey; +}; + +class DiveAmbPressureItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + DiveAmbPressureItem(); + virtual void modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + virtual void settingsChanged(); + +private: + QString visibilityKey; +}; + +class DiveGFLineItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + DiveGFLineItem(); + virtual void modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + virtual void settingsChanged(); + +private: + QString visibilityKey; +}; + +class DiveGasPressureItem : public AbstractProfilePolygonItem { + Q_OBJECT + +public: + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + +private: + void plotPressureValue(int mbar, int sec, QFlags<Qt::AlignmentFlag> align, double offset); + void plotGasValue(int mbar, int sec, struct gasmix gasmix, QFlags<Qt::AlignmentFlag> align, double offset); + QVector<QPolygonF> polygons; +}; + +class DiveCalculatedCeiling : public AbstractProfilePolygonItem { + Q_OBJECT + +public: + DiveCalculatedCeiling(); + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + virtual void settingsChanged(); + +public +slots: + void recalc(); + +private: + bool is3mIncrement; +}; + +class DiveReportedCeiling : public AbstractProfilePolygonItem { + Q_OBJECT + +public: + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + virtual void settingsChanged(); +}; + +class DiveCalculatedTissue : public DiveCalculatedCeiling { + Q_OBJECT +public: + DiveCalculatedTissue(); + virtual void settingsChanged(); +}; + +class PartialPressureGasItem : public AbstractProfilePolygonItem { + Q_OBJECT +public: + PartialPressureGasItem(); + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0); + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + virtual void settingsChanged(); + void setThreshouldSettingsKey(double *prefPointer); + void setVisibilitySettingsKey(const QString &setVisibilitySettingsKey); + void setColors(const QColor &normalColor, const QColor &alertColor); + +private: + QVector<QPolygonF> alertPolygons; + double *thresholdPtr; + QString visibilityKey; + QColor normalColor; + QColor alertColor; +}; +#endif // DIVEPROFILEITEM_H diff --git a/desktop-widgets/profile/diverectitem.cpp b/desktop-widgets/profile/diverectitem.cpp new file mode 100644 index 000000000..8cb60c3f5 --- /dev/null +++ b/desktop-widgets/profile/diverectitem.cpp @@ -0,0 +1,5 @@ +#include "diverectitem.h" + +DiveRectItem::DiveRectItem(QObject *parent, QGraphicsItem *parentItem) : QObject(parent), QGraphicsRectItem(parentItem) +{ +} diff --git a/desktop-widgets/profile/diverectitem.h b/desktop-widgets/profile/diverectitem.h new file mode 100644 index 000000000..e616cf591 --- /dev/null +++ b/desktop-widgets/profile/diverectitem.h @@ -0,0 +1,17 @@ +#ifndef DIVERECTITEM_H +#define DIVERECTITEM_H + +#include <QObject> +#include <QGraphicsRectItem> + +class DiveRectItem : public QObject, public QGraphicsRectItem { + Q_OBJECT + Q_PROPERTY(QRectF rect WRITE setRect READ rect) + Q_PROPERTY(QPointF pos WRITE setPos READ pos) + Q_PROPERTY(qreal x WRITE setX READ x) + Q_PROPERTY(qreal y WRITE setY READ y) +public: + DiveRectItem(QObject *parent = 0, QGraphicsItem *parentItem = 0); +}; + +#endif // DIVERECTITEM_H diff --git a/desktop-widgets/profile/divetextitem.cpp b/desktop-widgets/profile/divetextitem.cpp new file mode 100644 index 000000000..3bf00d68f --- /dev/null +++ b/desktop-widgets/profile/divetextitem.cpp @@ -0,0 +1,113 @@ +#include "divetextitem.h" +#include "mainwindow.h" +#include "profilewidget2.h" +#include "subsurface-core/color.h" + +#include <QBrush> + +DiveTextItem::DiveTextItem(QGraphicsItem *parent) : QGraphicsItemGroup(parent), + internalAlignFlags(Qt::AlignHCenter | Qt::AlignVCenter), + textBackgroundItem(new QGraphicsPathItem(this)), + textItem(new QGraphicsPathItem(this)), + printScale(1.0), + scale(1.0), + connected(false) +{ + setFlag(ItemIgnoresTransformations); + textBackgroundItem->setBrush(QBrush(getColor(TEXT_BACKGROUND))); + textBackgroundItem->setPen(Qt::NoPen); + textItem->setPen(Qt::NoPen); +} + +void DiveTextItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + updateText(); + QGraphicsItemGroup::paint(painter, option, widget); +} + +void DiveTextItem::fontPrintScaleUpdate(double scale) +{ + printScale = scale; +} + +void DiveTextItem::setAlignment(int alignFlags) +{ + if (alignFlags != internalAlignFlags) { + internalAlignFlags = alignFlags; + } +} + +void DiveTextItem::setBrush(const QBrush &b) +{ + textItem->setBrush(b); +} + +void DiveTextItem::setScale(double newscale) +{ + if (scale != newscale) { + scale = newscale; + } +} + +void DiveTextItem::setText(const QString &t) +{ + if (internalText != t) { + if (!connected) { + if (scene()) { + // by now we should be on a scene. grab the profile widget from it and setup our printScale + // and connect to the signal that makes sure we keep track if that changes + ProfileWidget2 *profile = qobject_cast<ProfileWidget2 *>(scene()->views().first()); + connect(profile, SIGNAL(fontPrintScaleChanged(double)), this, SLOT(fontPrintScaleUpdate(double)), Qt::UniqueConnection); + fontPrintScaleUpdate(profile->getFontPrintScale()); + connected = true; + } else { + qDebug() << "called before scene was set up" << t; + } + } + internalText = t; + updateText(); + } +} + +const QString &DiveTextItem::text() +{ + return internalText; +} + +void DiveTextItem::updateText() +{ + double size; + if (internalText.isEmpty()) { + return; + } + + QFont fnt(qApp->font()); + if ((size = fnt.pixelSize()) > 0) { + // set in pixels - so the scale factor may not make a difference if it's too close to 1 + size *= scale * printScale; + fnt.setPixelSize(size); + } else { + size = fnt.pointSizeF(); + size *= scale * printScale; + fnt.setPointSizeF(size); + } + QFontMetrics fm(fnt); + + QPainterPath textPath; + qreal xPos = 0, yPos = 0; + + QRectF rect = fm.boundingRect(internalText); + yPos = (internalAlignFlags & Qt::AlignTop) ? 0 : + (internalAlignFlags & Qt::AlignBottom) ? +rect.height() : + /*(internalAlignFlags & Qt::AlignVCenter ? */ +rect.height() / 4; + + xPos = (internalAlignFlags & Qt::AlignLeft) ? -rect.width() : + (internalAlignFlags & Qt::AlignHCenter) ? -rect.width() / 2 : + /* (internalAlignFlags & Qt::AlignRight) */ 0; + + textPath.addText(xPos, yPos, fnt, internalText); + QPainterPathStroker stroker; + stroker.setWidth(3); + textBackgroundItem->setPath(stroker.createStroke(textPath)); + textItem->setPath(textPath); +} diff --git a/desktop-widgets/profile/divetextitem.h b/desktop-widgets/profile/divetextitem.h new file mode 100644 index 000000000..be0adf292 --- /dev/null +++ b/desktop-widgets/profile/divetextitem.h @@ -0,0 +1,38 @@ +#ifndef DIVETEXTITEM_H +#define DIVETEXTITEM_H + +#include <QObject> +#include <QGraphicsItemGroup> + +class QBrush; + +/* A Line Item that has animated-properties. */ +class DiveTextItem : public QObject, public QGraphicsItemGroup { + Q_OBJECT + Q_PROPERTY(QPointF pos READ pos WRITE setPos) + Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity) +public: + DiveTextItem(QGraphicsItem *parent = 0); + void setText(const QString &text); + void setAlignment(int alignFlags); + void setScale(double newscale); + void setBrush(const QBrush &brush); + const QString &text(); + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + +private +slots: + void fontPrintScaleUpdate(double scale); + +private: + void updateText(); + int internalAlignFlags; + QGraphicsPathItem *textBackgroundItem; + QGraphicsPathItem *textItem; + QString internalText; + double printScale; + double scale; + bool connected; +}; + +#endif // DIVETEXTITEM_H diff --git a/desktop-widgets/profile/divetooltipitem.cpp b/desktop-widgets/profile/divetooltipitem.cpp new file mode 100644 index 000000000..d4818422b --- /dev/null +++ b/desktop-widgets/profile/divetooltipitem.cpp @@ -0,0 +1,285 @@ +#include "divetooltipitem.h" +#include "divecartesianaxis.h" +#include "dive.h" +#include "profile.h" +#include "membuffer.h" +#include "metrics.h" +#include <QPropertyAnimation> +#include <QSettings> +#include <QGraphicsView> +#include <QStyleOptionGraphicsItem> + +#define PORT_IN_PROGRESS 1 +#ifdef PORT_IN_PROGRESS +#include "display.h" +#endif + +void ToolTipItem::addToolTip(const QString &toolTip, const QIcon &icon, const QPixmap& pixmap) +{ + const IconMetrics& iconMetrics = defaultIconMetrics(); + + QGraphicsPixmapItem *iconItem = 0; + double yValue = title->boundingRect().height() + iconMetrics.spacing; + Q_FOREACH (ToolTip t, toolTips) { + yValue += t.second->boundingRect().height(); + } + if (entryToolTip.second) { + yValue += entryToolTip.second->boundingRect().height(); + } + iconItem = new QGraphicsPixmapItem(this); + if (!icon.isNull()) { + iconItem->setPixmap(icon.pixmap(iconMetrics.sz_small, iconMetrics.sz_small)); + } else if (!pixmap.isNull()) { + iconItem->setPixmap(pixmap); + } + iconItem->setPos(iconMetrics.spacing, yValue); + + QGraphicsSimpleTextItem *textItem = new QGraphicsSimpleTextItem(toolTip, this); + textItem->setPos(iconMetrics.spacing + iconMetrics.sz_small + iconMetrics.spacing, yValue); + textItem->setBrush(QBrush(Qt::white)); + textItem->setFlag(ItemIgnoresTransformations); + toolTips.push_back(qMakePair(iconItem, textItem)); +} + +void ToolTipItem::clear() +{ + Q_FOREACH (ToolTip t, toolTips) { + delete t.first; + delete t.second; + } + toolTips.clear(); +} + +void ToolTipItem::setRect(const QRectF &r) +{ + if( r == rect() ) { + return; + } + + QGraphicsRectItem::setRect(r); + updateTitlePosition(); +} + +void ToolTipItem::collapse() +{ + int dim = defaultIconMetrics().sz_small; + + if (prefs.animation_speed) { + QPropertyAnimation *animation = new QPropertyAnimation(this, "rect"); + animation->setDuration(100); + animation->setStartValue(nextRectangle); + animation->setEndValue(QRect(0, 0, dim, dim)); + animation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + setRect(nextRectangle); + } + clear(); + + status = COLLAPSED; +} + +void ToolTipItem::expand() +{ + if (!title) + return; + + const IconMetrics& iconMetrics = defaultIconMetrics(); + + double width = 0, height = title->boundingRect().height() + iconMetrics.spacing; + Q_FOREACH (const ToolTip& t, toolTips) { + QRectF sRect = t.second->boundingRect(); + if (sRect.width() > width) + width = sRect.width(); + height += sRect.height(); + } + + if (entryToolTip.first) { + QRectF sRect = entryToolTip.second->boundingRect(); + if (sRect.width() > width) + width = sRect.width(); + height += sRect.height(); + } + + /* Left padding, Icon Size, space, right padding */ + width += iconMetrics.spacing + iconMetrics.sz_small + iconMetrics.spacing + iconMetrics.spacing; + + if (width < title->boundingRect().width() + iconMetrics.spacing * 2) + width = title->boundingRect().width() + iconMetrics.spacing * 2; + + if (height < iconMetrics.sz_small) + height = iconMetrics.sz_small; + + nextRectangle.setWidth(width); + nextRectangle.setHeight(height); + + if (nextRectangle != rect()) { + if (prefs.animation_speed) { + QPropertyAnimation *animation = new QPropertyAnimation(this, "rect", this); + animation->setDuration(prefs.animation_speed); + animation->setStartValue(rect()); + animation->setEndValue(nextRectangle); + animation->start(QAbstractAnimation::DeleteWhenStopped); + } else { + setRect(nextRectangle); + } + } + + status = EXPANDED; +} + +ToolTipItem::ToolTipItem(QGraphicsItem *parent) : QGraphicsRectItem(parent), + title(new QGraphicsSimpleTextItem(tr("Information"), this)), + status(COLLAPSED), + timeAxis(0), + lastTime(-1) +{ + memset(&pInfo, 0, sizeof(pInfo)); + entryToolTip.first = NULL; + entryToolTip.second = NULL; + setFlags(ItemIgnoresTransformations | ItemIsMovable | ItemClipsChildrenToShape); + + QColor c = QColor(Qt::black); + c.setAlpha(155); + setBrush(c); + + setZValue(99); + + addToolTip(QString(), QIcon(), QPixmap(16,60)); + entryToolTip = toolTips.first(); + toolTips.clear(); + + title->setFlag(ItemIgnoresTransformations); + title->setPen(QPen(Qt::white, 1)); + title->setBrush(Qt::white); + + setPen(QPen(Qt::white, 2)); + refreshTime.start(); +} + +ToolTipItem::~ToolTipItem() +{ + clear(); +} + +void ToolTipItem::updateTitlePosition() +{ + const IconMetrics& iconMetrics = defaultIconMetrics(); + if (rect().width() < title->boundingRect().width() + iconMetrics.spacing * 4) { + QRectF newRect = rect(); + newRect.setWidth(title->boundingRect().width() + iconMetrics.spacing * 4); + newRect.setHeight((newRect.height() && isExpanded()) ? newRect.height() : iconMetrics.sz_small); + setRect(newRect); + } + + title->setPos(rect().width() / 2 - title->boundingRect().width() / 2 - 1, 0); +} + +bool ToolTipItem::isExpanded() const +{ + return status == EXPANDED; +} + +void ToolTipItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + persistPos(); + QGraphicsRectItem::mouseReleaseEvent(event); + Q_FOREACH (QGraphicsItem *item, oldSelection) { + item->setSelected(true); + } +} + +void ToolTipItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + Q_UNUSED(widget); + painter->save(); + painter->setClipRect(option->rect); + painter->setPen(pen()); + painter->setBrush(brush()); + painter->drawRoundedRect(rect(), 10, 10, Qt::AbsoluteSize); + painter->restore(); +} + +void ToolTipItem::persistPos() +{ + QSettings s; + s.beginGroup("ProfileMap"); + s.setValue("tooltip_position", pos()); + s.endGroup(); +} + +void ToolTipItem::readPos() +{ + QSettings s; + s.beginGroup("ProfileMap"); + QPointF value = s.value("tooltip_position").toPoint(); + if (!scene()->sceneRect().contains(value)) { + value = QPointF(0, 0); + } + setPos(value); +} + +void ToolTipItem::setPlotInfo(const plot_info &plot) +{ + pInfo = plot; +} + +void ToolTipItem::setTimeAxis(DiveCartesianAxis *axis) +{ + timeAxis = axis; +} + +void ToolTipItem::refresh(const QPointF &pos) +{ + struct plot_data *entry; + static QPixmap tissues(16,60); + static QPainter painter(&tissues); + static struct membuffer mb = { 0 }; + + if(refreshTime.elapsed() < 40) + return; + refreshTime.start(); + + int time = timeAxis->valueAt(pos); + if (time == lastTime) + return; + + lastTime = time; + clear(); + + mb.len = 0; + entry = get_plot_details_new(&pInfo, time, &mb); + if (entry) { + tissues.fill(); + painter.setPen(QColor(0, 0, 0, 0)); + painter.setBrush(QColor(LIMENADE1)); + painter.drawRect(0, 10 + (100 - AMB_PERCENTAGE) / 2, 16, AMB_PERCENTAGE / 2); + painter.setBrush(QColor(SPRINGWOOD1)); + painter.drawRect(0, 10, 16, (100 - AMB_PERCENTAGE) / 2); + painter.setBrush(QColor(Qt::red)); + painter.drawRect(0,0,16,10); + painter.setPen(QColor(0, 0, 0, 255)); + painter.drawLine(0, 60 - entry->gfline / 2, 16, 60 - entry->gfline / 2); + painter.drawLine(0, 60 - AMB_PERCENTAGE * (entry->pressures.n2 + entry->pressures.he) / entry->ambpressure / 2, + 16, 60 - AMB_PERCENTAGE * (entry->pressures.n2 + entry->pressures.he) / entry->ambpressure /2); + painter.setPen(QColor(0, 0, 0, 127)); + for (int i=0; i<16; i++) { + painter.drawLine(i, 60, i, 60 - entry->percentages[i] / 2); + } + entryToolTip.first->setPixmap(tissues); + entryToolTip.second->setText(QString::fromUtf8(mb.buffer, mb.len)); + } + + Q_FOREACH (QGraphicsItem *item, scene()->items(pos, Qt::IntersectsItemBoundingRect + ,Qt::DescendingOrder, scene()->views().first()->transform())) { + if (!item->toolTip().isEmpty()) + addToolTip(item->toolTip()); + } + expand(); +} + +void ToolTipItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + oldSelection = scene()->selectedItems(); + scene()->clearSelection(); + QGraphicsItem::mousePressEvent(event); +} diff --git a/desktop-widgets/profile/divetooltipitem.h b/desktop-widgets/profile/divetooltipitem.h new file mode 100644 index 000000000..4fa7ec2d7 --- /dev/null +++ b/desktop-widgets/profile/divetooltipitem.h @@ -0,0 +1,67 @@ +#ifndef DIVETOOLTIPITEM_H +#define DIVETOOLTIPITEM_H + +#include <QGraphicsRectItem> +#include <QVector> +#include <QPair> +#include <QRectF> +#include <QIcon> +#include <QTime> +#include "display.h" + +class DiveCartesianAxis; +class QGraphicsLineItem; +class QGraphicsSimpleTextItem; +class QGraphicsPixmapItem; +struct graphics_context; + +/* To use a tooltip, simply ->setToolTip on the QGraphicsItem that you want + * or, if it's a "global" tooltip, set it on the mouseMoveEvent of the ProfileGraphicsView. + */ +class ToolTipItem : public QObject, public QGraphicsRectItem { + Q_OBJECT + void updateTitlePosition(); + Q_PROPERTY(QRectF rect READ rect WRITE setRect) + +public: + enum Status { + COLLAPSED, + EXPANDED + }; + + explicit ToolTipItem(QGraphicsItem *parent = 0); + virtual ~ToolTipItem(); + + void collapse(); + void expand(); + void clear(); + void addToolTip(const QString &toolTip, const QIcon &icon = QIcon(), const QPixmap &pixmap = QPixmap()); + void refresh(const QPointF &pos); + bool isExpanded() const; + void persistPos(); + void readPos(); + void mousePressEvent(QGraphicsSceneMouseEvent *event); + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event); + void setTimeAxis(DiveCartesianAxis *axis); + void setPlotInfo(const plot_info &plot); + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); +public +slots: + void setRect(const QRectF &rect); + +private: + typedef QPair<QGraphicsPixmapItem *, QGraphicsSimpleTextItem *> ToolTip; + QVector<ToolTip> toolTips; + ToolTip entryToolTip; + QGraphicsSimpleTextItem *title; + Status status; + QRectF rectangle; + QRectF nextRectangle; + DiveCartesianAxis *timeAxis; + plot_info pInfo; + int lastTime; + QTime refreshTime; + QList<QGraphicsItem*> oldSelection; +}; + +#endif // DIVETOOLTIPITEM_H diff --git a/desktop-widgets/profile/profilewidget2.cpp b/desktop-widgets/profile/profilewidget2.cpp new file mode 100644 index 000000000..3ccd1bb6d --- /dev/null +++ b/desktop-widgets/profile/profilewidget2.cpp @@ -0,0 +1,1836 @@ +#include "profilewidget2.h" +#include "diveplotdatamodel.h" +#include "helpers.h" +#include "profile.h" +#include "diveeventitem.h" +#include "divetextitem.h" +#include "divetooltipitem.h" +#include "planner.h" +#include "device.h" +#include "ruleritem.h" +#include "tankitem.h" +#include "pref.h" +#include "divepicturewidget.h" +#include "diveplannermodel.h" +#include "models.h" +#include "divepicturemodel.h" +#include "maintab.h" +#include "diveplanner.h" + +#include <libdivecomputer/parser.h> +#include <QScrollBar> +#include <QtCore/qmath.h> +#include <QMessageBox> +#include <QInputDialog> +#include <QDebug> +#include <QWheelEvent> + +#ifndef QT_NO_DEBUG +#include <QTableView> +#endif +#include "mainwindow.h" +#include <preferences.h> + +/* This is the global 'Item position' variable. + * it should tell you where to position things up + * on the canvas. + * + * please, please, please, use this instead of + * hard coding the item on the scene with a random + * value. + */ +static struct _ItemPos { + struct _Pos { + QPointF on; + QPointF off; + }; + struct _Axis { + _Pos pos; + QLineF shrinked; + QLineF expanded; + QLineF intermediate; + }; + _Pos background; + _Pos dcLabel; + _Pos tankBar; + _Axis depth; + _Axis partialPressure; + _Axis partialPressureTissue; + _Axis partialPressureWithTankBar; + _Axis percentage; + _Axis percentageWithTankBar; + _Axis time; + _Axis cylinder; + _Axis temperature; + _Axis temperatureAll; + _Axis heartBeat; + _Axis heartBeatWithTankBar; +} itemPos; + +ProfileWidget2::ProfileWidget2(QWidget *parent) : QGraphicsView(parent), + currentState(INVALID), + dataModel(new DivePlotDataModel(this)), + zoomLevel(0), + zoomFactor(1.15), + background(new DivePixmapItem()), + backgroundFile(":poster"), + toolTipItem(new ToolTipItem()), + isPlotZoomed(prefs.zoomed_plot),// no! bad use of prefs. 'PreferencesDialog::loadSettings' NOT CALLED yet. + profileYAxis(new DepthAxis()), + gasYAxis(new PartialGasPressureAxis()), + temperatureAxis(new TemperatureAxis()), + timeAxis(new TimeAxis()), + diveProfileItem(new DiveProfileItem()), + temperatureItem(new DiveTemperatureItem()), + meanDepthItem(new DiveMeanDepthItem()), + cylinderPressureAxis(new DiveCartesianAxis()), + gasPressureItem(new DiveGasPressureItem()), + diveComputerText(new DiveTextItem()), + diveCeiling(new DiveCalculatedCeiling()), + gradientFactor(new DiveTextItem()), + reportedCeiling(new DiveReportedCeiling()), + pn2GasItem(new PartialPressureGasItem()), + pheGasItem(new PartialPressureGasItem()), + po2GasItem(new PartialPressureGasItem()), + o2SetpointGasItem(new PartialPressureGasItem()), + ccrsensor1GasItem(new PartialPressureGasItem()), + ccrsensor2GasItem(new PartialPressureGasItem()), + ccrsensor3GasItem(new PartialPressureGasItem()), + heartBeatAxis(new DiveCartesianAxis()), + heartBeatItem(new DiveHeartrateItem()), + percentageAxis(new DiveCartesianAxis()), + ambPressureItem(new DiveAmbPressureItem()), + gflineItem(new DiveGFLineItem()), + mouseFollowerVertical(new DiveLineItem()), + mouseFollowerHorizontal(new DiveLineItem()), + rulerItem(new RulerItem2()), + tankItem(new TankItem()), + isGrayscale(false), + printMode(false), + shouldCalculateMaxTime(true), + shouldCalculateMaxDepth(true), + fontPrintScale(1.0) +{ + // would like to be able to ASSERT here that PreferencesDialog::loadSettings has been called. + isPlotZoomed = prefs.zoomed_plot; // now it seems that 'prefs' has loaded our preferences + + memset(&plotInfo, 0, sizeof(plotInfo)); + + setupSceneAndFlags(); + setupItemSizes(); + setupItemOnScene(); + addItemsToScene(); + scene()->installEventFilter(this); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); + QAction *action = NULL; +#define ADD_ACTION(SHORTCUT, Slot) \ + action = new QAction(this); \ + action->setShortcut(SHORTCUT); \ + action->setShortcutContext(Qt::WindowShortcut); \ + addAction(action); \ + connect(action, SIGNAL(triggered(bool)), this, SLOT(Slot)); \ + actionsForKeys[SHORTCUT] = action; + + ADD_ACTION(Qt::Key_Escape, keyEscAction()); + ADD_ACTION(Qt::Key_Delete, keyDeleteAction()); + ADD_ACTION(Qt::Key_Up, keyUpAction()); + ADD_ACTION(Qt::Key_Down, keyDownAction()); + ADD_ACTION(Qt::Key_Left, keyLeftAction()); + ADD_ACTION(Qt::Key_Right, keyRightAction()); +#undef ADD_ACTION + +#if !defined(QT_NO_DEBUG) && defined(SHOW_PLOT_INFO_TABLE) + QTableView *diveDepthTableView = new QTableView(); + diveDepthTableView->setModel(dataModel); + diveDepthTableView->show(); +#endif +} + + +ProfileWidget2::~ProfileWidget2() +{ + delete background; + delete toolTipItem; + delete profileYAxis; + delete gasYAxis; + delete temperatureAxis; + delete timeAxis; + delete diveProfileItem; + delete temperatureItem; + delete meanDepthItem; + delete cylinderPressureAxis; + delete gasPressureItem; + delete diveComputerText; + delete diveCeiling; + delete reportedCeiling; + delete pn2GasItem; + delete pheGasItem; + delete po2GasItem; + delete o2SetpointGasItem; + delete ccrsensor1GasItem; + delete ccrsensor2GasItem; + delete ccrsensor3GasItem; + delete heartBeatAxis; + delete heartBeatItem; + delete percentageAxis; + delete ambPressureItem; + delete gflineItem; + delete mouseFollowerVertical; + delete mouseFollowerHorizontal; + delete rulerItem; + delete tankItem; +} + +#define SUBSURFACE_OBJ_DATA 1 +#define SUBSURFACE_OBJ_DC_TEXT 0x42 + +void ProfileWidget2::addItemsToScene() +{ + scene()->addItem(background); + scene()->addItem(toolTipItem); + scene()->addItem(profileYAxis); + scene()->addItem(gasYAxis); + scene()->addItem(temperatureAxis); + scene()->addItem(timeAxis); + scene()->addItem(diveProfileItem); + scene()->addItem(cylinderPressureAxis); + scene()->addItem(temperatureItem); + scene()->addItem(meanDepthItem); + scene()->addItem(gasPressureItem); + // I cannot seem to figure out if an object that I find with itemAt() on the scene + // is the object I am looking for - my guess is there's a simple way in Qt to do that + // but nothing I tried worked. + // so instead this adds a special magic key/value pair to the object to mark it + diveComputerText->setData(SUBSURFACE_OBJ_DATA, SUBSURFACE_OBJ_DC_TEXT); + scene()->addItem(diveComputerText); + scene()->addItem(diveCeiling); + scene()->addItem(gradientFactor); + scene()->addItem(reportedCeiling); + scene()->addItem(pn2GasItem); + scene()->addItem(pheGasItem); + scene()->addItem(po2GasItem); + scene()->addItem(o2SetpointGasItem); + scene()->addItem(ccrsensor1GasItem); + scene()->addItem(ccrsensor2GasItem); + scene()->addItem(ccrsensor3GasItem); + scene()->addItem(percentageAxis); + scene()->addItem(heartBeatAxis); + scene()->addItem(heartBeatItem); + scene()->addItem(rulerItem); + scene()->addItem(rulerItem->sourceNode()); + scene()->addItem(rulerItem->destNode()); + scene()->addItem(tankItem); + scene()->addItem(mouseFollowerHorizontal); + scene()->addItem(mouseFollowerVertical); + QPen pen(QColor(Qt::red).lighter()); + pen.setWidth(0); + mouseFollowerHorizontal->setPen(pen); + mouseFollowerVertical->setPen(pen); + Q_FOREACH (DiveCalculatedTissue *tissue, allTissues) { + scene()->addItem(tissue); + } + Q_FOREACH (DivePercentageItem *percentage, allPercentages) { + scene()->addItem(percentage); + } + scene()->addItem(ambPressureItem); + scene()->addItem(gflineItem); +} + +void ProfileWidget2::setupItemOnScene() +{ + background->setZValue(9999); + toolTipItem->setZValue(9998); + toolTipItem->setTimeAxis(timeAxis); + rulerItem->setZValue(9997); + tankItem->setZValue(100); + + profileYAxis->setOrientation(DiveCartesianAxis::TopToBottom); + profileYAxis->setMinimum(0); + profileYAxis->setTickInterval(M_OR_FT(10, 30)); + profileYAxis->setTickSize(0.5); + profileYAxis->setLineSize(96); + + timeAxis->setLineSize(92); + timeAxis->setTickSize(-0.5); + + gasYAxis->setOrientation(DiveCartesianAxis::BottomToTop); + gasYAxis->setTickInterval(1); + gasYAxis->setTickSize(1); + gasYAxis->setMinimum(0); + gasYAxis->setModel(dataModel); + gasYAxis->setFontLabelScale(0.7); + gasYAxis->setLineSize(96); + + heartBeatAxis->setOrientation(DiveCartesianAxis::BottomToTop); + heartBeatAxis->setTickSize(0.2); + heartBeatAxis->setTickInterval(10); + heartBeatAxis->setFontLabelScale(0.7); + heartBeatAxis->setLineSize(96); + + percentageAxis->setOrientation(DiveCartesianAxis::BottomToTop); + percentageAxis->setTickSize(0.2); + percentageAxis->setTickInterval(10); + percentageAxis->setFontLabelScale(0.7); + percentageAxis->setLineSize(96); + + temperatureAxis->setOrientation(DiveCartesianAxis::BottomToTop); + temperatureAxis->setTickSize(2); + temperatureAxis->setTickInterval(300); + + cylinderPressureAxis->setOrientation(DiveCartesianAxis::BottomToTop); + cylinderPressureAxis->setTickSize(2); + cylinderPressureAxis->setTickInterval(30000); + + + diveComputerText->setAlignment(Qt::AlignRight | Qt::AlignTop); + diveComputerText->setBrush(getColor(TIME_TEXT, isGrayscale)); + + rulerItem->setAxis(timeAxis, profileYAxis); + tankItem->setHorizontalAxis(timeAxis); + + // show the gradient factor at the top in the center + gradientFactor->setY(0); + gradientFactor->setX(50); + gradientFactor->setBrush(getColor(PRESSURE_TEXT)); + gradientFactor->setAlignment(Qt::AlignHCenter | Qt::AlignBottom); + + setupItem(reportedCeiling, timeAxis, profileYAxis, dataModel, DivePlotDataModel::CEILING, DivePlotDataModel::TIME, 1); + setupItem(diveCeiling, timeAxis, profileYAxis, dataModel, DivePlotDataModel::CEILING, DivePlotDataModel::TIME, 1); + for (int i = 0; i < 16; i++) { + DiveCalculatedTissue *tissueItem = new DiveCalculatedTissue(); + setupItem(tissueItem, timeAxis, profileYAxis, dataModel, DivePlotDataModel::TISSUE_1 + i, DivePlotDataModel::TIME, 1 + i); + allTissues.append(tissueItem); + DivePercentageItem *percentageItem = new DivePercentageItem(i); + setupItem(percentageItem, timeAxis, percentageAxis, dataModel, DivePlotDataModel::PERCENTAGE_1 + i, DivePlotDataModel::TIME, 1 + i); + allPercentages.append(percentageItem); + } + setupItem(gasPressureItem, timeAxis, cylinderPressureAxis, dataModel, DivePlotDataModel::TEMPERATURE, DivePlotDataModel::TIME, 1); + setupItem(temperatureItem, timeAxis, temperatureAxis, dataModel, DivePlotDataModel::TEMPERATURE, DivePlotDataModel::TIME, 1); + setupItem(heartBeatItem, timeAxis, heartBeatAxis, dataModel, DivePlotDataModel::HEARTBEAT, DivePlotDataModel::TIME, 1); + setupItem(ambPressureItem, timeAxis, percentageAxis, dataModel, DivePlotDataModel::AMBPRESSURE, DivePlotDataModel::TIME, 1); + setupItem(gflineItem, timeAxis, percentageAxis, dataModel, DivePlotDataModel::GFLINE, DivePlotDataModel::TIME, 1); + setupItem(diveProfileItem, timeAxis, profileYAxis, dataModel, DivePlotDataModel::DEPTH, DivePlotDataModel::TIME, 0); + setupItem(meanDepthItem, timeAxis, profileYAxis, dataModel, DivePlotDataModel::INSTANT_MEANDEPTH, DivePlotDataModel::TIME, 1); + + +#define CREATE_PP_GAS(ITEM, VERTICAL_COLUMN, COLOR, COLOR_ALERT, THRESHOULD_SETTINGS, VISIBILITY_SETTINGS) \ + setupItem(ITEM, timeAxis, gasYAxis, dataModel, DivePlotDataModel::VERTICAL_COLUMN, DivePlotDataModel::TIME, 0); \ + ITEM->setThreshouldSettingsKey(THRESHOULD_SETTINGS); \ + ITEM->setVisibilitySettingsKey(VISIBILITY_SETTINGS); \ + ITEM->setColors(getColor(COLOR, isGrayscale), getColor(COLOR_ALERT, isGrayscale)); \ + ITEM->settingsChanged(); \ + ITEM->setZValue(99); + + CREATE_PP_GAS(pn2GasItem, PN2, PN2, PN2_ALERT, &prefs.pp_graphs.pn2_threshold, "pn2graph"); + CREATE_PP_GAS(pheGasItem, PHE, PHE, PHE_ALERT, &prefs.pp_graphs.phe_threshold, "phegraph"); + CREATE_PP_GAS(po2GasItem, PO2, PO2, PO2_ALERT, &prefs.pp_graphs.po2_threshold, "po2graph"); + CREATE_PP_GAS(o2SetpointGasItem, O2SETPOINT, PO2_ALERT, PO2_ALERT, &prefs.pp_graphs.po2_threshold, "po2graph"); + CREATE_PP_GAS(ccrsensor1GasItem, CCRSENSOR1, CCRSENSOR1, PO2_ALERT, &prefs.pp_graphs.po2_threshold, "ccrsensorgraph"); + CREATE_PP_GAS(ccrsensor2GasItem, CCRSENSOR2, CCRSENSOR2, PO2_ALERT, &prefs.pp_graphs.po2_threshold, "ccrsensorgraph"); + CREATE_PP_GAS(ccrsensor3GasItem, CCRSENSOR3, CCRSENSOR3, PO2_ALERT, &prefs.pp_graphs.po2_threshold, "ccrsensorgraph"); +#undef CREATE_PP_GAS + + temperatureAxis->setTextVisible(false); + temperatureAxis->setLinesVisible(false); + cylinderPressureAxis->setTextVisible(false); + cylinderPressureAxis->setLinesVisible(false); + timeAxis->setLinesVisible(true); + profileYAxis->setLinesVisible(true); + gasYAxis->setZValue(timeAxis->zValue() + 1); + heartBeatAxis->setTextVisible(true); + heartBeatAxis->setLinesVisible(true); + percentageAxis->setTextVisible(true); + percentageAxis->setLinesVisible(true); + + replotEnabled = true; +} + +void ProfileWidget2::replot(struct dive *d) +{ + if (!replotEnabled) + return; + dataModel->clear(); + plotDive(d, true); +} + +void ProfileWidget2::setupItemSizes() +{ + // Scene is *always* (double) 100 / 100. + // Background Config + /* Much probably a better math is needed here. + * good thing is that we only need to change the + * Axis and everything else is auto-adjusted.* + */ + + itemPos.background.on.setX(0); + itemPos.background.on.setY(0); + itemPos.background.off.setX(0); + itemPos.background.off.setY(110); + + //Depth Axis Config + itemPos.depth.pos.on.setX(3); + itemPos.depth.pos.on.setY(3); + itemPos.depth.pos.off.setX(-2); + itemPos.depth.pos.off.setY(3); + itemPos.depth.expanded.setP1(QPointF(0, 0)); + itemPos.depth.expanded.setP2(QPointF(0, 85)); + itemPos.depth.shrinked.setP1(QPointF(0, 0)); + itemPos.depth.shrinked.setP2(QPointF(0, 55)); + itemPos.depth.intermediate.setP1(QPointF(0, 0)); + itemPos.depth.intermediate.setP2(QPointF(0, 65)); + + // Time Axis Config + itemPos.time.pos.on.setX(3); + itemPos.time.pos.on.setY(95); + itemPos.time.pos.off.setX(3); + itemPos.time.pos.off.setY(110); + itemPos.time.expanded.setP1(QPointF(0, 0)); + itemPos.time.expanded.setP2(QPointF(94, 0)); + + // Partial Gas Axis Config + itemPos.partialPressure.pos.on.setX(97); + itemPos.partialPressure.pos.on.setY(75); + itemPos.partialPressure.pos.off.setX(110); + itemPos.partialPressure.pos.off.setY(63); + itemPos.partialPressure.expanded.setP1(QPointF(0, 0)); + itemPos.partialPressure.expanded.setP2(QPointF(0, 19)); + itemPos.partialPressureWithTankBar = itemPos.partialPressure; + itemPos.partialPressureWithTankBar.expanded.setP2(QPointF(0, 17)); + itemPos.partialPressureTissue = itemPos.partialPressure; + itemPos.partialPressureTissue.pos.on.setX(97); + itemPos.partialPressureTissue.pos.on.setY(65); + itemPos.partialPressureTissue.expanded.setP2(QPointF(0, 16)); + + // cylinder axis config + itemPos.cylinder.pos.on.setX(3); + itemPos.cylinder.pos.on.setY(20); + itemPos.cylinder.pos.off.setX(-10); + itemPos.cylinder.pos.off.setY(20); + itemPos.cylinder.expanded.setP1(QPointF(0, 15)); + itemPos.cylinder.expanded.setP2(QPointF(0, 50)); + itemPos.cylinder.shrinked.setP1(QPointF(0, 0)); + itemPos.cylinder.shrinked.setP2(QPointF(0, 20)); + itemPos.cylinder.intermediate.setP1(QPointF(0, 0)); + itemPos.cylinder.intermediate.setP2(QPointF(0, 20)); + + // Temperature axis config + itemPos.temperature.pos.on.setX(3); + itemPos.temperature.pos.on.setY(60); + itemPos.temperatureAll.pos.on.setY(51); + itemPos.temperature.pos.off.setX(-10); + itemPos.temperature.pos.off.setY(40); + itemPos.temperature.expanded.setP1(QPointF(0, 20)); + itemPos.temperature.expanded.setP2(QPointF(0, 33)); + itemPos.temperature.shrinked.setP1(QPointF(0, 2)); + itemPos.temperature.shrinked.setP2(QPointF(0, 12)); + itemPos.temperature.intermediate.setP1(QPointF(0, 2)); + itemPos.temperature.intermediate.setP2(QPointF(0, 12)); + + // Heartbeat axis config + itemPos.heartBeat.pos.on.setX(3); + itemPos.heartBeat.pos.on.setY(82); + itemPos.heartBeat.expanded.setP1(QPointF(0, 0)); + itemPos.heartBeat.expanded.setP2(QPointF(0, 10)); + itemPos.heartBeatWithTankBar = itemPos.heartBeat; + itemPos.heartBeatWithTankBar.expanded.setP2(QPointF(0, 7)); + + // Percentage axis config + itemPos.percentage.pos.on.setX(3); + itemPos.percentage.pos.on.setY(80); + itemPos.percentage.expanded.setP1(QPointF(0, 0)); + itemPos.percentage.expanded.setP2(QPointF(0, 15)); + itemPos.percentageWithTankBar = itemPos.percentage; + itemPos.percentageWithTankBar.expanded.setP2(QPointF(0, 12)); + + itemPos.dcLabel.on.setX(3); + itemPos.dcLabel.on.setY(100); + itemPos.dcLabel.off.setX(-10); + itemPos.dcLabel.off.setY(100); + + itemPos.tankBar.on.setX(0); + itemPos.tankBar.on.setY(91.5); +} + +void ProfileWidget2::setupItem(AbstractProfilePolygonItem *item, DiveCartesianAxis *hAxis, + DiveCartesianAxis *vAxis, DivePlotDataModel *model, + int vData, int hData, int zValue) +{ + item->setHorizontalAxis(hAxis); + item->setVerticalAxis(vAxis); + item->setModel(model); + item->setVerticalDataColumn(vData); + item->setHorizontalDataColumn(hData); + item->setZValue(zValue); +} + +void ProfileWidget2::setupSceneAndFlags() +{ + setScene(new QGraphicsScene(this)); + scene()->setSceneRect(0, 0, 100, 100); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scene()->setItemIndexMethod(QGraphicsScene::NoIndex); + setOptimizationFlags(QGraphicsView::DontSavePainterState); + setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate); + setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform); + setMouseTracking(true); + background->setFlag(QGraphicsItem::ItemIgnoresTransformations); +} + +void ProfileWidget2::resetZoom() +{ + if (!zoomLevel) + return; + const qreal defScale = 1.0 / qPow(zoomFactor, (qreal)zoomLevel); + scale(defScale, defScale); + zoomLevel = 0; +} + +// Currently just one dive, but the plan is to enable All of the selected dives. +void ProfileWidget2::plotDive(struct dive *d, bool force) +{ + static bool firstCall = true; + QTime measureDuration; // let's measure how long this takes us (maybe we'll turn of TTL calculation later + measureDuration.start(); + + if (currentState != ADD && currentState != PLAN) { + if (!d) { + if (selected_dive == -1) + return; + d = current_dive; // display the current dive + } + + // No need to do this again if we are already showing the same dive + // computer of the same dive, so we check the unique id of the dive + // and the selected dive computer number against the ones we are + // showing (can't compare the dive pointers as those might change). + if (d->id == displayed_dive.id && dc_number == dataModel->dcShown() && !force) + return; + + // this copies the dive and makes copies of all the relevant additional data + copy_dive(d, &displayed_dive); + gradientFactor->setText(QString("GF %1/%2").arg(prefs.gflow).arg(prefs.gfhigh)); + } else { + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + plannerModel->createTemporaryPlan(); + struct diveplan &diveplan = plannerModel->getDiveplan(); + if (!diveplan.dp) { + plannerModel->deleteTemporaryPlan(); + return; + } + gradientFactor->setText(QString("GF %1/%2").arg(diveplan.gflow).arg(diveplan.gfhigh)); + } + + // special handling for the first time we display things + int animSpeedBackup = 0; + if (firstCall && MainWindow::instance()->filesFromCommandLine()) { + animSpeedBackup = prefs.animation_speed; + prefs.animation_speed = 0; + firstCall = false; + } + + // restore default zoom level + resetZoom(); + + // reset some item visibility on printMode changes + toolTipItem->setVisible(!printMode); + rulerItem->setVisible(prefs.rulergraph && !printMode && currentState != PLAN && currentState != ADD); + + if (currentState == EMPTY) + setProfileState(); + + // next get the dive computer structure - if there are no samples + // let's create a fake profile that's somewhat reasonable for the + // data that we have + struct divecomputer *currentdc = select_dc(&displayed_dive); + Q_ASSERT(currentdc); + if (!currentdc || !currentdc->samples) { + currentdc = fake_dc(currentdc); + } + + bool setpointflag = (currentdc->divemode == CCR) && prefs.pp_graphs.po2 && current_dive; + bool sensorflag = setpointflag && prefs.show_ccr_sensors; + o2SetpointGasItem->setVisible(setpointflag && prefs.show_ccr_setpoint); + ccrsensor1GasItem->setVisible(sensorflag); + ccrsensor2GasItem->setVisible(sensorflag && (currentdc->no_o2sensors > 1)); + ccrsensor3GasItem->setVisible(sensorflag && (currentdc->no_o2sensors > 2)); + + /* This struct holds all the data that's about to be plotted. + * I'm not sure this is the best approach ( but since we are + * interpolating some points of the Dive, maybe it is... ) + * The Calculation of the points should be done per graph, + * so I'll *not* calculate everything if something is not being + * shown. + */ + plotInfo = calculate_max_limits_new(&displayed_dive, currentdc); + create_plot_info_new(&displayed_dive, currentdc, &plotInfo, !shouldCalculateMaxDepth); + if (shouldCalculateMaxTime) + maxtime = get_maxtime(&plotInfo); + + /* Only update the max depth if it's bigger than the current ones + * when we are dragging the handler to plan / add dive. + * otherwhise, update normally. + */ + int newMaxDepth = get_maxdepth(&plotInfo); + if (!shouldCalculateMaxDepth) { + if (maxdepth < newMaxDepth) { + maxdepth = newMaxDepth; + } + } else { + maxdepth = newMaxDepth; + } + + dataModel->setDive(&displayed_dive, plotInfo); + toolTipItem->setPlotInfo(plotInfo); + + // It seems that I'll have a lot of boilerplate setting the model / axis for + // each item, I'll mostly like to fix this in the future, but I'll keep at this for now. + profileYAxis->setMaximum(maxdepth); + profileYAxis->updateTicks(); + + temperatureAxis->setMinimum(plotInfo.mintemp); + temperatureAxis->setMaximum(plotInfo.maxtemp - plotInfo.mintemp > 2000 ? plotInfo.maxtemp : plotInfo.mintemp + 2000); + + if (plotInfo.maxhr) { + heartBeatAxis->setMinimum(plotInfo.minhr); + heartBeatAxis->setMaximum(plotInfo.maxhr); + heartBeatAxis->updateTicks(HR_AXIS); // this shows the ticks + } + heartBeatAxis->setVisible(prefs.hrgraph && plotInfo.maxhr); + + percentageAxis->setMinimum(0); + percentageAxis->setMaximum(100); + percentageAxis->setVisible(false); + percentageAxis->updateTicks(HR_AXIS); + + timeAxis->setMaximum(maxtime); + int i, incr; + static int increments[8] = { 10, 20, 30, 60, 5 * 60, 10 * 60, 15 * 60, 30 * 60 }; + /* Time markers: at most every 10 seconds, but no more than 12 markers. + * We start out with 10 seconds and increment up to 30 minutes, + * depending on the dive time. + * This allows for 6h dives - enough (I hope) for even the craziest + * divers - but just in case, for those 8h depth-record-breaking dives, + * we double the interval if this still doesn't get us to 12 or fewer + * time markers */ + i = 0; + while (i < 7 && maxtime / increments[i] > 12) + i++; + incr = increments[i]; + while (maxtime / incr > 12) + incr *= 2; + timeAxis->setTickInterval(incr); + timeAxis->updateTicks(); + cylinderPressureAxis->setMinimum(plotInfo.minpressure); + cylinderPressureAxis->setMaximum(plotInfo.maxpressure); + + rulerItem->setPlotInfo(plotInfo); + tankItem->setData(dataModel, &plotInfo, &displayed_dive); + + dataModel->emitDataChanged(); + // The event items are a bit special since we don't know how many events are going to + // exist on a dive, so I cant create cache items for that. that's why they are here + // while all other items are up there on the constructor. + qDeleteAll(eventItems); + eventItems.clear(); + struct event *event = currentdc->events; + while (event) { + // if print mode is selected only draw headings, SP change, gas events or bookmark event + if (printMode) { + if (same_string(event->name, "") || + !(strcmp(event->name, "heading") == 0 || + (same_string(event->name, "SP change") && event->time.seconds == 0) || + event_is_gaschange(event) || + event->type == SAMPLE_EVENT_BOOKMARK)) { + event = event->next; + continue; + } + } + DiveEventItem *item = new DiveEventItem(); + item->setHorizontalAxis(timeAxis); + item->setVerticalAxis(profileYAxis); + item->setModel(dataModel); + item->setEvent(event); + item->setZValue(2); + scene()->addItem(item); + eventItems.push_back(item); + event = event->next; + } + // Only set visible the events that should be visible + Q_FOREACH (DiveEventItem *event, eventItems) { + event->setVisible(!event->shouldBeHidden()); + } + QString dcText = get_dc_nickname(currentdc->model, currentdc->deviceid); + int nr; + if ((nr = number_of_computers(&displayed_dive)) > 1) + dcText += tr(" (#%1 of %2)").arg(dc_number + 1).arg(nr); + if (dcText.isEmpty()) + dcText = tr("Unknown dive computer"); + diveComputerText->setText(dcText); + if (MainWindow::instance()->filesFromCommandLine() && animSpeedBackup != 0) { + prefs.animation_speed = animSpeedBackup; + } + + if (currentState == ADD || currentState == PLAN) { // TODO: figure a way to move this from here. + repositionDiveHandlers(); + DivePlannerPointsModel *model = DivePlannerPointsModel::instance(); + model->deleteTemporaryPlan(); + } + plotPictures(); + + // OK, how long did this take us? Anything above the second is way too long, + // so if we are calculation TTS / NDL then let's force that off. + if (measureDuration.elapsed() > 1000 && prefs.calcndltts) { + MainWindow::instance()->turnOffNdlTts(); + MainWindow::instance()->getNotificationWidget()->showNotification(tr("Show NDL / TTS was disabled because of excessive processing time"), KMessageWidget::Error); + } + MainWindow::instance()->getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + +} + +void ProfileWidget2::recalcCeiling() +{ + diveCeiling->recalc(); +} + +void ProfileWidget2::settingsChanged() +{ + // if we are showing calculated ceilings then we have to replot() + // because the GF could have changed; otherwise we try to avoid replot() + bool needReplot = prefs.calcceiling; + if ((prefs.percentagegraph||prefs.hrgraph) && PP_GRAPHS_ENABLED) { + profileYAxis->animateChangeLine(itemPos.depth.shrinked); + temperatureAxis->setPos(itemPos.temperatureAll.pos.on); + temperatureAxis->animateChangeLine(itemPos.temperature.shrinked); + cylinderPressureAxis->animateChangeLine(itemPos.cylinder.shrinked); + + if (prefs.tankbar) { + percentageAxis->setPos(itemPos.percentageWithTankBar.pos.on); + percentageAxis->animateChangeLine(itemPos.percentageWithTankBar.expanded); + heartBeatAxis->setPos(itemPos.heartBeatWithTankBar.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeatWithTankBar.expanded); + }else { + percentageAxis->setPos(itemPos.percentage.pos.on); + percentageAxis->animateChangeLine(itemPos.percentage.expanded); + heartBeatAxis->setPos(itemPos.heartBeat.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeat.expanded); + } + gasYAxis->setPos(itemPos.partialPressureTissue.pos.on); + gasYAxis->animateChangeLine(itemPos.partialPressureTissue.expanded); + + } else if (PP_GRAPHS_ENABLED || prefs.hrgraph || prefs.percentagegraph) { + profileYAxis->animateChangeLine(itemPos.depth.intermediate); + temperatureAxis->setPos(itemPos.temperature.pos.on); + temperatureAxis->animateChangeLine(itemPos.temperature.intermediate); + cylinderPressureAxis->animateChangeLine(itemPos.cylinder.intermediate); + if (prefs.tankbar) { + percentageAxis->setPos(itemPos.percentageWithTankBar.pos.on); + percentageAxis->animateChangeLine(itemPos.percentageWithTankBar.expanded); + gasYAxis->setPos(itemPos.partialPressureWithTankBar.pos.on); + gasYAxis->setLine(itemPos.partialPressureWithTankBar.expanded); + heartBeatAxis->setPos(itemPos.heartBeatWithTankBar.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeatWithTankBar.expanded); + } else { + gasYAxis->setPos(itemPos.partialPressure.pos.on); + gasYAxis->animateChangeLine(itemPos.partialPressure.expanded); + percentageAxis->setPos(itemPos.percentage.pos.on); + percentageAxis->setLine(itemPos.percentage.expanded); + heartBeatAxis->setPos(itemPos.heartBeat.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeat.expanded); + } + } else { + profileYAxis->animateChangeLine(itemPos.depth.expanded); + if (prefs.tankbar) { + temperatureAxis->setPos(itemPos.temperatureAll.pos.on); + } else { + temperatureAxis->setPos(itemPos.temperature.pos.on); + } + temperatureAxis->animateChangeLine(itemPos.temperature.expanded); + cylinderPressureAxis->animateChangeLine(itemPos.cylinder.expanded); + } + + tankItem->setVisible(prefs.tankbar); + if (prefs.zoomed_plot != isPlotZoomed) { + isPlotZoomed = prefs.zoomed_plot; + needReplot = true; + } + if (needReplot) + replot(); +} + +void ProfileWidget2::resizeEvent(QResizeEvent *event) +{ + QGraphicsView::resizeEvent(event); + fitInView(sceneRect(), Qt::IgnoreAspectRatio); + fixBackgroundPos(); +} + +void ProfileWidget2::mousePressEvent(QMouseEvent *event) +{ + if (zoomLevel) + return; + QGraphicsView::mousePressEvent(event); + if (currentState == PLAN) + shouldCalculateMaxTime = false; +} + +void ProfileWidget2::divePlannerHandlerClicked() +{ + if (zoomLevel) + return; + shouldCalculateMaxDepth = false; + replot(); +} + +void ProfileWidget2::divePlannerHandlerReleased() +{ + if (zoomLevel) + return; + shouldCalculateMaxDepth = true; + replot(); +} + +void ProfileWidget2::mouseReleaseEvent(QMouseEvent *event) +{ + if (zoomLevel) + return; + QGraphicsView::mouseReleaseEvent(event); + if (currentState == PLAN) { + shouldCalculateMaxTime = true; + replot(); + } +} + +void ProfileWidget2::fixBackgroundPos() +{ + static QPixmap toBeScaled(backgroundFile); + if (currentState != EMPTY) + return; + QPixmap p = toBeScaled.scaledToHeight(viewport()->height() - 40, Qt::SmoothTransformation); + int x = viewport()->width() / 2 - p.width() / 2; + int y = viewport()->height() / 2 - p.height() / 2; + background->setPixmap(p); + background->setX(mapToScene(x, 0).x()); + background->setY(mapToScene(y, 20).y()); +} + +void ProfileWidget2::wheelEvent(QWheelEvent *event) +{ + if (currentState == EMPTY) + return; + QPoint toolTipPos = mapFromScene(toolTipItem->pos()); + if (event->buttons() == Qt::LeftButton) + return; + if (event->delta() > 0 && zoomLevel < 20) { + scale(zoomFactor, zoomFactor); + zoomLevel++; + } else if (event->delta() < 0 && zoomLevel > 0) { + // Zooming out + scale(1.0 / zoomFactor, 1.0 / zoomFactor); + zoomLevel--; + } + scrollViewTo(event->pos()); + toolTipItem->setPos(mapToScene(toolTipPos)); +} + +void ProfileWidget2::mouseDoubleClickEvent(QMouseEvent *event) +{ + if (currentState == PLAN || currentState == ADD) { + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + QPointF mappedPos = mapToScene(event->pos()); + if (isPointOutOfBoundaries(mappedPos)) + return; + + int minutes = rint(timeAxis->valueAt(mappedPos) / 60); + int milimeters = rint(profileYAxis->valueAt(mappedPos) / M_OR_FT(1, 1)) * M_OR_FT(1, 1); + plannerModel->addStop(milimeters, minutes * 60, 0, 0, true); + } +} + +bool ProfileWidget2::isPointOutOfBoundaries(const QPointF &point) const +{ + double xpos = timeAxis->valueAt(point); + double ypos = profileYAxis->valueAt(point); + return (xpos > timeAxis->maximum() || + xpos < timeAxis->minimum() || + ypos > profileYAxis->maximum() || + ypos < profileYAxis->minimum()); +} + +void ProfileWidget2::scrollViewTo(const QPoint &pos) +{ + /* since we cannot use translate() directly on the scene we hack on + * the scroll bars (hidden) functionality */ + if (!zoomLevel || currentState == EMPTY) + return; + QScrollBar *vs = verticalScrollBar(); + QScrollBar *hs = horizontalScrollBar(); + const qreal yRat = (qreal)pos.y() / viewport()->height(); + const qreal xRat = (qreal)pos.x() / viewport()->width(); + vs->setValue(yRat * vs->maximum()); + hs->setValue(xRat * hs->maximum()); +} + +void ProfileWidget2::mouseMoveEvent(QMouseEvent *event) +{ + QPointF pos = mapToScene(event->pos()); + toolTipItem->refresh(pos); + if (zoomLevel == 0) { + QGraphicsView::mouseMoveEvent(event); + } else { + QPoint toolTipPos = mapFromScene(toolTipItem->pos()); + scrollViewTo(event->pos()); + toolTipItem->setPos(mapToScene(toolTipPos)); + } + + qreal vValue = profileYAxis->valueAt(pos); + qreal hValue = timeAxis->valueAt(pos); + if (profileYAxis->maximum() >= vValue && profileYAxis->minimum() <= vValue) { + mouseFollowerHorizontal->setPos(timeAxis->pos().x(), pos.y()); + } + if (timeAxis->maximum() >= hValue && timeAxis->minimum() <= hValue) { + mouseFollowerVertical->setPos(pos.x(), profileYAxis->line().y1()); + } +} + +bool ProfileWidget2::eventFilter(QObject *object, QEvent *event) +{ + QGraphicsScene *s = qobject_cast<QGraphicsScene *>(object); + if (s && event->type() == QEvent::GraphicsSceneHelp) { + event->ignore(); + return true; + } + return QGraphicsView::eventFilter(object, event); +} + +void ProfileWidget2::setEmptyState() +{ + // Then starting Empty State, move the background up. + if (currentState == EMPTY) + return; + + disconnectTemporaryConnections(); + setBackgroundBrush(getColor(::BACKGROUND, isGrayscale)); + dataModel->clear(); + currentState = EMPTY; + MainWindow::instance()->setEnabledToolbar(false); + + fixBackgroundPos(); + background->setVisible(true); + + profileYAxis->setVisible(false); + gasYAxis->setVisible(false); + timeAxis->setVisible(false); + temperatureAxis->setVisible(false); + cylinderPressureAxis->setVisible(false); + toolTipItem->setVisible(false); + diveComputerText->setVisible(false); + diveCeiling->setVisible(false); + gradientFactor->setVisible(false); + reportedCeiling->setVisible(false); + rulerItem->setVisible(false); + tankItem->setVisible(false); + pn2GasItem->setVisible(false); + po2GasItem->setVisible(false); + o2SetpointGasItem->setVisible(false); + ccrsensor1GasItem->setVisible(false); + ccrsensor2GasItem->setVisible(false); + ccrsensor3GasItem->setVisible(false); + pheGasItem->setVisible(false); + ambPressureItem->setVisible(false); + gflineItem->setVisible(false); + mouseFollowerHorizontal->setVisible(false); + mouseFollowerVertical->setVisible(false); + +#define HIDE_ALL(TYPE, CONTAINER) \ + Q_FOREACH (TYPE *item, CONTAINER) item->setVisible(false); + HIDE_ALL(DiveCalculatedTissue, allTissues); + HIDE_ALL(DivePercentageItem, allPercentages); + HIDE_ALL(DiveEventItem, eventItems); + HIDE_ALL(DiveHandler, handles); + HIDE_ALL(QGraphicsSimpleTextItem, gases); +#undef HIDE_ALL +} + +void ProfileWidget2::setProfileState() +{ + // Then starting Empty State, move the background up. + if (currentState == PROFILE) + return; + + disconnectTemporaryConnections(); + connect(DivePictureModel::instance(), SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(plotPictures())); + connect(DivePictureModel::instance(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(plotPictures())); + connect(DivePictureModel::instance(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(plotPictures())); + /* show the same stuff that the profile shows. */ + + //TODO: Move the DC handling to another method. + MainWindow::instance()->enableShortcuts(); + + currentState = PROFILE; + MainWindow::instance()->setEnabledToolbar(true); + toolTipItem->readPos(); + setBackgroundBrush(getColor(::BACKGROUND, isGrayscale)); + + background->setVisible(false); + toolTipItem->setVisible(true); + profileYAxis->setVisible(true); + gasYAxis->setVisible(true); + timeAxis->setVisible(true); + temperatureAxis->setVisible(true); + cylinderPressureAxis->setVisible(true); + + profileYAxis->setPos(itemPos.depth.pos.on); + if ((prefs.percentagegraph||prefs.hrgraph) && PP_GRAPHS_ENABLED) { + profileYAxis->animateChangeLine(itemPos.depth.shrinked); + temperatureAxis->setPos(itemPos.temperatureAll.pos.on); + temperatureAxis->animateChangeLine(itemPos.temperature.shrinked); + cylinderPressureAxis->animateChangeLine(itemPos.cylinder.shrinked); + + if (prefs.tankbar) { + percentageAxis->setPos(itemPos.percentageWithTankBar.pos.on); + percentageAxis->animateChangeLine(itemPos.percentageWithTankBar.expanded); + heartBeatAxis->setPos(itemPos.heartBeatWithTankBar.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeatWithTankBar.expanded); + }else { + percentageAxis->setPos(itemPos.percentage.pos.on); + percentageAxis->animateChangeLine(itemPos.percentage.expanded); + heartBeatAxis->setPos(itemPos.heartBeat.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeat.expanded); + } + gasYAxis->setPos(itemPos.partialPressureTissue.pos.on); + gasYAxis->animateChangeLine(itemPos.partialPressureTissue.expanded); + + } else if (PP_GRAPHS_ENABLED || prefs.hrgraph || prefs.percentagegraph) { + profileYAxis->animateChangeLine(itemPos.depth.intermediate); + temperatureAxis->setPos(itemPos.temperature.pos.on); + temperatureAxis->animateChangeLine(itemPos.temperature.intermediate); + cylinderPressureAxis->animateChangeLine(itemPos.cylinder.intermediate); + if (prefs.tankbar) { + percentageAxis->setPos(itemPos.percentageWithTankBar.pos.on); + percentageAxis->animateChangeLine(itemPos.percentageWithTankBar.expanded); + gasYAxis->setPos(itemPos.partialPressureWithTankBar.pos.on); + gasYAxis->setLine(itemPos.partialPressureWithTankBar.expanded); + heartBeatAxis->setPos(itemPos.heartBeatWithTankBar.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeatWithTankBar.expanded); + } else { + gasYAxis->setPos(itemPos.partialPressure.pos.on); + gasYAxis->animateChangeLine(itemPos.partialPressure.expanded); + percentageAxis->setPos(itemPos.percentage.pos.on); + percentageAxis->setLine(itemPos.percentage.expanded); + heartBeatAxis->setPos(itemPos.heartBeat.pos.on); + heartBeatAxis->animateChangeLine(itemPos.heartBeat.expanded); + } + } else { + profileYAxis->animateChangeLine(itemPos.depth.expanded); + if (prefs.tankbar) { + temperatureAxis->setPos(itemPos.temperatureAll.pos.on); + } else { + temperatureAxis->setPos(itemPos.temperature.pos.on); + } + temperatureAxis->animateChangeLine(itemPos.temperature.expanded); + cylinderPressureAxis->animateChangeLine(itemPos.cylinder.expanded); + } + pn2GasItem->setVisible(prefs.pp_graphs.pn2); + po2GasItem->setVisible(prefs.pp_graphs.po2); + pheGasItem->setVisible(prefs.pp_graphs.phe); + + bool setpointflag = current_dive && (current_dc->divemode == CCR) && prefs.pp_graphs.po2; + bool sensorflag = setpointflag && prefs.show_ccr_sensors; + o2SetpointGasItem->setVisible(setpointflag && prefs.show_ccr_setpoint); + ccrsensor1GasItem->setVisible(sensorflag); + ccrsensor2GasItem->setVisible(sensorflag && (current_dc->no_o2sensors > 1)); + ccrsensor3GasItem->setVisible(sensorflag && (current_dc->no_o2sensors > 2)); + + timeAxis->setPos(itemPos.time.pos.on); + timeAxis->setLine(itemPos.time.expanded); + + cylinderPressureAxis->setPos(itemPos.cylinder.pos.on); + heartBeatItem->setVisible(prefs.hrgraph); + meanDepthItem->setVisible(prefs.show_average_depth); + + diveComputerText->setVisible(true); + diveComputerText->setPos(itemPos.dcLabel.on); + + diveCeiling->setVisible(prefs.calcceiling); + gradientFactor->setVisible(prefs.calcceiling); + reportedCeiling->setVisible(prefs.dcceiling); + + if (prefs.calcalltissues) { + Q_FOREACH (DiveCalculatedTissue *tissue, allTissues) { + tissue->setVisible(true); + } + } + + if (prefs.percentagegraph) { + Q_FOREACH (DivePercentageItem *percentage, allPercentages) { + percentage->setVisible(true); + } + + ambPressureItem->setVisible(true); + gflineItem->setVisible(true); + } + + rulerItem->setVisible(prefs.rulergraph); + tankItem->setVisible(prefs.tankbar); + tankItem->setPos(itemPos.tankBar.on); + +#define HIDE_ALL(TYPE, CONTAINER) \ + Q_FOREACH (TYPE *item, CONTAINER) item->setVisible(false); + HIDE_ALL(DiveHandler, handles); + HIDE_ALL(QGraphicsSimpleTextItem, gases); +#undef HIDE_ALL + mouseFollowerHorizontal->setVisible(false); + mouseFollowerVertical->setVisible(false); +} + +void ProfileWidget2::clearHandlers() +{ + if (handles.count()) { + foreach (DiveHandler *handle, handles) { + scene()->removeItem(handle); + delete handle; + } + handles.clear(); + } +} + +void ProfileWidget2::setToolTipVisibile(bool visible) +{ + toolTipItem->setVisible(visible); +} + +void ProfileWidget2::setAddState() +{ + if (currentState == ADD) + return; + + clearHandlers(); + setProfileState(); + mouseFollowerHorizontal->setVisible(true); + mouseFollowerVertical->setVisible(true); + mouseFollowerHorizontal->setLine(timeAxis->line()); + mouseFollowerVertical->setLine(QLineF(0, profileYAxis->pos().y(), 0, timeAxis->pos().y())); + disconnectTemporaryConnections(); + //TODO: Move this method to another place, shouldn't be on mainwindow. + MainWindow::instance()->disableShortcuts(false); + actionsForKeys[Qt::Key_Left]->setShortcut(Qt::Key_Left); + actionsForKeys[Qt::Key_Right]->setShortcut(Qt::Key_Right); + actionsForKeys[Qt::Key_Up]->setShortcut(Qt::Key_Up); + actionsForKeys[Qt::Key_Down]->setShortcut(Qt::Key_Down); + actionsForKeys[Qt::Key_Escape]->setShortcut(Qt::Key_Escape); + actionsForKeys[Qt::Key_Delete]->setShortcut(Qt::Key_Delete); + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + connect(plannerModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(replot())); + connect(plannerModel, SIGNAL(cylinderModelEdited()), this, SLOT(replot())); + connect(plannerModel, SIGNAL(rowsInserted(const QModelIndex &, int, int)), + this, SLOT(pointInserted(const QModelIndex &, int, int))); + connect(plannerModel, SIGNAL(rowsRemoved(const QModelIndex &, int, int)), + this, SLOT(pointsRemoved(const QModelIndex &, int, int))); + /* show the same stuff that the profile shows. */ + currentState = ADD; /* enable the add state. */ + diveCeiling->setVisible(true); + gradientFactor->setVisible(true); + setBackgroundBrush(QColor("#A7DCFF")); +} + +void ProfileWidget2::setPlanState() +{ + if (currentState == PLAN) + return; + + setProfileState(); + mouseFollowerHorizontal->setVisible(true); + mouseFollowerVertical->setVisible(true); + mouseFollowerHorizontal->setLine(timeAxis->line()); + mouseFollowerVertical->setLine(QLineF(0, profileYAxis->pos().y(), 0, timeAxis->pos().y())); + disconnectTemporaryConnections(); + //TODO: Move this method to another place, shouldn't be on mainwindow. + MainWindow::instance()->disableShortcuts(); + actionsForKeys[Qt::Key_Left]->setShortcut(Qt::Key_Left); + actionsForKeys[Qt::Key_Right]->setShortcut(Qt::Key_Right); + actionsForKeys[Qt::Key_Up]->setShortcut(Qt::Key_Up); + actionsForKeys[Qt::Key_Down]->setShortcut(Qt::Key_Down); + actionsForKeys[Qt::Key_Escape]->setShortcut(Qt::Key_Escape); + actionsForKeys[Qt::Key_Delete]->setShortcut(Qt::Key_Delete); + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + connect(plannerModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(replot())); + connect(plannerModel, SIGNAL(cylinderModelEdited()), this, SLOT(replot())); + connect(plannerModel, SIGNAL(rowsInserted(const QModelIndex &, int, int)), + this, SLOT(pointInserted(const QModelIndex &, int, int))); + connect(plannerModel, SIGNAL(rowsRemoved(const QModelIndex &, int, int)), + this, SLOT(pointsRemoved(const QModelIndex &, int, int))); + /* show the same stuff that the profile shows. */ + currentState = PLAN; /* enable the add state. */ + diveCeiling->setVisible(true); + gradientFactor->setVisible(true); + setBackgroundBrush(QColor("#D7E3EF")); +} + +extern struct ev_select *ev_namelist; +extern int evn_allocated; +extern int evn_used; + +bool ProfileWidget2::isPlanner() +{ + return currentState == PLAN; +} + +bool ProfileWidget2::isAddOrPlanner() +{ + return currentState == PLAN || currentState == ADD; +} + +struct plot_data *ProfileWidget2::getEntryFromPos(QPointF pos) +{ + // find the time stamp corresponding to the mouse position + int seconds = timeAxis->valueAt(pos); + struct plot_data *entry = NULL; + + for (int i = 0; i < plotInfo.nr; i++) { + entry = plotInfo.entry + i; + if (entry->sec >= seconds) + break; + } + return entry; +} + +void ProfileWidget2::setReplot(bool state) +{ + replotEnabled = state; +} + +void ProfileWidget2::contextMenuEvent(QContextMenuEvent *event) +{ + if (currentState == ADD || currentState == PLAN) { + QGraphicsView::contextMenuEvent(event); + return; + } + QMenu m; + bool isDCName = false; + if (selected_dive == -1) + return; + // figure out if we are ontop of the dive computer name in the profile + QGraphicsItem *sceneItem = itemAt(mapFromGlobal(event->globalPos())); + if (sceneItem) { + QGraphicsItem *parentItem = sceneItem; + while (parentItem) { + if (parentItem->data(SUBSURFACE_OBJ_DATA) == SUBSURFACE_OBJ_DC_TEXT) { + isDCName = true; + break; + } + parentItem = parentItem->parentItem(); + } + if (isDCName) { + if (dc_number == 0 && count_divecomputers() == 1) + // nothing to do, can't delete or reorder + return; + // create menu to show when right clicking on dive computer name + if (dc_number > 0) + m.addAction(tr("Make first divecomputer"), this, SLOT(makeFirstDC())); + if (count_divecomputers() > 1) + m.addAction(tr("Delete this divecomputer"), this, SLOT(deleteCurrentDC())); + m.exec(event->globalPos()); + // don't show the regular profile context menu + return; + } + } + // create the profile context menu + QPointF scenePos = mapToScene(event->pos()); + struct plot_data *entry = getEntryFromPos(scenePos); + GasSelectionModel *model = GasSelectionModel::instance(); + model->repopulate(); + int rowCount = model->rowCount(); + if (rowCount > 1) { + // if we have more than one gas, offer to switch to another one + QMenu *gasChange = m.addMenu(tr("Add gas change")); + for (int i = 0; i < rowCount; i++) { + QAction *action = new QAction(&m); + action->setText(model->data(model->index(i, 0), Qt::DisplayRole).toString() + QString(tr(" (Tank %1)")).arg(i + 1)); + connect(action, SIGNAL(triggered(bool)), this, SLOT(changeGas())); + action->setData(event->globalPos()); + if (i == entry->cylinderindex) + action->setDisabled(true); + gasChange->addAction(action); + } + } + QAction *setpointAction = m.addAction(tr("Add set-point change"), this, SLOT(addSetpointChange())); + setpointAction->setData(event->globalPos()); + QAction *action = m.addAction(tr("Add bookmark"), this, SLOT(addBookmark())); + action->setData(event->globalPos()); + + if (same_string(current_dc->model, "manually added dive")) + QAction *editProfileAction = m.addAction(tr("Edit the profile"), MainWindow::instance(), SLOT(editCurrentDive())); + + if (DiveEventItem *item = dynamic_cast<DiveEventItem *>(sceneItem)) { + action = new QAction(&m); + action->setText(tr("Remove event")); + action->setData(QVariant::fromValue<void *>(item)); // so we know what to remove. + connect(action, SIGNAL(triggered(bool)), this, SLOT(removeEvent())); + m.addAction(action); + action = new QAction(&m); + action->setText(tr("Hide similar events")); + action->setData(QVariant::fromValue<void *>(item)); + connect(action, SIGNAL(triggered(bool)), this, SLOT(hideEvents())); + m.addAction(action); + struct event *dcEvent = item->getEvent(); + if (dcEvent->type == SAMPLE_EVENT_BOOKMARK) { + action = new QAction(&m); + action->setText(tr("Edit name")); + action->setData(QVariant::fromValue<void *>(item)); + connect(action, SIGNAL(triggered(bool)), this, SLOT(editName())); + m.addAction(action); + } +#if 0 // FIXME::: FINISH OR DISABLE + // this shows how to figure out if we should ask the user if they want adjust interpolated pressures + // at either side of a gas change + if (dcEvent->type == SAMPLE_EVENT_GASCHANGE || dcEvent->type == SAMPLE_EVENT_GASCHANGE2) { + qDebug() << "figure out if there are interpolated pressures"; + struct plot_data *gasChangeEntry = entry; + struct plot_data *newGasEntry; + while (gasChangeEntry > plotInfo.entry) { + --gasChangeEntry; + if (gasChangeEntry->sec <= dcEvent->time.seconds) + break; + } + qDebug() << "at gas change at" << gasChangeEntry->sec << ": sensor pressure" << gasChangeEntry->pressure[0] << "interpolated" << gasChangeEntry->pressure[1]; + // now gasChangeEntry points at the gas change, that entry has the final pressure of + // the old tank, the next entry has the starting pressure of the next tank + if (gasChangeEntry + 1 <= plotInfo.entry + plotInfo.nr) { + newGasEntry = gasChangeEntry + 1; + qDebug() << "after gas change at " << newGasEntry->sec << ": sensor pressure" << newGasEntry->pressure[0] << "interpolated" << newGasEntry->pressure[1]; + if (SENSOR_PRESSURE(gasChangeEntry) == 0 || displayed_dive.cylinder[gasChangeEntry->cylinderindex].sample_start.mbar == 0) { + // if we have no sensorpressure or if we have no pressure from samples we can assume that + // we only have interpolated pressure (the pressure in the entry may be stored in the sensor + // pressure field if this is the first or last entry for this tank... see details in gaspressures.c + pressure_t pressure; + pressure.mbar = INTERPOLATED_PRESSURE(gasChangeEntry) ? : SENSOR_PRESSURE(gasChangeEntry); + QAction *adjustOldPressure = m.addAction(tr("Adjust pressure of tank %1 (currently interpolated as %2)") + .arg(gasChangeEntry->cylinderindex + 1).arg(get_pressure_string(pressure))); + } + if (SENSOR_PRESSURE(newGasEntry) == 0 || displayed_dive.cylinder[newGasEntry->cylinderindex].sample_start.mbar == 0) { + // we only have interpolated press -- see commend above + pressure_t pressure; + pressure.mbar = INTERPOLATED_PRESSURE(newGasEntry) ? : SENSOR_PRESSURE(newGasEntry); + QAction *adjustOldPressure = m.addAction(tr("Adjust pressure of tank %1 (currently interpolated as %2)") + .arg(newGasEntry->cylinderindex + 1).arg(get_pressure_string(pressure))); + } + } + } +#endif + } + bool some_hidden = false; + for (int i = 0; i < evn_used; i++) { + if (ev_namelist[i].plot_ev == false) { + some_hidden = true; + break; + } + } + if (some_hidden) { + action = m.addAction(tr("Unhide all events"), this, SLOT(unhideEvents())); + action->setData(event->globalPos()); + } + m.exec(event->globalPos()); +} + +void ProfileWidget2::deleteCurrentDC() +{ + delete_current_divecomputer(); + mark_divelist_changed(true); + // we need to force it since it's likely the same dive and same dc_number - but that's a different dive computer now + MainWindow::instance()->graphics()->plotDive(0, true); + MainWindow::instance()->refreshDisplay(); +} + +void ProfileWidget2::makeFirstDC() +{ + make_first_dc(); + mark_divelist_changed(true); + // this is now the first DC, so we need to redraw the profile and refresh the dive list + // (and no, it's not just enough to rewrite the text - the first DC is special so values in the + // dive list may change). + // As a side benefit, this returns focus to the dive list. + dc_number = 0; + MainWindow::instance()->refreshDisplay(); +} + +void ProfileWidget2::hideEvents() +{ + QAction *action = qobject_cast<QAction *>(sender()); + DiveEventItem *item = static_cast<DiveEventItem *>(action->data().value<void *>()); + struct event *event = item->getEvent(); + + if (QMessageBox::question(MainWindow::instance(), + TITLE_OR_TEXT(tr("Hide events"), tr("Hide all %1 events?").arg(event->name)), + QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) { + if (!same_string(event->name, "")) { + for (int i = 0; i < evn_used; i++) { + if (same_string(event->name, ev_namelist[i].ev_name)) { + ev_namelist[i].plot_ev = false; + break; + } + } + Q_FOREACH (DiveEventItem *evItem, eventItems) { + if (same_string(evItem->getEvent()->name, event->name)) + evItem->hide(); + } + } else { + item->hide(); + } + } +} + +void ProfileWidget2::unhideEvents() +{ + for (int i = 0; i < evn_used; i++) { + ev_namelist[i].plot_ev = true; + } + Q_FOREACH (DiveEventItem *item, eventItems) + item->show(); +} + +void ProfileWidget2::removeEvent() +{ + QAction *action = qobject_cast<QAction *>(sender()); + DiveEventItem *item = static_cast<DiveEventItem *>(action->data().value<void *>()); + struct event *event = item->getEvent(); + + if (QMessageBox::question(MainWindow::instance(), TITLE_OR_TEXT( + tr("Remove the selected event?"), + tr("%1 @ %2:%3").arg(event->name).arg(event->time.seconds / 60).arg(event->time.seconds % 60, 2, 10, QChar('0'))), + QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) { + remove_event(event); + mark_divelist_changed(true); + replot(); + } +} + +void ProfileWidget2::addBookmark() +{ + QAction *action = qobject_cast<QAction *>(sender()); + QPointF scenePos = mapToScene(mapFromGlobal(action->data().toPoint())); + add_event(current_dc, timeAxis->valueAt(scenePos), SAMPLE_EVENT_BOOKMARK, 0, 0, "bookmark"); + mark_divelist_changed(true); + replot(); +} + +void ProfileWidget2::addSetpointChange() +{ + QAction *action = qobject_cast<QAction *>(sender()); + QPointF scenePos = mapToScene(mapFromGlobal(action->data().toPoint())); + SetpointDialog::instance()->setpointData(current_dc, timeAxis->valueAt(scenePos)); + SetpointDialog::instance()->show(); +} + +void ProfileWidget2::changeGas() +{ + QAction *action = qobject_cast<QAction *>(sender()); + QPointF scenePos = mapToScene(mapFromGlobal(action->data().toPoint())); + QString gas = action->text(); + gas.remove(QRegExp(" \\(.*\\)")); + + // backup the things on the dataModel, since we will clear that out. + struct gasmix gasmix; + qreal sec_val = timeAxis->valueAt(scenePos); + + // no gas changes before the dive starts + unsigned int seconds = (sec_val < 0.0) ? 0 : (unsigned int)sec_val; + + // if there is a gas change at this time stamp, remove it before adding the new one + struct event *gasChangeEvent = current_dc->events; + while ((gasChangeEvent = get_next_event(gasChangeEvent, "gaschange")) != NULL) { + if (gasChangeEvent->time.seconds == seconds) { + remove_event(gasChangeEvent); + gasChangeEvent = current_dc->events; + } else { + gasChangeEvent = gasChangeEvent->next; + } + } + validate_gas(gas.toUtf8().constData(), &gasmix); + QRegExp rx("\\(\\D*(\\d+)"); + int tank; + if (rx.indexIn(action->text()) > -1) { + tank = rx.cap(1).toInt() - 1; // we display the tank 1 based + } else { + qDebug() << "failed to parse tank number"; + tank = get_gasidx(&displayed_dive, &gasmix); + } + // add this both to the displayed dive and the current dive + add_gas_switch_event(current_dive, current_dc, seconds, tank); + add_gas_switch_event(&displayed_dive, get_dive_dc(&displayed_dive, dc_number), seconds, tank); + // this means we potentially have a new tank that is being used and needs to be shown + fixup_dive(&displayed_dive); + + // FIXME - this no longer gets written to the dive list - so we need to enableEdition() here + + MainWindow::instance()->information()->updateDiveInfo(); + mark_divelist_changed(true); + replot(); +} + +bool ProfileWidget2::getPrintMode() +{ + return printMode; +} + +void ProfileWidget2::setPrintMode(bool mode, bool grayscale) +{ + printMode = mode; + resetZoom(); + + // set printMode for axes + profileYAxis->setPrintMode(mode); + gasYAxis->setPrintMode(mode); + temperatureAxis->setPrintMode(mode); + timeAxis->setPrintMode(mode); + cylinderPressureAxis->setPrintMode(mode); + heartBeatAxis->setPrintMode(mode); + percentageAxis->setPrintMode(mode); + + isGrayscale = mode ? grayscale : false; + mouseFollowerHorizontal->setVisible(!mode); + mouseFollowerVertical->setVisible(!mode); +} + +void ProfileWidget2::setFontPrintScale(double scale) +{ + fontPrintScale = scale; + emit fontPrintScaleChanged(scale); +} + +double ProfileWidget2::getFontPrintScale() +{ + if (printMode) + return fontPrintScale; + else + return 1.0; +} + +void ProfileWidget2::editName() +{ + QAction *action = qobject_cast<QAction *>(sender()); + DiveEventItem *item = static_cast<DiveEventItem *>(action->data().value<void *>()); + struct event *event = item->getEvent(); + bool ok; + QString newName = QInputDialog::getText(MainWindow::instance(), tr("Edit name of bookmark"), + tr("Custom name:"), QLineEdit::Normal, + event->name, &ok); + if (ok && !newName.isEmpty()) { + if (newName.length() > 22) { //longer names will display as garbage. + QMessageBox lengthWarning; + lengthWarning.setText(tr("Name is too long!")); + lengthWarning.exec(); + return; + } + // order is important! first update the current dive (by matching the unchanged event), + // then update the displayed dive (as event is part of the events on displayed dive + // and will be freed as part of changing the name! + update_event_name(current_dive, event, newName.toUtf8().data()); + update_event_name(&displayed_dive, event, newName.toUtf8().data()); + mark_divelist_changed(true); + replot(); + } +} + +void ProfileWidget2::disconnectTemporaryConnections() +{ + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + disconnect(plannerModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(replot())); + disconnect(plannerModel, SIGNAL(cylinderModelEdited()), this, SLOT(replot())); + + disconnect(plannerModel, SIGNAL(rowsInserted(const QModelIndex &, int, int)), + this, SLOT(pointInserted(const QModelIndex &, int, int))); + disconnect(plannerModel, SIGNAL(rowsRemoved(const QModelIndex &, int, int)), + this, SLOT(pointsRemoved(const QModelIndex &, int, int))); + + Q_FOREACH (QAction *action, actionsForKeys.values()) { + action->setShortcut(QKeySequence()); + action->setShortcutContext(Qt::WidgetShortcut); + } +} + +void ProfileWidget2::pointInserted(const QModelIndex &parent, int start, int end) +{ + DiveHandler *item = new DiveHandler(); + scene()->addItem(item); + handles << item; + + connect(item, SIGNAL(moved()), this, SLOT(recreatePlannedDive())); + connect(item, SIGNAL(clicked()), this, SLOT(divePlannerHandlerClicked())); + connect(item, SIGNAL(released()), this, SLOT(divePlannerHandlerReleased())); + QGraphicsSimpleTextItem *gasChooseBtn = new QGraphicsSimpleTextItem(); + scene()->addItem(gasChooseBtn); + gasChooseBtn->setZValue(10); + gasChooseBtn->setFlag(QGraphicsItem::ItemIgnoresTransformations); + gases << gasChooseBtn; + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + if (plannerModel->recalcQ()) + replot(); +} + +void ProfileWidget2::pointsRemoved(const QModelIndex &, int start, int end) +{ // start and end are inclusive. + int num = (end - start) + 1; + for (int i = num; i != 0; i--) { + delete handles.back(); + handles.pop_back(); + delete gases.back(); + gases.pop_back(); + } + scene()->clearSelection(); + replot(); +} + +void ProfileWidget2::repositionDiveHandlers() +{ + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + // Re-position the user generated dive handlers + struct gasmix mix, lastmix; + for (int i = 0; i < plannerModel->rowCount(); i++) { + struct divedatapoint datapoint = plannerModel->at(i); + if (datapoint.time == 0) // those are the magic entries for tanks + continue; + DiveHandler *h = handles.at(i); + h->setVisible(datapoint.entered); + h->setPos(timeAxis->posAtValue(datapoint.time), profileYAxis->posAtValue(datapoint.depth)); + QPointF p1; + if (i == 0) { + if (prefs.drop_stone_mode) + // place the text on the straight line from the drop to stone position + p1 = QPointF(timeAxis->posAtValue(datapoint.depth / prefs.descrate), + profileYAxis->posAtValue(datapoint.depth)); + else + // place the text on the straight line from the origin to the first position + p1 = QPointF(timeAxis->posAtValue(0), profileYAxis->posAtValue(0)); + } else { + // place the text on the line from the last position + p1 = handles[i - 1]->pos(); + } + QPointF p2 = handles[i]->pos(); + QLineF line(p1, p2); + QPointF pos = line.pointAt(0.5); + gases[i]->setPos(pos); + gases[i]->setText(get_divepoint_gas_string(datapoint)); + gases[i]->setVisible(datapoint.entered && + (i == 0 || gases[i]->text() != gases[i-1]->text())); + } +} + +int ProfileWidget2::fixHandlerIndex(DiveHandler *activeHandler) +{ + int index = handles.indexOf(activeHandler); + if (index > 0 && index < handles.count() - 1) { + DiveHandler *before = handles[index - 1]; + if (before->pos().x() > activeHandler->pos().x()) { + handles.swap(index, index - 1); + return index - 1; + } + DiveHandler *after = handles[index + 1]; + if (after->pos().x() < activeHandler->pos().x()) { + handles.swap(index, index + 1); + return index + 1; + } + } + return index; +} + +void ProfileWidget2::recreatePlannedDive() +{ + DiveHandler *activeHandler = qobject_cast<DiveHandler *>(sender()); + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + int index = fixHandlerIndex(activeHandler); + int mintime = 0, maxtime = (timeAxis->maximum() + 10) * 60; + if (index > 0) + mintime = plannerModel->at(index - 1).time; + if (index < plannerModel->size() - 1) + maxtime = plannerModel->at(index + 1).time; + + int minutes = rint(timeAxis->valueAt(activeHandler->pos()) / 60); + if (minutes * 60 <= mintime || minutes * 60 >= maxtime) + return; + + divedatapoint data = plannerModel->at(index); + data.depth = rint(profileYAxis->valueAt(activeHandler->pos()) / M_OR_FT(1, 1)) * M_OR_FT(1, 1); + data.time = rint(timeAxis->valueAt(activeHandler->pos())); + + plannerModel->editStop(index, data); +} + +void ProfileWidget2::keyDownAction() +{ + if (currentState != ADD && currentState != PLAN) + return; + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { + if (DiveHandler *handler = qgraphicsitem_cast<DiveHandler *>(i)) { + int row = handles.indexOf(handler); + divedatapoint dp = plannerModel->at(row); + if (dp.depth >= profileYAxis->maximum()) + continue; + + dp.depth += M_OR_FT(1, 5); + plannerModel->editStop(row, dp); + } + } +} + +void ProfileWidget2::keyUpAction() +{ + if (currentState != ADD && currentState != PLAN) + return; + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { + if (DiveHandler *handler = qgraphicsitem_cast<DiveHandler *>(i)) { + int row = handles.indexOf(handler); + divedatapoint dp = plannerModel->at(row); + + if (dp.depth <= 0) + continue; + + dp.depth -= M_OR_FT(1, 5); + plannerModel->editStop(row, dp); + } + } +} + +void ProfileWidget2::keyLeftAction() +{ + if (currentState != ADD && currentState != PLAN) + return; + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { + if (DiveHandler *handler = qgraphicsitem_cast<DiveHandler *>(i)) { + int row = handles.indexOf(handler); + divedatapoint dp = plannerModel->at(row); + + if (dp.time / 60 <= 0) + continue; + + // don't overlap positions. + // maybe this is a good place for a 'goto'? + double xpos = timeAxis->posAtValue((dp.time - 60) / 60); + bool nextStep = false; + Q_FOREACH (DiveHandler *h, handles) { + if (IS_FP_SAME(h->pos().x(), xpos)) { + nextStep = true; + break; + } + } + if (nextStep) + continue; + + dp.time -= 60; + plannerModel->editStop(row, dp); + } + } +} + +void ProfileWidget2::keyRightAction() +{ + if (currentState != ADD && currentState != PLAN) + return; + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { + if (DiveHandler *handler = qgraphicsitem_cast<DiveHandler *>(i)) { + int row = handles.indexOf(handler); + divedatapoint dp = plannerModel->at(row); + if (dp.time / 60.0 >= timeAxis->maximum()) + continue; + + // don't overlap positions. + // maybe this is a good place for a 'goto'? + double xpos = timeAxis->posAtValue((dp.time + 60) / 60); + bool nextStep = false; + Q_FOREACH (DiveHandler *h, handles) { + if (IS_FP_SAME(h->pos().x(), xpos)) { + nextStep = true; + break; + } + } + if (nextStep) + continue; + + dp.time += 60; + plannerModel->editStop(row, dp); + } + } +} + +void ProfileWidget2::keyDeleteAction() +{ + if (currentState != ADD && currentState != PLAN) + return; + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + int selCount = scene()->selectedItems().count(); + if (selCount) { + QVector<int> selectedIndexes; + Q_FOREACH (QGraphicsItem *i, scene()->selectedItems()) { + if (DiveHandler *handler = qgraphicsitem_cast<DiveHandler *>(i)) { + selectedIndexes.push_back(handles.indexOf(handler)); + handler->hide(); + } + } + plannerModel->removeSelectedPoints(selectedIndexes); + } +} + +void ProfileWidget2::keyEscAction() +{ + if (currentState != ADD && currentState != PLAN) + return; + + if (scene()->selectedItems().count()) { + scene()->clearSelection(); + return; + } + + DivePlannerPointsModel *plannerModel = DivePlannerPointsModel::instance(); + if (plannerModel->isPlanner()) + plannerModel->cancelPlan(); +} + +void ProfileWidget2::plotPictures() +{ + Q_FOREACH (DivePictureItem *item, pictures) { + item->hide(); + item->deleteLater(); + } + pictures.clear(); + + if (printMode) + return; + + double x, y, lastX = -1.0, lastY = -1.0; + DivePictureModel *m = DivePictureModel::instance(); + for (int i = 0; i < m->rowCount(); i++) { + int offsetSeconds = m->index(i, 1).data(Qt::UserRole).value<int>(); + // it's a correct picture, but doesn't have a timestamp: only show on the widget near the + // information area. + if (!offsetSeconds) + continue; + DivePictureItem *item = new DivePictureItem(); + item->setPixmap(m->index(i, 0).data(Qt::DecorationRole).value<QPixmap>()); + item->setFileUrl(m->index(i, 1).data().toString()); + // let's put the picture at the correct time, but at a fixed "depth" on the profile + // not sure this is ideal, but it seems to look right. + x = timeAxis->posAtValue(offsetSeconds); + if (i == 0) + y = 10; + else if (fabs(x - lastX) < 4) + y = lastY + 3; + else + y = 10; + lastX = x; + lastY = y; + item->setPos(x, y); + scene()->addItem(item); + pictures.push_back(item); + } +} diff --git a/desktop-widgets/profile/profilewidget2.h b/desktop-widgets/profile/profilewidget2.h new file mode 100644 index 000000000..f11ec5be1 --- /dev/null +++ b/desktop-widgets/profile/profilewidget2.h @@ -0,0 +1,211 @@ +#ifndef PROFILEWIDGET2_H +#define PROFILEWIDGET2_H + +#include <QGraphicsView> + +// /* The idea of this widget is to display and edit the profile. +// * It has: +// * 1 - ToolTip / Legend item, displays every information of the current mouse position on it, plus the legends of the maps. +// * 2 - ToolBox, displays the QActions that are used to do special stuff on the profile ( like activating the plugins. ) +// * 3 - Cartesian Axis for depth ( y ) +// * 4 - Cartesian Axis for Gases ( y ) +// * 5 - Cartesian Axis for Time ( x ) +// * +// * It needs to be dynamic, things should *flow* on it, not just appear / disappear. +// */ +#include "divelineitem.h" +#include "diveprofileitem.h" +#include "display.h" + +class RulerItem2; +struct dive; +struct plot_info; +class ToolTipItem; +class DiveMeanDepth; +class DiveReportedCeiling; +class DiveTextItem; +class TemperatureAxis; +class DiveEventItem; +class DivePlotDataModel; +class DivePixmapItem; +class DiveRectItem; +class DepthAxis; +class DiveCartesianAxis; +class DiveProfileItem; +class TimeAxis; +class DiveTemperatureItem; +class DiveHeartrateItem; +class PercentageItem; +class DiveGasPressureItem; +class DiveCalculatedCeiling; +class DiveCalculatedTissue; +class PartialPressureGasItem; +class PartialGasPressureAxis; +class AbstractProfilePolygonItem; +class TankItem; +class DiveHandler; +class QGraphicsSimpleTextItem; +class QModelIndex; +class DivePictureItem; + +class ProfileWidget2 : public QGraphicsView { + Q_OBJECT +public: + enum State { + EMPTY, + PROFILE, + EDIT, + ADD, + PLAN, + INVALID + }; + enum Items { + BACKGROUND, + PROFILE_Y_AXIS, + GAS_Y_AXIS, + TIME_AXIS, + DEPTH_CONTROLLER, + TIME_CONTROLLER, + COLUMNS + }; + + ProfileWidget2(QWidget *parent = 0); + void resetZoom(); + void plotDive(struct dive *d = 0, bool force = false); + virtual bool eventFilter(QObject *, QEvent *); + void setupItem(AbstractProfilePolygonItem *item, DiveCartesianAxis *hAxis, DiveCartesianAxis *vAxis, DivePlotDataModel *model, int vData, int hData, int zValue); + void setPrintMode(bool mode, bool grayscale = false); + bool getPrintMode(); + bool isPointOutOfBoundaries(const QPointF &point) const; + bool isPlanner(); + bool isAddOrPlanner(); + double getFontPrintScale(); + void setFontPrintScale(double scale); + void clearHandlers(); + void recalcCeiling(); + void setToolTipVisibile(bool visible); + State currentState; + +signals: + void fontPrintScaleChanged(double scale); + +public +slots: // Necessary to call from QAction's signals. + void settingsChanged(); + void setEmptyState(); + void setProfileState(); + void setPlanState(); + void setAddState(); + void changeGas(); + void addSetpointChange(); + void addBookmark(); + void hideEvents(); + void unhideEvents(); + void removeEvent(); + void editName(); + void makeFirstDC(); + void deleteCurrentDC(); + void pointInserted(const QModelIndex &parent, int start, int end); + void pointsRemoved(const QModelIndex &, int start, int end); + void plotPictures(); + void setReplot(bool state); + void replot(dive *d = 0); + + /* this is called for every move on the handlers. maybe we can speed up this a bit? */ + void recreatePlannedDive(); + + /* key press handlers */ + void keyEscAction(); + void keyDeleteAction(); + void keyUpAction(); + void keyDownAction(); + void keyLeftAction(); + void keyRightAction(); + + void divePlannerHandlerClicked(); + void divePlannerHandlerReleased(); + +protected: + virtual ~ProfileWidget2(); + virtual void resizeEvent(QResizeEvent *event); + virtual void wheelEvent(QWheelEvent *event); + virtual void mouseMoveEvent(QMouseEvent *event); + virtual void contextMenuEvent(QContextMenuEvent *event); + virtual void mouseDoubleClickEvent(QMouseEvent *event); + virtual void mousePressEvent(QMouseEvent *event); + virtual void mouseReleaseEvent(QMouseEvent *event); + +private: /*methods*/ + void fixBackgroundPos(); + void scrollViewTo(const QPoint &pos); + void setupSceneAndFlags(); + void setupItemSizes(); + void addItemsToScene(); + void setupItemOnScene(); + void disconnectTemporaryConnections(); + struct plot_data *getEntryFromPos(QPointF pos); + +private: + DivePlotDataModel *dataModel; + int zoomLevel; + qreal zoomFactor; + DivePixmapItem *background; + QString backgroundFile; + ToolTipItem *toolTipItem; + bool isPlotZoomed; + bool replotEnabled; + // All those here should probably be merged into one structure, + // So it's esyer to replicate for more dives later. + // In the meantime, keep it here. + struct plot_info plotInfo; + DepthAxis *profileYAxis; + PartialGasPressureAxis *gasYAxis; + TemperatureAxis *temperatureAxis; + TimeAxis *timeAxis; + DiveProfileItem *diveProfileItem; + DiveTemperatureItem *temperatureItem; + DiveMeanDepthItem *meanDepthItem; + DiveCartesianAxis *cylinderPressureAxis; + DiveGasPressureItem *gasPressureItem; + QList<DiveEventItem *> eventItems; + DiveTextItem *diveComputerText; + DiveCalculatedCeiling *diveCeiling; + DiveTextItem *gradientFactor; + QList<DiveCalculatedTissue *> allTissues; + DiveReportedCeiling *reportedCeiling; + PartialPressureGasItem *pn2GasItem; + PartialPressureGasItem *pheGasItem; + PartialPressureGasItem *po2GasItem; + PartialPressureGasItem *o2SetpointGasItem; + PartialPressureGasItem *ccrsensor1GasItem; + PartialPressureGasItem *ccrsensor2GasItem; + PartialPressureGasItem *ccrsensor3GasItem; + DiveCartesianAxis *heartBeatAxis; + DiveHeartrateItem *heartBeatItem; + DiveCartesianAxis *percentageAxis; + QList<DivePercentageItem *> allPercentages; + DiveAmbPressureItem *ambPressureItem; + DiveGFLineItem *gflineItem; + DiveLineItem *mouseFollowerVertical; + DiveLineItem *mouseFollowerHorizontal; + RulerItem2 *rulerItem; + TankItem *tankItem; + bool isGrayscale; + bool printMode; + + //specifics for ADD and PLAN + QList<DiveHandler *> handles; + QList<QGraphicsSimpleTextItem *> gases; + QList<DivePictureItem *> pictures; + void repositionDiveHandlers(); + int fixHandlerIndex(DiveHandler *activeHandler); + friend class DiveHandler; + QHash<Qt::Key, QAction *> actionsForKeys; + bool shouldCalculateMaxTime; + bool shouldCalculateMaxDepth; + int maxtime; + int maxdepth; + double fontPrintScale; +}; + +#endif // PROFILEWIDGET2_H diff --git a/desktop-widgets/profile/ruleritem.cpp b/desktop-widgets/profile/ruleritem.cpp new file mode 100644 index 000000000..830985552 --- /dev/null +++ b/desktop-widgets/profile/ruleritem.cpp @@ -0,0 +1,179 @@ +#include "ruleritem.h" +#include "preferences.h" +#include "mainwindow.h" +#include "profilewidget2.h" +#include "display.h" + +#include <qgraphicssceneevent.h> + +#include "profile.h" + +RulerNodeItem2::RulerNodeItem2() : + entry(NULL), + ruler(NULL), + timeAxis(NULL), + depthAxis(NULL) +{ + memset(&pInfo, 0, sizeof(pInfo)); + setRect(-8, -8, 16, 16); + setBrush(QColor(0xff, 0, 0, 127)); + setPen(QColor(Qt::red)); + setFlag(ItemIsMovable); + setFlag(ItemSendsGeometryChanges); + setFlag(ItemIgnoresTransformations); +} + +void RulerNodeItem2::setPlotInfo(plot_info &info) +{ + pInfo = info; + entry = pInfo.entry; +} + +void RulerNodeItem2::setRuler(RulerItem2 *r) +{ + ruler = r; +} + +void RulerNodeItem2::recalculate() +{ + struct plot_data *data = pInfo.entry + (pInfo.nr - 1); + uint16_t count = 0; + if (x() < 0) { + setPos(0, y()); + } else if (x() > timeAxis->posAtValue(data->sec)) { + setPos(timeAxis->posAtValue(data->sec), depthAxis->posAtValue(data->depth)); + } else { + data = pInfo.entry; + count = 0; + while (timeAxis->posAtValue(data->sec) < x() && count < pInfo.nr) { + data = pInfo.entry + count; + count++; + } + setPos(timeAxis->posAtValue(data->sec), depthAxis->posAtValue(data->depth)); + entry = data; + } +} + +void RulerNodeItem2::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +{ + qreal x = event->scenePos().x(); + if (x < 0.0) + x = 0.0; + setPos(x, event->scenePos().y()); + recalculate(); + ruler->recalculate(); +} + +RulerItem2::RulerItem2() : source(new RulerNodeItem2()), + dest(new RulerNodeItem2()), + timeAxis(NULL), + depthAxis(NULL), + textItemBack(new QGraphicsRectItem(this)), + textItem(new QGraphicsSimpleTextItem(this)) +{ + memset(&pInfo, 0, sizeof(pInfo)); + source->setRuler(this); + dest->setRuler(this); + textItem->setFlag(QGraphicsItem::ItemIgnoresTransformations); + textItemBack->setBrush(QColor(0xff, 0xff, 0xff, 190)); + textItemBack->setPen(QColor(Qt::white)); + textItemBack->setFlag(QGraphicsItem::ItemIgnoresTransformations); + setPen(QPen(QColor(Qt::black), 0.0)); + connect(PreferencesDialog::instance(), SIGNAL(settingsChanged()), this, SLOT(settingsChanged())); +} + +void RulerItem2::settingsChanged() +{ + ProfileWidget2 *profWidget = NULL; + if (scene() && scene()->views().count()) + profWidget = qobject_cast<ProfileWidget2 *>(scene()->views().first()); + + if (profWidget && profWidget->currentState == ProfileWidget2::PROFILE) + setVisible(prefs.rulergraph); + else + setVisible(false); +} + +void RulerItem2::recalculate() +{ + char buffer[500]; + QPointF tmp; + QFont font; + QFontMetrics fm(font); + + if (timeAxis == NULL || depthAxis == NULL || pInfo.nr == 0) + return; + + prepareGeometryChange(); + startPoint = mapFromItem(source, 0, 0); + endPoint = mapFromItem(dest, 0, 0); + + if (startPoint.x() > endPoint.x()) { + tmp = endPoint; + endPoint = startPoint; + startPoint = tmp; + } + QLineF line(startPoint, endPoint); + setLine(line); + compare_samples(source->entry, dest->entry, buffer, 500, 1); + text = QString(buffer); + + // draw text + QGraphicsView *view = scene()->views().first(); + QPoint begin = view->mapFromScene(mapToScene(startPoint)); + textItem->setText(text); + qreal tgtX = startPoint.x(); + const qreal diff = begin.x() + textItem->boundingRect().width(); + // clamp so that the text doesn't go out of the screen to the right + if (diff > view->width()) { + begin.setX(begin.x() - (diff - view->width())); + tgtX = mapFromScene(view->mapToScene(begin)).x(); + } + // always show the text bellow the lowest of the start and end points + qreal tgtY = (startPoint.y() >= endPoint.y()) ? startPoint.y() : endPoint.y(); + // this isn't exactly optimal, since we want to scale the 1.0, 4.0 distances as well + textItem->setPos(tgtX - 1.0, tgtY + 4.0); + + // setup the text background + textItemBack->setVisible(startPoint.x() != endPoint.x()); + textItemBack->setPos(textItem->x(), textItem->y()); + textItemBack->setRect(0, 0, textItem->boundingRect().width(), textItem->boundingRect().height()); +} + +RulerNodeItem2 *RulerItem2::sourceNode() const +{ + return source; +} + +RulerNodeItem2 *RulerItem2::destNode() const +{ + return dest; +} + +void RulerItem2::setPlotInfo(plot_info info) +{ + pInfo = info; + dest->setPlotInfo(info); + source->setPlotInfo(info); + dest->recalculate(); + source->recalculate(); + recalculate(); +} + +void RulerItem2::setAxis(DiveCartesianAxis *time, DiveCartesianAxis *depth) +{ + timeAxis = time; + depthAxis = depth; + dest->depthAxis = depth; + dest->timeAxis = time; + source->depthAxis = depth; + source->timeAxis = time; + recalculate(); +} + +void RulerItem2::setVisible(bool visible) +{ + QGraphicsLineItem::setVisible(visible); + source->setVisible(visible); + dest->setVisible(visible); +} diff --git a/desktop-widgets/profile/ruleritem.h b/desktop-widgets/profile/ruleritem.h new file mode 100644 index 000000000..4fad0451c --- /dev/null +++ b/desktop-widgets/profile/ruleritem.h @@ -0,0 +1,59 @@ +#ifndef RULERITEM_H +#define RULERITEM_H + +#include <QObject> +#include <QGraphicsEllipseItem> +#include <QGraphicsObject> +#include "divecartesianaxis.h" +#include "display.h" + +struct plot_data; +class RulerItem2; + +class RulerNodeItem2 : public QObject, public QGraphicsEllipseItem { + Q_OBJECT + friend class RulerItem2; + +public: + explicit RulerNodeItem2(); + void setRuler(RulerItem2 *r); + void setPlotInfo(struct plot_info &info); + void recalculate(); + +protected: + virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *event); +private: + struct plot_info pInfo; + struct plot_data *entry; + RulerItem2 *ruler; + DiveCartesianAxis *timeAxis; + DiveCartesianAxis *depthAxis; +}; + +class RulerItem2 : public QObject, public QGraphicsLineItem { + Q_OBJECT +public: + explicit RulerItem2(); + void recalculate(); + + void setPlotInfo(struct plot_info pInfo); + RulerNodeItem2 *sourceNode() const; + RulerNodeItem2 *destNode() const; + void setAxis(DiveCartesianAxis *time, DiveCartesianAxis *depth); + void setVisible(bool visible); + +public +slots: + void settingsChanged(); + +private: + struct plot_info pInfo; + QPointF startPoint, endPoint; + RulerNodeItem2 *source, *dest; + QString text; + DiveCartesianAxis *timeAxis; + DiveCartesianAxis *depthAxis; + QGraphicsRectItem *textItemBack; + QGraphicsSimpleTextItem *textItem; +}; +#endif diff --git a/desktop-widgets/profile/tankitem.cpp b/desktop-widgets/profile/tankitem.cpp new file mode 100644 index 000000000..c0e75a371 --- /dev/null +++ b/desktop-widgets/profile/tankitem.cpp @@ -0,0 +1,120 @@ +#include "tankitem.h" +#include "diveplotdatamodel.h" +#include "divetextitem.h" +#include "profile.h" +#include <QPen> + +TankItem::TankItem(QObject *parent) : + QGraphicsRectItem(), + dataModel(0), + pInfoEntry(0), + pInfoNr(0) +{ + height = 3; + QColor red(PERSIANRED1); + QColor blue(AIR_BLUE); + QColor yellow(NITROX_YELLOW); + QColor green(NITROX_GREEN); + QLinearGradient nitroxGradient(QPointF(0, 0), QPointF(0, height)); + nitroxGradient.setColorAt(0.0, green); + nitroxGradient.setColorAt(0.49, green); + nitroxGradient.setColorAt(0.5, yellow); + nitroxGradient.setColorAt(1.0, yellow); + nitrox = nitroxGradient; + oxygen = green; + QLinearGradient trimixGradient(QPointF(0, 0), QPointF(0, height)); + trimixGradient.setColorAt(0.0, green); + trimixGradient.setColorAt(0.49, green); + trimixGradient.setColorAt(0.5, red); + trimixGradient.setColorAt(1.0, red); + trimix = trimixGradient; + air = blue; + memset(&diveCylinderStore, 0, sizeof(diveCylinderStore)); +} + +TankItem::~TankItem() +{ + // Should this be clear_dive(diveCylinderStore)? + for (int i = 0; i < MAX_CYLINDERS; i++) + free((void *)diveCylinderStore.cylinder[i].type.description); +} + +void TankItem::setData(DivePlotDataModel *model, struct plot_info *plotInfo, struct dive *d) +{ + free(pInfoEntry); + // the plotInfo and dive structures passed in could become invalid before we stop using them, + // so copy the data that we need + int size = plotInfo->nr * sizeof(plotInfo->entry[0]); + pInfoEntry = (struct plot_data *)malloc(size); + pInfoNr = plotInfo->nr; + memcpy(pInfoEntry, plotInfo->entry, size); + copy_cylinders(d, &diveCylinderStore, false); + dataModel = model; + connect(dataModel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(modelDataChanged(QModelIndex, QModelIndex)), Qt::UniqueConnection); + modelDataChanged(); +} + +void TankItem::createBar(qreal x, qreal w, struct gasmix *gas) +{ + // pick the right gradient, size, position and text + QGraphicsRectItem *rect = new QGraphicsRectItem(x, 0, w, height, this); + if (gasmix_is_air(gas)) + rect->setBrush(air); + else if (gas->he.permille) + rect->setBrush(trimix); + else if (gas->o2.permille == 1000) + rect->setBrush(oxygen); + else + rect->setBrush(nitrox); + rect->setPen(QPen(QBrush(), 0.0)); // get rid of the thick line around the rectangle + rects.push_back(rect); + DiveTextItem *label = new DiveTextItem(rect); + label->setText(gasname(gas)); + label->setBrush(Qt::black); + label->setPos(x + 1, 0); + label->setAlignment(Qt::AlignBottom | Qt::AlignRight); + label->setZValue(101); +} + +void TankItem::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + // We don't have enougth data to calculate things, quit. + + if (!dataModel || !pInfoEntry || !pInfoNr) + return; + + // remove the old rectangles + foreach (QGraphicsRectItem *r, rects) { + delete(r); + } + rects.clear(); + + // walk the list and figure out which tanks go where + struct plot_data *entry = pInfoEntry; + int cylIdx = entry->cylinderindex; + int i = -1; + int startTime = 0; + struct gasmix *gas = &diveCylinderStore.cylinder[cylIdx].gasmix; + qreal width, left; + while (++i < pInfoNr) { + entry = &pInfoEntry[i]; + if (entry->cylinderindex == cylIdx) + continue; + width = hAxis->posAtValue(entry->sec) - hAxis->posAtValue(startTime); + left = hAxis->posAtValue(startTime); + createBar(left, width, gas); + cylIdx = entry->cylinderindex; + gas = &diveCylinderStore.cylinder[cylIdx].gasmix; + startTime = entry->sec; + } + width = hAxis->posAtValue(entry->sec) - hAxis->posAtValue(startTime); + left = hAxis->posAtValue(startTime); + createBar(left, width, gas); +} + +void TankItem::setHorizontalAxis(DiveCartesianAxis *horizontal) +{ + hAxis = horizontal; + connect(hAxis, SIGNAL(sizeChanged()), this, SLOT(modelDataChanged())); + modelDataChanged(); +} diff --git a/desktop-widgets/profile/tankitem.h b/desktop-widgets/profile/tankitem.h new file mode 100644 index 000000000..fd685fc82 --- /dev/null +++ b/desktop-widgets/profile/tankitem.h @@ -0,0 +1,39 @@ +#ifndef TANKITEM_H +#define TANKITEM_H + +#include <QGraphicsItem> +#include <QModelIndex> +#include <QBrush> +#include "divelineitem.h" +#include "divecartesianaxis.h" +#include "dive.h" + +class TankItem : public QObject, public QGraphicsRectItem +{ + Q_OBJECT + +public: + explicit TankItem(QObject *parent = 0); + ~TankItem(); + void setHorizontalAxis(DiveCartesianAxis *horizontal); + void setData(DivePlotDataModel *model, struct plot_info *plotInfo, struct dive *d); + +signals: + +public slots: + virtual void modelDataChanged(const QModelIndex &topLeft = QModelIndex(), const QModelIndex &bottomRight = QModelIndex()); + +private: + void createBar(qreal x, qreal w, struct gasmix *gas); + DivePlotDataModel *dataModel; + DiveCartesianAxis *hAxis; + int hDataColumn; + struct dive diveCylinderStore; + struct plot_data *pInfoEntry; + int pInfoNr; + qreal height; + QBrush air, nitrox, oxygen, trimix; + QList<QGraphicsRectItem *> rects; +}; + +#endif // TANKITEM_H diff --git a/desktop-widgets/qtwaitingspinner.cpp b/desktop-widgets/qtwaitingspinner.cpp new file mode 100644 index 000000000..14e8669b0 --- /dev/null +++ b/desktop-widgets/qtwaitingspinner.cpp @@ -0,0 +1,288 @@ + +/* Original Work Copyright (c) 2012-2014 Alexander Turkin + Modified 2014 by William Hallatt + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#include <cmath> +#include <algorithm> + +#include <QPainter> +#include <QTimer> + +#include "qtwaitingspinner.h" + +/*----------------------------------------------------------------------------*/ + +// Defaults +const QColor c_color(Qt::black); +const qreal c_roundness(70.0); +const qreal c_minTrailOpacity(15.0); +const qreal c_trailFadePercentage(70.0); +const int c_lines(12); +const int c_lineLength(10); +const int c_lineWidth(5); +const int c_innerRadius(10); +const int c_revPerSec(1); + +/*----------------------------------------------------------------------------*/ + +QtWaitingSpinner::QtWaitingSpinner(QWidget *parent) + : QWidget(parent), + + // Configurable settings. + m_color(c_color), m_roundness(c_roundness), + m_minTrailOpacity(c_minTrailOpacity), + m_trailFadePercentage(c_trailFadePercentage), m_revPerSec(c_revPerSec), + m_numberOfLines(c_lines), m_lineLength(c_lineLength + c_lineWidth), + m_lineWidth(c_lineWidth), m_innerRadius(c_innerRadius), + + // Other + m_timer(NULL), m_parent(parent), m_centreOnParent(false), + m_currentCounter(0), m_isSpinning(false) { + initialise(); +} + +/*----------------------------------------------------------------------------*/ + +QtWaitingSpinner::QtWaitingSpinner(Qt::WindowModality modality, QWidget *parent, + bool centreOnParent) + : QWidget(parent, Qt::Dialog | Qt::FramelessWindowHint), + + // Configurable settings. + m_color(c_color), m_roundness(c_roundness), + m_minTrailOpacity(c_minTrailOpacity), + m_trailFadePercentage(c_trailFadePercentage), m_revPerSec(c_revPerSec), + m_numberOfLines(c_lines), m_lineLength(c_lineLength + c_lineWidth), + m_lineWidth(c_lineWidth), m_innerRadius(c_innerRadius), + + // Other + m_timer(NULL), m_parent(parent), m_centreOnParent(centreOnParent), + m_currentCounter(0) { + initialise(); + + // We need to set the window modality AFTER we've hidden the + // widget for the first time since changing this property while + // the widget is visible has no effect. + this->setWindowModality(modality); + this->setAttribute(Qt::WA_TranslucentBackground); +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::initialise() { + m_timer = new QTimer(this); + connect(m_timer, SIGNAL(timeout()), this, SLOT(rotate())); + updateSize(); + updateTimer(); + this->hide(); +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::paintEvent(QPaintEvent * /*ev*/) { + QPainter painter(this); + painter.fillRect(this->rect(), Qt::transparent); + painter.setRenderHint(QPainter::Antialiasing, true); + + if (m_currentCounter >= m_numberOfLines) { + m_currentCounter = 0; + } + painter.setPen(Qt::NoPen); + for (int i = 0; i < m_numberOfLines; ++i) { + painter.save(); + painter.translate(m_innerRadius + m_lineLength, + m_innerRadius + m_lineLength); + qreal rotateAngle = + static_cast<qreal>(360 * i) / static_cast<qreal>(m_numberOfLines); + painter.rotate(rotateAngle); + painter.translate(m_innerRadius, 0); + int distance = + lineCountDistanceFromPrimary(i, m_currentCounter, m_numberOfLines); + QColor color = + currentLineColor(distance, m_numberOfLines, m_trailFadePercentage, + m_minTrailOpacity, m_color); + painter.setBrush(color); + // TODO improve the way rounded rect is painted + painter.drawRoundedRect( + QRect(0, -m_lineWidth / 2, m_lineLength, m_lineWidth), m_roundness, + m_roundness, Qt::RelativeSize); + painter.restore(); + } +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::start() { + updatePosition(); + m_isSpinning = true; + this->show(); + if (!m_timer->isActive()) { + m_timer->start(); + m_currentCounter = 0; + } +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::stop() { + m_isSpinning = false; + this->hide(); + if (m_timer->isActive()) { + m_timer->stop(); + m_currentCounter = 0; + } +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::setNumberOfLines(int lines) { + m_numberOfLines = lines; + m_currentCounter = 0; + updateTimer(); +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::setLineLength(int length) { + m_lineLength = length; + updateSize(); +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::setLineWidth(int width) { + m_lineWidth = width; + updateSize(); +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::setInnerRadius(int radius) { + m_innerRadius = radius; + updateSize(); +} + +/*----------------------------------------------------------------------------*/ + +bool QtWaitingSpinner::isSpinning() const { return m_isSpinning; } + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::setRoundness(qreal roundness) { + m_roundness = std::max(0.0, std::min(100.0, roundness)); +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::setColor(QColor color) { m_color = color; } + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::setRevolutionsPerSecond(int rps) { + m_revPerSec = rps; + updateTimer(); +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::setTrailFadePercentage(qreal trail) { + m_trailFadePercentage = trail; +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::setMinimumTrailOpacity(qreal minOpacity) { + m_minTrailOpacity = minOpacity; +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::rotate() { + ++m_currentCounter; + if (m_currentCounter >= m_numberOfLines) { + m_currentCounter = 0; + } + update(); +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::updateSize() { + int size = (m_innerRadius + m_lineLength) * 2; + setFixedSize(size, size); +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::updateTimer() { + m_timer->setInterval(calculateTimerInterval(m_numberOfLines, m_revPerSec)); +} + +/*----------------------------------------------------------------------------*/ + +void QtWaitingSpinner::updatePosition() { + if (m_parent && m_centreOnParent) { + this->move(m_parent->frameGeometry().topLeft() + m_parent->rect().center() - + this->rect().center()); + } +} + +/*----------------------------------------------------------------------------*/ + +int QtWaitingSpinner::calculateTimerInterval(int lines, int speed) { + return 1000 / (lines * speed); +} + +/*----------------------------------------------------------------------------*/ + +int QtWaitingSpinner::lineCountDistanceFromPrimary(int current, int primary, + int totalNrOfLines) { + int distance = primary - current; + if (distance < 0) { + distance += totalNrOfLines; + } + return distance; +} + +/*----------------------------------------------------------------------------*/ + +QColor QtWaitingSpinner::currentLineColor(int countDistance, int totalNrOfLines, + qreal trailFadePerc, qreal minOpacity, + QColor color) { + if (countDistance == 0) { + return color; + } + const qreal minAlphaF = minOpacity / 100.0; + int distanceThreshold = + static_cast<int>(ceil((totalNrOfLines - 1) * trailFadePerc / 100.0)); + if (countDistance > distanceThreshold) { + color.setAlphaF(minAlphaF); + } else { + qreal alphaDiff = color.alphaF() - minAlphaF; + qreal gradient = alphaDiff / static_cast<qreal>(distanceThreshold + 1); + qreal resultAlpha = color.alphaF() - gradient * countDistance; + + // If alpha is out of bounds, clip it. + resultAlpha = std::min(1.0, std::max(0.0, resultAlpha)); + color.setAlphaF(resultAlpha); + } + return color; +} + +/*----------------------------------------------------------------------------*/ diff --git a/desktop-widgets/qtwaitingspinner.h b/desktop-widgets/qtwaitingspinner.h new file mode 100644 index 000000000..254b52ec7 --- /dev/null +++ b/desktop-widgets/qtwaitingspinner.h @@ -0,0 +1,103 @@ +/* Original Work Copyright (c) 2012-2014 Alexander Turkin + Modified 2014 by William Hallatt + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#ifndef QTWAITINGSPINNER_H +#define QTWAITINGSPINNER_H + +#include <QWidget> + +#include <QTimer> +#include <QColor> + +class QtWaitingSpinner : public QWidget { + Q_OBJECT +public: + /*! Constructor for "standard" widget behaviour - use this + * constructor if you wish to, e.g. embed your widget in another. */ + QtWaitingSpinner(QWidget *parent = 0); + + /*! Constructor - use this constructor to automatically create a modal + * ("blocking") spinner on top of the calling widget/window. If a valid + * parent widget is provided, "centreOnParent" will ensure that + * QtWaitingSpinner automatically centres itself on it, if not, + * "centreOnParent" is ignored. */ + QtWaitingSpinner(Qt::WindowModality modality, QWidget *parent = 0, + bool centreOnParent = true); + +public Q_SLOTS: + void start(); + void stop(); + +public: + void setColor(QColor color); + void setRoundness(qreal roundness); + void setMinimumTrailOpacity(qreal minOpacity); + void setTrailFadePercentage(qreal trail); + void setRevolutionsPerSecond(int rps); + void setNumberOfLines(int lines); + void setLineLength(int length); + void setLineWidth(int width); + void setInnerRadius(int radius); + + bool isSpinning() const; + +private Q_SLOTS: + void rotate(); + +protected: + void paintEvent(QPaintEvent *ev); + +private: + static int calculateTimerInterval(int lines, int speed); + static int lineCountDistanceFromPrimary(int current, int primary, + int totalNrOfLines); + static QColor currentLineColor(int distance, int totalNrOfLines, + qreal trailFadePerc, qreal minOpacity, + QColor color); + + void initialise(); + void updateSize(); + void updateTimer(); + void updatePosition(); + +private: + // Configurable settings. + QColor m_color; + qreal m_roundness; // 0..100 + qreal m_minTrailOpacity; + qreal m_trailFadePercentage; + int m_revPerSec; // revolutions per second + int m_numberOfLines; + int m_lineLength; + int m_lineWidth; + int m_innerRadius; + +private: + QtWaitingSpinner(const QtWaitingSpinner&); + QtWaitingSpinner& operator=(const QtWaitingSpinner&); + + QTimer *m_timer; + QWidget *m_parent; + bool m_centreOnParent; + int m_currentCounter; + bool m_isSpinning; +}; + +#endif // QTWAITINGSPINNER_H diff --git a/desktop-widgets/renumber.ui b/desktop-widgets/renumber.ui new file mode 100644 index 000000000..00e7b84bb --- /dev/null +++ b/desktop-widgets/renumber.ui @@ -0,0 +1,120 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>RenumberDialog</class> + <widget class="QDialog" name="RenumberDialog"> + <property name="windowModality"> + <enum>Qt::WindowModal</enum> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>211</width> + <height>125</height> + </rect> + </property> + <property name="windowTitle"> + <string>Renumber</string> + </property> + <property name="windowIcon"> + <iconset> + <normalon>:/subsurface-icon</normalon> + </iconset> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="spacing"> + <number>1</number> + </property> + <property name="leftMargin"> + <number>3</number> + </property> + <property name="topMargin"> + <number>3</number> + </property> + <property name="rightMargin"> + <number>3</number> + </property> + <property name="bottomMargin"> + <number>3</number> + </property> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>New starting number</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QSpinBox" name="spinBox"> + <property name="maximum"> + <number>99999</number> + </property> + <property name="value"> + <number>1</number> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources> + <include location="../subsurface.qrc"/> + </resources> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>RenumberDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>RenumberDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/desktop-widgets/searchbar.ui b/desktop-widgets/searchbar.ui new file mode 100644 index 000000000..22bce39c6 --- /dev/null +++ b/desktop-widgets/searchbar.ui @@ -0,0 +1,134 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SearchBar</class> + <widget class="QWidget" name="SearchBar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>34</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>2</number> + </property> + <property name="leftMargin"> + <number>2</number> + </property> + <property name="topMargin"> + <number>2</number> + </property> + <property name="rightMargin"> + <number>2</number> + </property> + <property name="bottomMargin"> + <number>2</number> + </property> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLineEdit" name="searchEdit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>100</width> + <height>0</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="findPrev"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset theme="go-up"> + <normaloff/> + </iconset> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="findNext"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset theme="go-down"> + <normaloff/> + </iconset> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="findClose"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset theme="window-close"> + <normaloff/> + </iconset> + </property> + <property name="flat"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/desktop-widgets/setpoint.ui b/desktop-widgets/setpoint.ui new file mode 100644 index 000000000..d96488a31 --- /dev/null +++ b/desktop-widgets/setpoint.ui @@ -0,0 +1,130 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SetpointDialog</class> + <widget class="QDialog" name="SetpointDialog"> + <property name="windowModality"> + <enum>Qt::WindowModal</enum> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>211</width> + <height>125</height> + </rect> + </property> + <property name="windowTitle"> + <string>Renumber</string> + </property> + <property name="windowIcon"> + <iconset> + <normalon>:/subsurface-icon</normalon> + </iconset> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="spacing"> + <number>1</number> + </property> + <property name="leftMargin"> + <number>3</number> + </property> + <property name="topMargin"> + <number>3</number> + </property> + <property name="rightMargin"> + <number>3</number> + </property> + <property name="bottomMargin"> + <number>3</number> + </property> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>New set-point (0 for OC)</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QDoubleSpinBox" name="spinbox"> + <property name="suffix"> + <string>bar</string> + </property> + <property name="decimals"> + <number>1</number> + </property> + <property name="minimum"> + <double>0.000000000000000</double> + </property> + <property name="maximum"> + <double>2.000000000000000</double> + </property> + <property name="singleStep"> + <double>0.100000000000000</double> + </property> + <property name="value"> + <double>1.100000000000000</double> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>SetpointDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>SetpointDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/desktop-widgets/shiftimagetimes.ui b/desktop-widgets/shiftimagetimes.ui new file mode 100644 index 000000000..0478a62b4 --- /dev/null +++ b/desktop-widgets/shiftimagetimes.ui @@ -0,0 +1,293 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ShiftImageTimesDialog</class> + <widget class="QDialog" name="ShiftImageTimesDialog"> + <property name="windowModality"> + <enum>Qt::WindowModal</enum> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>693</width> + <height>606</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>Shift selected image times</string> + </property> + <property name="windowIcon"> + <iconset> + <normalon>:/subsurface-icon</normalon> + </iconset> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Shift times of image(s) by</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTimeEdit" name="timeEdit"> + <property name="date"> + <date> + <year>2000</year> + <month>1</month> + <day>1</day> + </date> + </property> + <property name="maximumDateTime"> + <datetime> + <hour>23</hour> + <minute>59</minute> + <second>59</second> + <year>2010</year> + <month>12</month> + <day>31</day> + </datetime> + </property> + <property name="minimumDateTime"> + <datetime> + <hour>0</hour> + <minute>0</minute> + <second>0</second> + <year>2000</year> + <month>1</month> + <day>1</day> + </datetime> + </property> + <property name="maximumDate"> + <date> + <year>2010</year> + <month>12</month> + <day>31</day> + </date> + </property> + <property name="minimumDate"> + <date> + <year>2000</year> + <month>1</month> + <day>1</day> + </date> + </property> + <property name="maximumTime"> + <time> + <hour>23</hour> + <minute>59</minute> + <second>59</second> + </time> + </property> + <property name="minimumTime"> + <time> + <hour>0</hour> + <minute>0</minute> + <second>0</second> + </time> + </property> + <property name="displayFormat"> + <string>h:mm</string> + </property> + <property name="timeSpec"> + <enum>Qt::LocalTime</enum> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="backwards"> + <property name="text"> + <string>Earlier</string> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="forward"> + <property name="text"> + <string>Later</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="warningLabel"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="styleSheet"> + <string notr="true">color: red;</string> + </property> + <property name="text"> + <string>Warning! +Not all images have timestamps in the range between +30 minutes before the start and 30 minutes after the end of any selected dive.</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="matchAllImages"> + <property name="text"> + <string>Load images even if the time does not match the dive time</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="invalidLabel"> + <property name="styleSheet"> + <string notr="true">color: red; </string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>60</height> + </size> + </property> + <property name="layoutDirection"> + <enum>Qt::LeftToRight</enum> + </property> + <property name="text"> + <string>To compute the offset between the clocks of your dive computer and your camera use your camera to take a picture of your dive compuer displaying the current time. Download that image to your computer and press this button.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="syncCamera"> + <property name="toolTip"> + <string>Determine camera time offset</string> + </property> + <property name="text"> + <string>Select image of divecomputer showing time</string> + </property> + </widget> + </item> + <item> + <widget class="QGroupBox" name="displayDC"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="title"> + <string/> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label_2"> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>60</height> + </size> + </property> + <property name="text"> + <string>Which date and time are displayed on the image?</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="1" rowspan="3"> + <widget class="QGraphicsView" name="DCImage"> + <property name="verticalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QDateTimeEdit" name="dcTime"/> + </item> + <item row="2" column="0"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>193</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <resources> + <include location="../subsurface.qrc"/> + </resources> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>ShiftImageTimesDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>ShiftImageTimesDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/desktop-widgets/shifttimes.ui b/desktop-widgets/shifttimes.ui new file mode 100644 index 000000000..486b1f43b --- /dev/null +++ b/desktop-widgets/shifttimes.ui @@ -0,0 +1,214 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ShiftTimesDialog</class> + <widget class="QDialog" name="ShiftTimesDialog"> + <property name="windowModality"> + <enum>Qt::WindowModal</enum> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>343</width> + <height>224</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>Shift selected dive times</string> + </property> + <property name="windowIcon"> + <iconset> + <normalon>:/subsurface-icon</normalon> + </iconset> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="spacing"> + <number>6</number> + </property> + <property name="leftMargin"> + <number>9</number> + </property> + <property name="topMargin"> + <number>9</number> + </property> + <property name="rightMargin"> + <number>9</number> + </property> + <property name="bottomMargin"> + <number>9</number> + </property> + <item alignment="Qt::AlignTop"> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Shift times of selected dives by</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="spacing"> + <number>6</number> + </property> + <property name="leftMargin"> + <number>9</number> + </property> + <property name="topMargin"> + <number>9</number> + </property> + <property name="rightMargin"> + <number>9</number> + </property> + <property name="bottomMargin"> + <number>9</number> + </property> + <item> + <layout class="QFormLayout" name="formLayout"> + <item row="2" column="0"> + <widget class="QLabel" name="shiftedTimeLabel"> + <property name="text"> + <string>Shifted time:</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="currentTimeLabel"> + <property name="text"> + <string>Current time:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="currentTime"> + <property name="text"> + <string>0:0</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLabel" name="shiftedTime"> + <property name="text"> + <string>0:0</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QTimeEdit" name="timeEdit"> + <property name="date"> + <date> + <year>2000</year> + <month>1</month> + <day>1</day> + </date> + </property> + <property name="minimumDateTime"> + <datetime> + <hour>0</hour> + <minute>0</minute> + <second>0</second> + <year>2000</year> + <month>1</month> + <day>1</day> + </datetime> + </property> + <property name="maximumDate"> + <date> + <year>2000</year> + <month>1</month> + <day>1</day> + </date> + </property> + <property name="displayFormat"> + <string>h:mm</string> + </property> + <property name="timeSpec"> + <enum>Qt::LocalTime</enum> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="backwards"> + <property name="text"> + <string>Earlier</string> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="forward"> + <property name="text"> + <string>Later</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources> + <include location="../subsurface.qrc"/> + </resources> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>ShiftTimesDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>ShiftTimesDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>timeEdit</sender> + <signal>timeChanged(const QTime)</signal> + <receiver>ShiftTimesDialog</receiver> + <slot>changeTime()</slot> + </connection> + <connection> + <sender>backwards</sender> + <signal>toggled(bool)</signal> + <receiver>ShiftTimesDialog</receiver> + <slot>changeTime()</slot> + </connection> + </connections> +</ui> diff --git a/desktop-widgets/simplewidgets.cpp b/desktop-widgets/simplewidgets.cpp new file mode 100644 index 000000000..62a9cc646 --- /dev/null +++ b/desktop-widgets/simplewidgets.cpp @@ -0,0 +1,736 @@ +#include "simplewidgets.h" +#include "filtermodels.h" + +#include <QProcess> +#include <QFileDialog> +#include <QShortcut> +#include <QCalendarWidget> +#include <QKeyEvent> +#include <QAction> + +#include "file.h" +#include "mainwindow.h" +#include "helpers.h" +#include "libdivecomputer/parser.h" +#include "divelistview.h" +#include "display.h" +#include "profile/profilewidget2.h" +#include "undocommands.h" + +class MinMaxAvgWidgetPrivate { +public: + QLabel *avgIco, *avgValue; + QLabel *minIco, *minValue; + QLabel *maxIco, *maxValue; + + MinMaxAvgWidgetPrivate(MinMaxAvgWidget *owner) + { + avgIco = new QLabel(owner); + avgIco->setPixmap(QIcon(":/average").pixmap(16, 16)); + avgIco->setToolTip(QObject::tr("Average")); + minIco = new QLabel(owner); + minIco->setPixmap(QIcon(":/minimum").pixmap(16, 16)); + minIco->setToolTip(QObject::tr("Minimum")); + maxIco = new QLabel(owner); + maxIco->setPixmap(QIcon(":/maximum").pixmap(16, 16)); + maxIco->setToolTip(QObject::tr("Maximum")); + avgValue = new QLabel(owner); + minValue = new QLabel(owner); + maxValue = new QLabel(owner); + + QGridLayout *formLayout = new QGridLayout(); + formLayout->addWidget(maxIco, 0, 0); + formLayout->addWidget(maxValue, 0, 1); + formLayout->addWidget(avgIco, 1, 0); + formLayout->addWidget(avgValue, 1, 1); + formLayout->addWidget(minIco, 2, 0); + formLayout->addWidget(minValue, 2, 1); + owner->setLayout(formLayout); + } +}; + +double MinMaxAvgWidget::average() const +{ + return d->avgValue->text().toDouble(); +} + +double MinMaxAvgWidget::maximum() const +{ + return d->maxValue->text().toDouble(); +} +double MinMaxAvgWidget::minimum() const +{ + return d->minValue->text().toDouble(); +} + +MinMaxAvgWidget::MinMaxAvgWidget(QWidget *parent) : d(new MinMaxAvgWidgetPrivate(this)) +{ +} + +MinMaxAvgWidget::~MinMaxAvgWidget() +{ +} + +void MinMaxAvgWidget::clear() +{ + d->avgValue->setText(QString()); + d->maxValue->setText(QString()); + d->minValue->setText(QString()); +} + +void MinMaxAvgWidget::setAverage(double average) +{ + d->avgValue->setText(QString::number(average)); +} + +void MinMaxAvgWidget::setMaximum(double maximum) +{ + d->maxValue->setText(QString::number(maximum)); +} +void MinMaxAvgWidget::setMinimum(double minimum) +{ + d->minValue->setText(QString::number(minimum)); +} + +void MinMaxAvgWidget::setAverage(const QString &average) +{ + d->avgValue->setText(average); +} + +void MinMaxAvgWidget::setMaximum(const QString &maximum) +{ + d->maxValue->setText(maximum); +} + +void MinMaxAvgWidget::setMinimum(const QString &minimum) +{ + d->minValue->setText(minimum); +} + +void MinMaxAvgWidget::overrideMinToolTipText(const QString &newTip) +{ + d->minIco->setToolTip(newTip); + d->minValue->setToolTip(newTip); +} + +void MinMaxAvgWidget::overrideAvgToolTipText(const QString &newTip) +{ + d->avgIco->setToolTip(newTip); + d->avgValue->setToolTip(newTip); +} + +void MinMaxAvgWidget::overrideMaxToolTipText(const QString &newTip) +{ + d->maxIco->setToolTip(newTip); + d->maxValue->setToolTip(newTip); +} + +RenumberDialog *RenumberDialog::instance() +{ + static RenumberDialog *self = new RenumberDialog(MainWindow::instance()); + return self; +} + +void RenumberDialog::renumberOnlySelected(bool selected) +{ + if (selected && amount_selected == 1) + ui.groupBox->setTitle(tr("New number")); + else + ui.groupBox->setTitle(tr("New starting number")); + selectedOnly = selected; +} + +void RenumberDialog::buttonClicked(QAbstractButton *button) +{ + if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { + MainWindow::instance()->dive_list()->rememberSelection(); + // we remember a map from dive uuid to a pair of old number / new number + QMap<int,QPair<int, int> > renumberedDives; + int i; + int newNr = ui.spinBox->value(); + struct dive *dive = NULL; + for_each_dive (i, dive) { + if (!selectedOnly || dive->selected) + renumberedDives.insert(dive->id, QPair<int,int>(dive->number, newNr++)); + } + UndoRenumberDives *undoCommand = new UndoRenumberDives(renumberedDives); + MainWindow::instance()->undoStack->push(undoCommand); + + MainWindow::instance()->dive_list()->fixMessyQtModelBehaviour(); + mark_divelist_changed(true); + MainWindow::instance()->dive_list()->restoreSelection(); + } +} + +RenumberDialog::RenumberDialog(QWidget *parent) : QDialog(parent), selectedOnly(false) +{ + ui.setupUi(this); + connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); +} + +SetpointDialog *SetpointDialog::instance() +{ + static SetpointDialog *self = new SetpointDialog(MainWindow::instance()); + return self; +} + +void SetpointDialog::setpointData(struct divecomputer *divecomputer, int second) +{ + dc = divecomputer; + time = second < 0 ? 0 : second; +} + +void SetpointDialog::buttonClicked(QAbstractButton *button) +{ + if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole && dc) + add_event(dc, time, SAMPLE_EVENT_PO2, 0, (int)(1000.0 * ui.spinbox->value()), "SP change"); + mark_divelist_changed(true); + MainWindow::instance()->graphics()->replot(); +} + +SetpointDialog::SetpointDialog(QWidget *parent) : + QDialog(parent), + dc(0) +{ + ui.setupUi(this); + connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); +} + +ShiftTimesDialog *ShiftTimesDialog::instance() +{ + static ShiftTimesDialog *self = new ShiftTimesDialog(MainWindow::instance()); + return self; +} + +void ShiftTimesDialog::buttonClicked(QAbstractButton *button) +{ + int amount; + + if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { + amount = ui.timeEdit->time().hour() * 3600 + ui.timeEdit->time().minute() * 60; + if (ui.backwards->isChecked()) + amount *= -1; + if (amount != 0) { + // DANGER, DANGER - this could get our dive_table unsorted... + int i; + struct dive *dive; + QList<int> affectedDives; + for_each_dive (i, dive) { + if (!dive->selected) + continue; + + affectedDives.append(dive->id); + } + MainWindow::instance()->undoStack->push(new UndoShiftTime(affectedDives, amount)); + sort_table(&dive_table); + mark_divelist_changed(true); + MainWindow::instance()->dive_list()->rememberSelection(); + MainWindow::instance()->refreshDisplay(); + MainWindow::instance()->dive_list()->restoreSelection(); + } + } +} + +void ShiftTimesDialog::showEvent(QShowEvent *event) +{ + ui.timeEdit->setTime(QTime(0, 0, 0, 0)); + when = get_times(); //get time of first selected dive + ui.currentTime->setText(get_dive_date_string(when)); + ui.shiftedTime->setText(get_dive_date_string(when)); +} + +void ShiftTimesDialog::changeTime() +{ + int amount; + + amount = ui.timeEdit->time().hour() * 3600 + ui.timeEdit->time().minute() * 60; + if (ui.backwards->isChecked()) + amount *= -1; + + ui.shiftedTime->setText(get_dive_date_string(amount + when)); +} + +ShiftTimesDialog::ShiftTimesDialog(QWidget *parent) : + QDialog(parent), + when(0) +{ + ui.setupUi(this); + connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); + connect(ui.timeEdit, SIGNAL(timeChanged(const QTime)), this, SLOT(changeTime())); + connect(ui.backwards, SIGNAL(toggled(bool)), this, SLOT(changeTime())); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); +} + +void ShiftImageTimesDialog::buttonClicked(QAbstractButton *button) +{ + if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { + m_amount = ui.timeEdit->time().hour() * 3600 + ui.timeEdit->time().minute() * 60; + if (ui.backwards->isChecked()) + m_amount *= -1; + } +} + +void ShiftImageTimesDialog::syncCameraClicked() +{ + QPixmap picture; + QDateTime dcDateTime = QDateTime(); + QStringList fileNames = QFileDialog::getOpenFileNames(this, + tr("Open image file"), + DiveListView::lastUsedImageDir(), + tr("Image files (*.jpg *.jpeg *.pnm *.tif *.tiff)")); + if (fileNames.isEmpty()) + return; + + picture.load(fileNames.at(0)); + ui.displayDC->setEnabled(true); + QGraphicsScene *scene = new QGraphicsScene(this); + + scene->addPixmap(picture.scaled(ui.DCImage->size())); + ui.DCImage->setScene(scene); + + dcImageEpoch = picture_get_timestamp(fileNames.at(0).toUtf8().data()); + dcDateTime.setTime_t(dcImageEpoch - gettimezoneoffset(displayed_dive.when)); + ui.dcTime->setDateTime(dcDateTime); + connect(ui.dcTime, SIGNAL(dateTimeChanged(const QDateTime &)), this, SLOT(dcDateTimeChanged(const QDateTime &))); +} + +void ShiftImageTimesDialog::dcDateTimeChanged(const QDateTime &newDateTime) +{ + QDateTime newtime(newDateTime); + if (!dcImageEpoch) + return; + newtime.setTimeSpec(Qt::UTC); + setOffset(newtime.toTime_t() - dcImageEpoch); +} + +void ShiftImageTimesDialog::matchAllImagesToggled(bool state) +{ + matchAllImages = state; +} + +bool ShiftImageTimesDialog::matchAll() +{ + return matchAllImages; +} + +ShiftImageTimesDialog::ShiftImageTimesDialog(QWidget *parent, QStringList fileNames) : + QDialog(parent), + fileNames(fileNames), + m_amount(0), + matchAllImages(false) +{ + ui.setupUi(this); + connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); + connect(ui.syncCamera, SIGNAL(clicked()), this, SLOT(syncCameraClicked())); + connect(ui.timeEdit, SIGNAL(timeChanged(const QTime &)), this, SLOT(timeEditChanged(const QTime &))); + connect(ui.matchAllImages, SIGNAL(toggled(bool)), this, SLOT(matchAllImagesToggled(bool))); + dcImageEpoch = (time_t)0; +} + +time_t ShiftImageTimesDialog::amount() const +{ + return m_amount; +} + +void ShiftImageTimesDialog::setOffset(time_t offset) +{ + if (offset >= 0) { + ui.forward->setChecked(true); + } else { + ui.backwards->setChecked(true); + offset *= -1; + } + ui.timeEdit->setTime(QTime(offset / 3600, (offset % 3600) / 60, offset % 60)); +} + +void ShiftImageTimesDialog::updateInvalid() +{ + timestamp_t timestamp; + QDateTime time; + bool allValid = true; + ui.warningLabel->hide(); + ui.invalidLabel->hide(); + time.setTime_t(displayed_dive.when - gettimezoneoffset(displayed_dive.when)); + ui.invalidLabel->setText("Dive:" + time.toString() + "\n"); + + Q_FOREACH (const QString &fileName, fileNames) { + if (picture_check_valid(fileName.toUtf8().data(), m_amount)) + continue; + + // We've found invalid image + timestamp = picture_get_timestamp(fileName.toUtf8().data()); + time.setTime_t(timestamp + m_amount - gettimezoneoffset(displayed_dive.when)); + ui.invalidLabel->setText(ui.invalidLabel->text() + fileName + " " + time.toString() + "\n"); + allValid = false; + } + + if (!allValid){ + ui.warningLabel->show(); + ui.invalidLabel->show(); + } +} + +void ShiftImageTimesDialog::timeEditChanged(const QTime &time) +{ + m_amount = time.hour() * 3600 + time.minute() * 60; + if (ui.backwards->isChecked()) + m_amount *= -1; + updateInvalid(); +} + +URLDialog::URLDialog(QWidget *parent) : QDialog(parent) +{ + ui.setupUi(this); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); +} + +QString URLDialog::url() const +{ + return ui.urlField->toPlainText(); +} + +bool isGnome3Session() +{ +#if defined(QT_OS_WIW) || defined(QT_OS_MAC) + return false; +#else + if (qApp->style()->objectName() != "gtk+") + return false; + QProcess p; + p.start("pidof", QStringList() << "gnome-shell"); + p.waitForFinished(-1); + QString p_stdout = p.readAllStandardOutput(); + return !p_stdout.isEmpty(); +#endif +} + +DateWidget::DateWidget(QWidget *parent) : QWidget(parent), + calendarWidget(new QCalendarWidget()) +{ + setDate(QDate::currentDate()); + setMinimumSize(QSize(80, 64)); + setFocusPolicy(Qt::StrongFocus); + calendarWidget->setWindowFlags(Qt::FramelessWindowHint); + calendarWidget->setFirstDayOfWeek(getLocale().firstDayOfWeek()); + calendarWidget->setVerticalHeaderFormat(QCalendarWidget::NoVerticalHeader); + + connect(calendarWidget, SIGNAL(activated(QDate)), calendarWidget, SLOT(hide())); + connect(calendarWidget, SIGNAL(clicked(QDate)), calendarWidget, SLOT(hide())); + connect(calendarWidget, SIGNAL(activated(QDate)), this, SLOT(setDate(QDate))); + connect(calendarWidget, SIGNAL(clicked(QDate)), this, SLOT(setDate(QDate))); + calendarWidget->installEventFilter(this); +} + +bool DateWidget::eventFilter(QObject *object, QEvent *event) +{ + if (event->type() == QEvent::FocusOut) { + calendarWidget->hide(); + return true; + } + if (event->type() == QEvent::KeyPress) { + QKeyEvent *ev = static_cast<QKeyEvent *>(event); + if (ev->key() == Qt::Key_Escape) { + calendarWidget->hide(); + } + } + return QObject::eventFilter(object, event); +} + + +void DateWidget::setDate(const QDate &date) +{ + mDate = date; + update(); + emit dateChanged(mDate); +} + +QDate DateWidget::date() const +{ + return mDate; +} + +void DateWidget::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::EnabledChange) { + update(); + } +} + +#define DATEWIDGETWIDTH 80 +void DateWidget::paintEvent(QPaintEvent *event) +{ + static QPixmap pix = QPixmap(":/calendar").scaled(DATEWIDGETWIDTH, 64); + + QPainter painter(this); + + painter.drawPixmap(QPoint(0, 0), isEnabled() ? pix : QPixmap::fromImage(grayImage(pix.toImage()))); + + QString month = mDate.toString("MMM"); + QString year = mDate.toString("yyyy"); + QString day = mDate.toString("dd"); + + QFont font = QFont("monospace", 10); + QFontMetrics metrics = QFontMetrics(font); + painter.setFont(font); + painter.setPen(QPen(QBrush(Qt::white), 0)); + painter.setBrush(QBrush(Qt::white)); + painter.drawText(QPoint(6, metrics.height() + 1), month); + painter.drawText(QPoint(DATEWIDGETWIDTH - metrics.width(year) - 6, metrics.height() + 1), year); + + font.setPointSize(14); + metrics = QFontMetrics(font); + painter.setPen(QPen(QBrush(Qt::black), 0)); + painter.setBrush(Qt::black); + painter.setFont(font); + painter.drawText(QPoint(DATEWIDGETWIDTH / 2 - metrics.width(day) / 2, 45), day); + + if (hasFocus()) { + QStyleOptionFocusRect option; + option.initFrom(this); + option.backgroundColor = palette().color(QPalette::Background); + style()->drawPrimitive(QStyle::PE_FrameFocusRect, &option, &painter, this); + } +} + +void DateWidget::mousePressEvent(QMouseEvent *event) +{ + calendarWidget->setSelectedDate(mDate); + calendarWidget->move(event->globalPos()); + calendarWidget->show(); + calendarWidget->raise(); + calendarWidget->setFocus(); +} + +void DateWidget::focusInEvent(QFocusEvent *event) +{ + setFocus(); + QWidget::focusInEvent(event); +} + +void DateWidget::focusOutEvent(QFocusEvent *event) +{ + QWidget::focusOutEvent(event); +} + +void DateWidget::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Return || + event->key() == Qt::Key_Enter || + event->key() == Qt::Key_Space) { + calendarWidget->move(mapToGlobal(QPoint(0, 64))); + calendarWidget->show(); + event->setAccepted(true); + } else { + QWidget::keyPressEvent(event); + } +} + +#define COMPONENT_FROM_UI(_component) what->_component = ui._component->isChecked() +#define UI_FROM_COMPONENT(_component) ui._component->setChecked(what->_component) + +DiveComponentSelection::DiveComponentSelection(QWidget *parent, struct dive *target, struct dive_components *_what) : targetDive(target) +{ + ui.setupUi(this); + what = _what; + UI_FROM_COMPONENT(divesite); + UI_FROM_COMPONENT(divemaster); + UI_FROM_COMPONENT(buddy); + UI_FROM_COMPONENT(rating); + UI_FROM_COMPONENT(visibility); + UI_FROM_COMPONENT(notes); + UI_FROM_COMPONENT(suit); + UI_FROM_COMPONENT(tags); + UI_FROM_COMPONENT(cylinders); + UI_FROM_COMPONENT(weights); + connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); +} + +void DiveComponentSelection::buttonClicked(QAbstractButton *button) +{ + if (ui.buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { + COMPONENT_FROM_UI(divesite); + COMPONENT_FROM_UI(divemaster); + COMPONENT_FROM_UI(buddy); + COMPONENT_FROM_UI(rating); + COMPONENT_FROM_UI(visibility); + COMPONENT_FROM_UI(notes); + COMPONENT_FROM_UI(suit); + COMPONENT_FROM_UI(tags); + COMPONENT_FROM_UI(cylinders); + COMPONENT_FROM_UI(weights); + selective_copy_dive(&displayed_dive, targetDive, *what, true); + } +} + +TagFilter::TagFilter(QWidget *parent) : QWidget(parent) +{ + ui.setupUi(this); + ui.label->setText(tr("Tags: ")); +#if QT_VERSION >= 0x050200 + ui.filterInternalList->setClearButtonEnabled(true); +#endif + QSortFilterProxyModel *filter = new QSortFilterProxyModel(); + filter->setSourceModel(TagFilterModel::instance()); + filter->setFilterCaseSensitivity(Qt::CaseInsensitive); + connect(ui.filterInternalList, SIGNAL(textChanged(QString)), filter, SLOT(setFilterFixedString(QString))); + ui.filterList->setModel(filter); +} + +void TagFilter::showEvent(QShowEvent *event) +{ + MultiFilterSortModel::instance()->addFilterModel(TagFilterModel::instance()); + QWidget::showEvent(event); +} + +void TagFilter::hideEvent(QHideEvent *event) +{ + MultiFilterSortModel::instance()->removeFilterModel(TagFilterModel::instance()); + QWidget::hideEvent(event); +} + +BuddyFilter::BuddyFilter(QWidget *parent) : QWidget(parent) +{ + ui.setupUi(this); + ui.label->setText(tr("Person: ")); + ui.label->setToolTip(tr("Searches for buddies and divemasters")); +#if QT_VERSION >= 0x050200 + ui.filterInternalList->setClearButtonEnabled(true); +#endif + QSortFilterProxyModel *filter = new QSortFilterProxyModel(); + filter->setSourceModel(BuddyFilterModel::instance()); + filter->setFilterCaseSensitivity(Qt::CaseInsensitive); + connect(ui.filterInternalList, SIGNAL(textChanged(QString)), filter, SLOT(setFilterFixedString(QString))); + ui.filterList->setModel(filter); +} + +void BuddyFilter::showEvent(QShowEvent *event) +{ + MultiFilterSortModel::instance()->addFilterModel(BuddyFilterModel::instance()); + QWidget::showEvent(event); +} + +void BuddyFilter::hideEvent(QHideEvent *event) +{ + MultiFilterSortModel::instance()->removeFilterModel(BuddyFilterModel::instance()); + QWidget::hideEvent(event); +} + +LocationFilter::LocationFilter(QWidget *parent) : QWidget(parent) +{ + ui.setupUi(this); + ui.label->setText(tr("Location: ")); +#if QT_VERSION >= 0x050200 + ui.filterInternalList->setClearButtonEnabled(true); +#endif + QSortFilterProxyModel *filter = new QSortFilterProxyModel(); + filter->setSourceModel(LocationFilterModel::instance()); + filter->setFilterCaseSensitivity(Qt::CaseInsensitive); + connect(ui.filterInternalList, SIGNAL(textChanged(QString)), filter, SLOT(setFilterFixedString(QString))); + ui.filterList->setModel(filter); +} + +void LocationFilter::showEvent(QShowEvent *event) +{ + MultiFilterSortModel::instance()->addFilterModel(LocationFilterModel::instance()); + QWidget::showEvent(event); +} + +void LocationFilter::hideEvent(QHideEvent *event) +{ + MultiFilterSortModel::instance()->removeFilterModel(LocationFilterModel::instance()); + QWidget::hideEvent(event); +} + +SuitFilter::SuitFilter(QWidget *parent) : QWidget(parent) +{ + ui.setupUi(this); + ui.label->setText(tr("Suits: ")); +#if QT_VERSION >= 0x050200 + ui.filterInternalList->setClearButtonEnabled(true); +#endif + QSortFilterProxyModel *filter = new QSortFilterProxyModel(); + filter->setSourceModel(SuitsFilterModel::instance()); + filter->setFilterCaseSensitivity(Qt::CaseInsensitive); + connect(ui.filterInternalList, SIGNAL(textChanged(QString)), filter, SLOT(setFilterFixedString(QString))); + ui.filterList->setModel(filter); +} + +void SuitFilter::showEvent(QShowEvent *event) +{ + MultiFilterSortModel::instance()->addFilterModel(SuitsFilterModel::instance()); + QWidget::showEvent(event); +} + +void SuitFilter::hideEvent(QHideEvent *event) +{ + MultiFilterSortModel::instance()->removeFilterModel(SuitsFilterModel::instance()); + QWidget::hideEvent(event); +} + +MultiFilter::MultiFilter(QWidget *parent) : QWidget(parent) +{ + ui.setupUi(this); + + QWidget *expandedWidget = new QWidget(); + QHBoxLayout *l = new QHBoxLayout(); + + TagFilter *tagFilter = new TagFilter(this); + int minimumHeight = tagFilter->ui.filterInternalList->height() + + tagFilter->ui.verticalLayout->spacing() * tagFilter->ui.verticalLayout->count(); + + QListView *dummyList = new QListView(); + QStringListModel *dummy = new QStringListModel(QStringList() << "Dummy Text"); + dummyList->setModel(dummy); + + connect(ui.close, SIGNAL(clicked(bool)), this, SLOT(closeFilter())); + connect(ui.clear, SIGNAL(clicked(bool)), MultiFilterSortModel::instance(), SLOT(clearFilter())); + connect(ui.maximize, SIGNAL(clicked(bool)), this, SLOT(adjustHeight())); + + l->addWidget(tagFilter); + l->addWidget(new BuddyFilter()); + l->addWidget(new LocationFilter()); + l->addWidget(new SuitFilter()); + l->setContentsMargins(0, 0, 0, 0); + l->setSpacing(0); + expandedWidget->setLayout(l); + + ui.scrollArea->setWidget(expandedWidget); + expandedWidget->resize(expandedWidget->width(), minimumHeight + dummyList->sizeHintForRow(0) * 5 ); + ui.scrollArea->setMinimumHeight(expandedWidget->height() + 5); + + connect(MultiFilterSortModel::instance(), SIGNAL(filterFinished()), this, SLOT(filterFinished())); +} + +void MultiFilter::filterFinished() +{ + ui.filterText->setText(tr("Filter shows %1 (of %2) dives").arg(MultiFilterSortModel::instance()->divesDisplayed).arg(dive_table.nr)); +} + +void MultiFilter::adjustHeight() +{ + ui.scrollArea->setVisible(!ui.scrollArea->isVisible()); +} + +void MultiFilter::closeFilter() +{ + MultiFilterSortModel::instance()->clearFilter(); + hide(); +} diff --git a/desktop-widgets/simplewidgets.h b/desktop-widgets/simplewidgets.h new file mode 100644 index 000000000..595c4cd4b --- /dev/null +++ b/desktop-widgets/simplewidgets.h @@ -0,0 +1,237 @@ +#ifndef SIMPLEWIDGETS_H +#define SIMPLEWIDGETS_H + +class MinMaxAvgWidgetPrivate; +class QAbstractButton; +class QNetworkReply; + +#include <QWidget> +#include <QGroupBox> +#include <QDialog> +#include <stdint.h> + +#include "ui_renumber.h" +#include "ui_setpoint.h" +#include "ui_shifttimes.h" +#include "ui_shiftimagetimes.h" +#include "ui_urldialog.h" +#include "ui_divecomponentselection.h" +#include "ui_listfilter.h" +#include "ui_filterwidget.h" +#include "exif.h" +#include <dive.h> + + +class MinMaxAvgWidget : public QWidget { + Q_OBJECT + Q_PROPERTY(double minimum READ minimum WRITE setMinimum) + Q_PROPERTY(double maximum READ maximum WRITE setMaximum) + Q_PROPERTY(double average READ average WRITE setAverage) +public: + MinMaxAvgWidget(QWidget *parent); + ~MinMaxAvgWidget(); + double minimum() const; + double maximum() const; + double average() const; + void setMinimum(double minimum); + void setMaximum(double maximum); + void setAverage(double average); + void setMinimum(const QString &minimum); + void setMaximum(const QString &maximum); + void setAverage(const QString &average); + void overrideMinToolTipText(const QString &newTip); + void overrideAvgToolTipText(const QString &newTip); + void overrideMaxToolTipText(const QString &newTip); + void clear(); + +private: + QScopedPointer<MinMaxAvgWidgetPrivate> d; +}; + +class RenumberDialog : public QDialog { + Q_OBJECT +public: + static RenumberDialog *instance(); + void renumberOnlySelected(bool selected = true); +private +slots: + void buttonClicked(QAbstractButton *button); + +private: + explicit RenumberDialog(QWidget *parent); + Ui::RenumberDialog ui; + bool selectedOnly; +}; + +class SetpointDialog : public QDialog { + Q_OBJECT +public: + static SetpointDialog *instance(); + void setpointData(struct divecomputer *divecomputer, int time); +private +slots: + void buttonClicked(QAbstractButton *button); + +private: + explicit SetpointDialog(QWidget *parent); + Ui::SetpointDialog ui; + struct divecomputer *dc; + int time; +}; + +class ShiftTimesDialog : public QDialog { + Q_OBJECT +public: + static ShiftTimesDialog *instance(); + void showEvent(QShowEvent *event); +private +slots: + void buttonClicked(QAbstractButton *button); + void changeTime(); + +private: + explicit ShiftTimesDialog(QWidget *parent); + int64_t when; + Ui::ShiftTimesDialog ui; +}; + +class ShiftImageTimesDialog : public QDialog { + Q_OBJECT +public: + explicit ShiftImageTimesDialog(QWidget *parent, QStringList fileNames); + time_t amount() const; + void setOffset(time_t offset); + bool matchAll(); +private +slots: + void buttonClicked(QAbstractButton *button); + void syncCameraClicked(); + void dcDateTimeChanged(const QDateTime &); + void timeEditChanged(const QTime &time); + void updateInvalid(); + void matchAllImagesToggled(bool); + +private: + QStringList fileNames; + Ui::ShiftImageTimesDialog ui; + time_t m_amount; + time_t dcImageEpoch; + bool matchAllImages; +}; + +class URLDialog : public QDialog { + Q_OBJECT +public: + explicit URLDialog(QWidget *parent); + QString url() const; +private: + Ui::URLDialog ui; +}; + +class QCalendarWidget; + +class DateWidget : public QWidget { + Q_OBJECT +public: + DateWidget(QWidget *parent = 0); + QDate date() const; +public +slots: + void setDate(const QDate &date); + +protected: + void paintEvent(QPaintEvent *event); + void mousePressEvent(QMouseEvent *event); + void focusInEvent(QFocusEvent *); + void focusOutEvent(QFocusEvent *); + void keyPressEvent(QKeyEvent *); + void changeEvent(QEvent *); + bool eventFilter(QObject *, QEvent *); +signals: + void dateChanged(const QDate &date); + +private: + QDate mDate; + QCalendarWidget *calendarWidget; +}; + +class DiveComponentSelection : public QDialog { + Q_OBJECT +public: + explicit DiveComponentSelection(QWidget *parent, struct dive *target, struct dive_components *_what); +private +slots: + void buttonClicked(QAbstractButton *button); + +private: + Ui::DiveComponentSelectionDialog ui; + struct dive *targetDive; + struct dive_components *what; +}; + +namespace Ui{ + class FilterWidget2; +}; + +class MultiFilter : public QWidget { + Q_OBJECT +public +slots: + void closeFilter(); + void adjustHeight(); + void filterFinished(); + +public: + MultiFilter(QWidget *parent); + Ui::FilterWidget2 ui; +}; + +class TagFilter : public QWidget { + Q_OBJECT +public: + TagFilter(QWidget *parent = 0); + virtual void showEvent(QShowEvent *); + virtual void hideEvent(QHideEvent *); + +private: + Ui::FilterWidget ui; + friend class MultiFilter; +}; + +class BuddyFilter : public QWidget { + Q_OBJECT +public: + BuddyFilter(QWidget *parent = 0); + virtual void showEvent(QShowEvent *); + virtual void hideEvent(QHideEvent *); + +private: + Ui::FilterWidget ui; +}; + +class SuitFilter : public QWidget { + Q_OBJECT +public: + SuitFilter(QWidget *parent = 0); + virtual void showEvent(QShowEvent *); + virtual void hideEvent(QHideEvent *); + +private: + Ui::FilterWidget ui; +}; + +class LocationFilter : public QWidget { + Q_OBJECT +public: + LocationFilter(QWidget *parent = 0); + virtual void showEvent(QShowEvent *); + virtual void hideEvent(QHideEvent *); + +private: + Ui::FilterWidget ui; +}; + +bool isGnome3Session(); +QImage grayImage(const QImage &coloredImg); + +#endif // SIMPLEWIDGETS_H diff --git a/desktop-widgets/socialnetworks.cpp b/desktop-widgets/socialnetworks.cpp new file mode 100644 index 000000000..6e191267a --- /dev/null +++ b/desktop-widgets/socialnetworks.cpp @@ -0,0 +1,326 @@ +#include "socialnetworks.h" + +#include <QJsonDocument> +#include <QJsonArray> +#include <QJsonObject> +#include <QNetworkReply> +#include <QNetworkRequest> +#include <QNetworkAccessManager> +#include <QUrlQuery> +#include <QEventLoop> +#include <QHttpMultiPart> +#include <QSettings> +#include <QFile> +#include <QBuffer> +#include <QDebug> +#include <QMessageBox> +#include <QInputDialog> +#include "mainwindow.h" +#include "profile/profilewidget2.h" +#include "pref.h" +#include "helpers.h" +#include "ui_socialnetworksdialog.h" + +#if SAVE_FB_CREDENTIALS +#define GET_TXT(name, field) \ + v = s.value(QString(name)); \ + if (v.isValid()) \ + prefs.field = strdup(v.toString().toUtf8().constData()); \ + else \ + prefs.field = default_prefs.field +#endif + +FacebookManager *FacebookManager::instance() +{ + static FacebookManager *self = new FacebookManager(); + return self; +} + +FacebookManager::FacebookManager(QObject *parent) : QObject(parent) +{ + sync(); +} + +QUrl FacebookManager::connectUrl() { + return QUrl("https://www.facebook.com/dialog/oauth?" + "client_id=427722490709000" + "&redirect_uri=http://www.facebook.com/connect/login_success.html" + "&response_type=token,granted_scopes" + "&display=popup" + "&scope=publish_actions,user_photos" + ); +} + +bool FacebookManager::loggedIn() { + return prefs.facebook.access_token != NULL; +} + +void FacebookManager::sync() +{ +#if SAVE_FB_CREDENTIALS + QSettings s; + s.beginGroup("WebApps"); + s.beginGroup("Facebook"); + + QVariant v; + GET_TXT("ConnectToken", facebook.access_token); + GET_TXT("UserId", facebook.user_id); + GET_TXT("AlbumId", facebook.album_id); +#endif +} + +void FacebookManager::tryLogin(const QUrl& loginResponse) +{ + QString result = loginResponse.toString(); + if (!result.contains("access_token")) + return; + + if (result.contains("denied_scopes=publish_actions") || result.contains("denied_scopes=user_photos")) { + qDebug() << "user did not allow us access" << result; + return; + } + int from = result.indexOf("access_token=") + strlen("access_token="); + int to = result.indexOf("&expires_in"); + QString securityToken = result.mid(from, to-from); + +#if SAVE_FB_CREDENTIALS + QSettings settings; + settings.beginGroup("WebApps"); + settings.beginGroup("Facebook"); + settings.setValue("ConnectToken", securityToken); + sync(); +#else + prefs.facebook.access_token = copy_string(securityToken.toUtf8().data()); +#endif + requestUserId(); + sync(); + emit justLoggedIn(true); +} + +void FacebookManager::logout() +{ +#if SAVE_FB_CREDENTIALS + QSettings settings; + settings.beginGroup("WebApps"); + settings.beginGroup("Facebook"); + settings.remove("ConnectToken"); + settings.remove("UserId"); + settings.remove("AlbumId"); + sync(); +#else + free(prefs.facebook.access_token); + free(prefs.facebook.album_id); + free(prefs.facebook.user_id); + prefs.facebook.access_token = NULL; + prefs.facebook.album_id = NULL; + prefs.facebook.user_id = NULL; +#endif + emit justLoggedOut(true); +} + +void FacebookManager::requestAlbumId() +{ + QUrl albumListUrl("https://graph.facebook.com/me/albums?access_token=" + QString(prefs.facebook.access_token)); + QNetworkAccessManager *manager = new QNetworkAccessManager(); + QNetworkReply *reply = manager->get(QNetworkRequest(albumListUrl)); + + QEventLoop loop; + connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + +#if SAVE_FB_CREDENTIALS + QSettings s; + s.beginGroup("WebApps"); + s.beginGroup("Facebook"); +#endif + + QJsonDocument albumsDoc = QJsonDocument::fromJson(reply->readAll()); + QJsonArray albumObj = albumsDoc.object().value("data").toArray(); + foreach(const QJsonValue &v, albumObj){ + QJsonObject obj = v.toObject(); + if (obj.value("name").toString() == albumName) { +#if SAVE_FB_CREDENTIALS + s.setValue("AlbumId", obj.value("id").toString()); +#else + prefs.facebook.album_id = copy_string(obj.value("id").toString().toUtf8().data()); +#endif + return; + } + } + + QUrlQuery params; + params.addQueryItem("name", albumName ); + params.addQueryItem("description", "Subsurface Album"); + params.addQueryItem("privacy", "{'value': 'SELF'}"); + + QNetworkRequest request(albumListUrl); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/octet-stream"); + reply = manager->post(request, params.query().toLocal8Bit()); + connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + + albumsDoc = QJsonDocument::fromJson(reply->readAll()); + QJsonObject album = albumsDoc.object(); + if (album.contains("id")) { +#if SAVE_FB_CREDENTIALS + s.setValue("AlbumId", album.value("id").toString()); +#else + prefs.facebook.album_id = copy_string(album.value("id").toString().toUtf8().data()); +#endif + sync(); + return; + } +} + +void FacebookManager::requestUserId() +{ + QUrl userIdRequest("https://graph.facebook.com/me?fields=id&access_token=" + QString(prefs.facebook.access_token)); + QNetworkAccessManager *getUserID = new QNetworkAccessManager(); + QNetworkReply *reply = getUserID->get(QNetworkRequest(userIdRequest)); + + QEventLoop loop; + connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + + QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll()); + QJsonObject obj = jsonDoc.object(); + if (obj.keys().contains("id")){ +#if SAVE_FB_CREDENTIALS + QSettings s; + s.beginGroup("WebApps"); + s.beginGroup("Facebook"); + s.setValue("UserId", obj.value("id").toVariant()); +#else + prefs.facebook.user_id = copy_string(obj.value("id").toString().toUtf8().data()); +#endif + return; + } +} + +void FacebookManager::setDesiredAlbumName(const QString& a) +{ + albumName = a; +} + +/* to be changed to export the currently selected dive as shown on the profile. + * Much much easier, and its also good to people do not select all the dives + * and send erroniously *all* of them to facebook. */ +void FacebookManager::sendDive() +{ + SocialNetworkDialog dialog(qApp->activeWindow()); + if (dialog.exec() != QDialog::Accepted) + return; + + setDesiredAlbumName(dialog.album()); + requestAlbumId(); + + ProfileWidget2 *profile = MainWindow::instance()->graphics(); + profile->setToolTipVisibile(false); + QPixmap pix = QPixmap::grabWidget(profile); + profile->setToolTipVisibile(true); + QByteArray bytes; + QBuffer buffer(&bytes); + buffer.open(QIODevice::WriteOnly); + pix.save(&buffer, "PNG"); + QUrl url("https://graph.facebook.com/v2.2/" + QString(prefs.facebook.album_id) + "/photos?" + + "&access_token=" + QString(prefs.facebook.access_token) + + "&source=image" + + "&message=" + dialog.text().replace(""", "%22")); + + QNetworkAccessManager *am = new QNetworkAccessManager(this); + QNetworkRequest request(url); + + QString bound="margin"; + + //according to rfc 1867 we need to put this string here: + QByteArray data(QString("--" + bound + "\r\n").toLocal8Bit()); + data.append("Content-Disposition: form-data; name=\"action\"\r\n\r\n"); + data.append("https://graph.facebook.com/v2.2/\r\n"); + data.append("--" + bound + "\r\n"); //according to rfc 1867 + data.append("Content-Disposition: form-data; name=\"uploaded\"; filename=\"" + QString::number(qrand()) + ".png\"\r\n"); //name of the input is "uploaded" in my form, next one is a file name. + data.append("Content-Type: image/jpeg\r\n\r\n"); //data type + data.append(bytes); //let's read the file + data.append("\r\n"); + data.append("--" + bound + "--\r\n"); //closing boundary according to rfc 1867 + + request.setRawHeader(QString("Content-Type").toLocal8Bit(),QString("multipart/form-data; boundary=" + bound).toLocal8Bit()); + request.setRawHeader(QString("Content-Length").toLocal8Bit(), QString::number(data.length()).toLocal8Bit()); + QNetworkReply *reply = am->post(request,data); + + QEventLoop loop; + connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + + QByteArray response = reply->readAll(); + QJsonDocument jsonDoc = QJsonDocument::fromJson(response); + QJsonObject obj = jsonDoc.object(); + if (obj.keys().contains("id")){ + QMessageBox::information(qApp->activeWindow(), + tr("Photo upload sucessfull"), + tr("Your dive profile was updated to Facebook."), + QMessageBox::Ok); + } else { + QMessageBox::information(qApp->activeWindow(), + tr("Photo upload failed"), + tr("Your dive profile was not updated to Facebook, \n " + "please send the following to the developer. \n" + + response), + QMessageBox::Ok); + } +} + +SocialNetworkDialog::SocialNetworkDialog(QWidget *parent) : + QDialog(parent), + ui( new Ui::SocialnetworksDialog()) +{ + ui->setupUi(this); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + connect(ui->date, SIGNAL(clicked()), this, SLOT(selectionChanged())); + connect(ui->duration, SIGNAL(clicked()), this, SLOT(selectionChanged())); + connect(ui->Buddy, SIGNAL(clicked()), this, SLOT(selectionChanged())); + connect(ui->Divemaster, SIGNAL(clicked()), this, SLOT(selectionChanged())); + connect(ui->Location, SIGNAL(clicked()), this, SLOT(selectionChanged())); + connect(ui->Notes, SIGNAL(clicked()), this, SLOT(selectionChanged())); + connect(ui->album, SIGNAL(textChanged(QString)), this, SLOT(albumChanged())); +} + +void SocialNetworkDialog::albumChanged() +{ + QAbstractButton *button = ui->buttonBox->button(QDialogButtonBox::Ok); + button->setEnabled(!ui->album->text().isEmpty()); +} + +void SocialNetworkDialog::selectionChanged() +{ + struct dive *d = current_dive; + QString fullText; + if (ui->date->isChecked()) { + fullText += tr("Dive date: %1 \n").arg(get_short_dive_date_string(d->when)); + } + if (ui->duration->isChecked()) { + fullText += tr("Duration: %1 \n").arg(get_dive_duration_string(d->duration.seconds, + tr("h:", "abbreviation for hours plus separator"), + tr("min", "abbreviation for minutes"))); + } + if (ui->Location->isChecked()) { + fullText += tr("Dive location: %1 \n").arg(get_dive_location(d)); + } + if (ui->Buddy->isChecked()) { + fullText += tr("Buddy: %1 \n").arg(d->buddy); + } + if (ui->Divemaster->isChecked()) { + fullText += tr("Divemaster: %1 \n").arg(d->divemaster); + } + if (ui->Notes->isChecked()) { + fullText += tr("\n%1").arg(d->notes); + } + ui->text->setPlainText(fullText); +} + +QString SocialNetworkDialog::text() const { + return ui->text->toPlainText().toHtmlEscaped(); +} + +QString SocialNetworkDialog::album() const { + return ui->album->text().toHtmlEscaped(); +} diff --git a/desktop-widgets/socialnetworks.h b/desktop-widgets/socialnetworks.h new file mode 100644 index 000000000..2f63915ca --- /dev/null +++ b/desktop-widgets/socialnetworks.h @@ -0,0 +1,49 @@ +#ifndef FACEBOOKMANAGER_H +#define FACEBOOKMANAGER_H + +#include <QObject> +#include <QUrl> +#include <QDialog> + +class FacebookManager : public QObject +{ + Q_OBJECT +public: + static FacebookManager *instance(); + void requestAlbumId(); + void requestUserId(); + void sync(); + QUrl connectUrl(); + bool loggedIn(); +signals: + void justLoggedIn(bool triggererd); + void justLoggedOut(bool triggered); + +public slots: + void tryLogin(const QUrl& loginResponse); + void logout(); + void setDesiredAlbumName(const QString& albumName); + void sendDive(); + +private: + explicit FacebookManager(QObject *parent = 0); + QString albumName; +}; + +namespace Ui { + class SocialnetworksDialog; +} + +class SocialNetworkDialog : public QDialog { + Q_OBJECT +public: + SocialNetworkDialog(QWidget *parent); + QString text() const; + QString album() const; +public slots: + void selectionChanged(); + void albumChanged(); +private: + Ui::SocialnetworksDialog *ui; +}; +#endif // FACEBOOKMANAGER_H diff --git a/desktop-widgets/socialnetworksdialog.ui b/desktop-widgets/socialnetworksdialog.ui new file mode 100644 index 000000000..e8953d1c7 --- /dev/null +++ b/desktop-widgets/socialnetworksdialog.ui @@ -0,0 +1,184 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SocialnetworksDialog</class> + <widget class="QDialog" name="SocialnetworksDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>475</width> + <height>416</height> + </rect> + </property> + <property name="windowTitle"> + <string>Dialog</string> + </property> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="geometry"> + <rect> + <x>290</x> + <y>380</y> + <width>166</width> + <height>22</height> + </rect> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + <widget class="QWidget" name="layoutWidget"> + <property name="geometry"> + <rect> + <x>10</x> + <y>15</y> + <width>451</width> + <height>361</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout" rowstretch="0,1,0,0,0,0,0,0,0,0,0"> + <property name="leftMargin"> + <number>1</number> + </property> + <property name="topMargin"> + <number>1</number> + </property> + <property name="rightMargin"> + <number>1</number> + </property> + <property name="bottomMargin"> + <number>1</number> + </property> + <item row="1" column="0" colspan="2"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>The text to the right will be posted as the description with your profile picture to Facebook. The album name is required (the profile picture will be posted to that album).</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Album</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLineEdit" name="album"> + <property name="toolTip"> + <string>The profile picture will be posted in this album (required)</string> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Include</string> + </property> + </widget> + </item> + <item row="5" column="0"> + <widget class="QCheckBox" name="date"> + <property name="text"> + <string>Date and time</string> + </property> + </widget> + </item> + <item row="6" column="0"> + <widget class="QCheckBox" name="duration"> + <property name="text"> + <string>Duration</string> + </property> + </widget> + </item> + <item row="7" column="0"> + <widget class="QCheckBox" name="Location"> + <property name="text"> + <string>Location</string> + </property> + </widget> + </item> + <item row="8" column="0"> + <widget class="QCheckBox" name="Divemaster"> + <property name="text"> + <string>Divemaster</string> + </property> + </widget> + </item> + <item row="9" column="0"> + <widget class="QCheckBox" name="Buddy"> + <property name="text"> + <string>Buddy</string> + </property> + </widget> + </item> + <item row="10" column="0"> + <widget class="QCheckBox" name="Notes"> + <property name="text"> + <string>Notes</string> + </property> + </widget> + </item> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="label"> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>Facebook post preview</string> + </property> + </widget> + </item> + <item row="2" column="1" rowspan="9"> + <widget class="QPlainTextEdit" name="text"/> + </item> + </layout> + </widget> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>SocialnetworksDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>SocialnetworksDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/desktop-widgets/starwidget.cpp b/desktop-widgets/starwidget.cpp new file mode 100644 index 000000000..d959ed3b9 --- /dev/null +++ b/desktop-widgets/starwidget.cpp @@ -0,0 +1,164 @@ +#include "starwidget.h" +#include "metrics.h" +#include <QSvgRenderer> +#include <QMouseEvent> +#include "simplewidgets.h" + +QImage StarWidget::activeStar; +QImage StarWidget::inactiveStar; + +const QImage& StarWidget::starActive() +{ + return activeStar; +} + +const QImage& StarWidget::starInactive() +{ + return inactiveStar; +} + +QImage focusedImage(const QImage& coloredImg) +{ + QImage img = coloredImg; + for (int i = 0; i < img.width(); ++i) { + for (int j = 0; j < img.height(); ++j) { + QRgb rgb = img.pixel(i, j); + if (!rgb) + continue; + + QColor c(rgb); + c = c.dark(); + img.setPixel(i, j, c.rgb()); + } + } + + return img; +} + + +int StarWidget::currentStars() const +{ + return current; +} + +void StarWidget::mouseReleaseEvent(QMouseEvent *event) +{ + if (readOnly) { + return; + } + + int starClicked = event->pos().x() / defaultIconMetrics().sz_small + 1; + if (starClicked > TOTALSTARS) + starClicked = TOTALSTARS; + + if (current == starClicked) + current -= 1; + else + current = starClicked; + + Q_EMIT valueChanged(current); + update(); +} + +void StarWidget::paintEvent(QPaintEvent *event) +{ + QPainter p(this); + QImage star = hasFocus() ? focusedImage(starActive()) : starActive(); + QPixmap selected = QPixmap::fromImage(star); + QPixmap inactive = QPixmap::fromImage(starInactive()); + const IconMetrics& metrics = defaultIconMetrics(); + + + for (int i = 0; i < current; i++) + p.drawPixmap(i * metrics.sz_small + metrics.spacing, 0, selected); + + for (int i = current; i < TOTALSTARS; i++) + p.drawPixmap(i * metrics.sz_small + metrics.spacing, 0, inactive); + + if (hasFocus()) { + QStyleOptionFocusRect option; + option.initFrom(this); + option.backgroundColor = palette().color(QPalette::Background); + style()->drawPrimitive(QStyle::PE_FrameFocusRect, &option, &p, this); + } +} + +void StarWidget::setCurrentStars(int value) +{ + current = value; + update(); + Q_EMIT valueChanged(current); +} + +StarWidget::StarWidget(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f), + current(0), + readOnly(false) +{ + int dim = defaultIconMetrics().sz_small; + + if (activeStar.isNull()) { + QSvgRenderer render(QString(":star")); + QPixmap renderedStar(dim, dim); + + renderedStar.fill(Qt::transparent); + QPainter painter(&renderedStar); + + render.render(&painter, QRectF(0, 0, dim, dim)); + activeStar = renderedStar.toImage(); + } + if (inactiveStar.isNull()) { + inactiveStar = grayImage(activeStar); + } + setFocusPolicy(Qt::StrongFocus); +} + +QImage grayImage(const QImage& coloredImg) +{ + QImage img = coloredImg; + for (int i = 0; i < img.width(); ++i) { + for (int j = 0; j < img.height(); ++j) { + QRgb rgb = img.pixel(i, j); + if (!rgb) + continue; + + QColor c(rgb); + int gray = 204 + (c.red() + c.green() + c.blue()) / 15; + img.setPixel(i, j, qRgb(gray, gray, gray)); + } + } + + return img; +} + +QSize StarWidget::sizeHint() const +{ + const IconMetrics& metrics = defaultIconMetrics(); + return QSize(metrics.sz_small * TOTALSTARS + metrics.spacing * (TOTALSTARS - 1), metrics.sz_small); +} + +void StarWidget::setReadOnly(bool r) +{ + readOnly = r; +} + +void StarWidget::focusInEvent(QFocusEvent *event) +{ + setFocus(); + QWidget::focusInEvent(event); +} + +void StarWidget::focusOutEvent(QFocusEvent *event) +{ + QWidget::focusOutEvent(event); +} + +void StarWidget::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Up || event->key() == Qt::Key_Right) { + if (currentStars() < TOTALSTARS) + setCurrentStars(currentStars() + 1); + } else if (event->key() == Qt::Key_Down || event->key() == Qt::Key_Left) { + if (currentStars() > 0) + setCurrentStars(currentStars() - 1); + } +} diff --git a/desktop-widgets/starwidget.h b/desktop-widgets/starwidget.h new file mode 100644 index 000000000..989aa527d --- /dev/null +++ b/desktop-widgets/starwidget.h @@ -0,0 +1,44 @@ +#ifndef STARWIDGET_H +#define STARWIDGET_H + +#include <QWidget> + +enum StarConfig { + TOTALSTARS = 5 +}; + +class StarWidget : public QWidget { + Q_OBJECT +public: + explicit StarWidget(QWidget *parent = 0, Qt::WindowFlags f = 0); + int currentStars() const; + + /*reimp*/ QSize sizeHint() const; + + static const QImage& starActive(); + static const QImage& starInactive(); + +signals: + void valueChanged(int stars); + +public +slots: + void setCurrentStars(int value); + void setReadOnly(bool readOnly); + +protected: + /*reimp*/ void mouseReleaseEvent(QMouseEvent *); + /*reimp*/ void paintEvent(QPaintEvent *); + /*reimp*/ void focusInEvent(QFocusEvent *); + /*reimp*/ void focusOutEvent(QFocusEvent *); + /*reimp*/ void keyPressEvent(QKeyEvent *); + +private: + int current; + bool readOnly; + + static QImage activeStar; + static QImage inactiveStar; +}; + +#endif // STARWIDGET_H diff --git a/desktop-widgets/statistics/monthstatistics.cpp b/desktop-widgets/statistics/monthstatistics.cpp new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/desktop-widgets/statistics/monthstatistics.cpp diff --git a/desktop-widgets/statistics/monthstatistics.h b/desktop-widgets/statistics/monthstatistics.h new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/desktop-widgets/statistics/monthstatistics.h diff --git a/desktop-widgets/statistics/statisticsbar.cpp b/desktop-widgets/statistics/statisticsbar.cpp new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/desktop-widgets/statistics/statisticsbar.cpp diff --git a/desktop-widgets/statistics/statisticsbar.h b/desktop-widgets/statistics/statisticsbar.h new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/desktop-widgets/statistics/statisticsbar.h diff --git a/desktop-widgets/statistics/statisticswidget.cpp b/desktop-widgets/statistics/statisticswidget.cpp new file mode 100644 index 000000000..3e91fa317 --- /dev/null +++ b/desktop-widgets/statistics/statisticswidget.cpp @@ -0,0 +1,41 @@ +#include "statisticswidget.h" +#include "yearlystatisticsmodel.h" +#include <QModelIndex> + +YearlyStatisticsWidget::YearlyStatisticsWidget(QWidget *parent): + QGraphicsView(parent), + m_model(NULL) +{ +} + +void YearlyStatisticsWidget::setModel(YearlyStatisticsModel *m) +{ + m_model = m; + connect(m, SIGNAL(dataChanged(QModelIndex,QModelIndex)), + this, SLOT(modelDataChanged(QModelIndex,QModelIndex))); + connect(m, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), + scene(), SLOT(clear())); + connect(m, SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(modelRowsInserted(QModelIndex,int,int))); + + modelRowsInserted(QModelIndex(),0,m_model->rowCount()-1); +} + +void YearlyStatisticsWidget::modelRowsInserted(const QModelIndex &index, int first, int last) +{ + // stub +} + +void YearlyStatisticsWidget::modelDataChanged(const QModelIndex &topLeft, const QModelIndex& bottomRight) +{ + Q_UNUSED(topLeft); + Q_UNUSED(bottomRight); + scene()->clear(); + modelRowsInserted(QModelIndex(),0,m_model->rowCount()-1); +} + +void YearlyStatisticsWidget::resizeEvent(QResizeEvent *event) +{ + QGraphicsView::resizeEvent(event); + fitInView(sceneRect(), Qt::IgnoreAspectRatio); +} diff --git a/desktop-widgets/statistics/statisticswidget.h b/desktop-widgets/statistics/statisticswidget.h new file mode 100644 index 000000000..ae988292d --- /dev/null +++ b/desktop-widgets/statistics/statisticswidget.h @@ -0,0 +1,23 @@ +#ifndef YEARLYSTATISTICSWIDGET_H +#define YEARLYSTATISTICSWIDGET_H + +#include <QGraphicsView> + +class YearlyStatisticsModel; +class QModelIndex; + +class YearlyStatisticsWidget : public QGraphicsView { + Q_OBJECT +public: + YearlyStatisticsWidget(QWidget *parent = 0); + void setModel(YearlyStatisticsModel *m); +protected: + virtual void resizeEvent(QResizeEvent *event); +public slots: + void modelRowsInserted(const QModelIndex& index, int first, int last); + void modelDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); +private: + YearlyStatisticsModel *m_model; +}; + +#endif
\ No newline at end of file diff --git a/desktop-widgets/statistics/yearstatistics.cpp b/desktop-widgets/statistics/yearstatistics.cpp new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/desktop-widgets/statistics/yearstatistics.cpp diff --git a/desktop-widgets/statistics/yearstatistics.h b/desktop-widgets/statistics/yearstatistics.h new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/desktop-widgets/statistics/yearstatistics.h diff --git a/desktop-widgets/subsurfacewebservices.cpp b/desktop-widgets/subsurfacewebservices.cpp new file mode 100644 index 000000000..ee079cc48 --- /dev/null +++ b/desktop-widgets/subsurfacewebservices.cpp @@ -0,0 +1,1121 @@ +#include "subsurfacewebservices.h" +#include "helpers.h" +#include "webservice.h" +#include "mainwindow.h" +#include "usersurvey.h" +#include "divelist.h" +#include "globe.h" +#include "maintab.h" +#include "display.h" +#include "membuffer.h" +#include <errno.h> + +#include <QDir> +#include <QHttpMultiPart> +#include <QMessageBox> +#include <QSettings> +#include <QXmlStreamReader> +#include <qdesktopservices.h> +#include <QShortcut> +#include <QDebug> + +#ifdef Q_OS_UNIX +#include <unistd.h> // for dup(2) +#endif + +#include <QUrlQuery> + +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + +struct dive_table gps_location_table; + +// we don't overwrite any existing GPS info in the dive +// so get the dive site and if there is none or there is one without GPS fix, add it +static void copy_gps_location(struct dive *from, struct dive *to) +{ + struct dive_site *ds = get_dive_site_for_dive(to); + if (!ds || !dive_site_has_gps_location(ds)) { + struct dive_site *gds = get_dive_site_for_dive(from); + if (!ds) { + // simply link to the one created for the fake dive + to->dive_site_uuid = gds->uuid; + } else { + ds->latitude = gds->latitude; + ds->longitude = gds->longitude; + if (same_string(ds->name, "")) + ds->name = copy_string(gds->name); + } + } +} + +#define SAME_GROUP 6 * 3600 // six hours +//TODO: C Code. static functions are not good if we plan to have a test for them. +static bool merge_locations_into_dives(void) +{ + int i, j, tracer=0, changed=0; + struct dive *gpsfix, *nextgpsfix, *dive; + + sort_table(&gps_location_table); + + for_each_dive (i, dive) { + if (!dive_has_gps_location(dive)) { + for (j = tracer; (gpsfix = get_dive_from_table(j, &gps_location_table)) !=NULL; j++) { + if (time_during_dive_with_offset(dive, gpsfix->when, SAME_GROUP)) { + if (verbose) + qDebug() << "processing gpsfix @" << get_dive_date_string(gpsfix->when) << + "which is withing six hours of dive from" << + get_dive_date_string(dive->when) << "until" << + get_dive_date_string(dive->when + dive->duration.seconds); + /* + * If position is fixed during dive. This is the good one. + * Asign and mark position, and end gps_location loop + */ + if (time_during_dive_with_offset(dive, gpsfix->when, 0)) { + if (verbose) + qDebug() << "gpsfix is during the dive, pick that one"; + copy_gps_location(gpsfix, dive); + changed++; + tracer = j; + break; + } else { + /* + * If it is not, check if there are more position fixes in SAME_GROUP range + */ + if ((nextgpsfix = get_dive_from_table(j + 1, &gps_location_table)) && + time_during_dive_with_offset(dive, nextgpsfix->when, SAME_GROUP)) { + if (verbose) + qDebug() << "look at the next gps fix @" << get_dive_date_string(nextgpsfix->when); + /* first let's test if this one is during the dive */ + if (time_during_dive_with_offset(dive, nextgpsfix->when, 0)) { + if (verbose) + qDebug() << "which is during the dive, pick that one"; + copy_gps_location(nextgpsfix, dive); + changed++; + tracer = j + 1; + break; + } + /* we know the gps fixes are sorted; if they are both before the dive, ignore the first, + * if theay are both after the dive, take the first, + * if the first is before and the second is after, take the closer one */ + if (nextgpsfix->when < dive->when) { + if (verbose) + qDebug() << "which is closer to the start of the dive, do continue with that"; + continue; + } else if (gpsfix->when > dive->when + dive->duration.seconds) { + if (verbose) + qDebug() << "which is even later after the end of the dive, so pick the previous one"; + copy_gps_location(gpsfix, dive); + changed++; + tracer = j; + break; + } else { + /* ok, gpsfix is before, nextgpsfix is after */ + if (dive->when - gpsfix->when <= nextgpsfix->when - (dive->when + dive->duration.seconds)) { + if (verbose) + qDebug() << "pick the one before as it's closer to the start"; + copy_gps_location(gpsfix, dive); + changed++; + tracer = j; + break; + } else { + if (verbose) + qDebug() << "pick the one after as it's closer to the start"; + copy_gps_location(nextgpsfix, dive); + changed++; + tracer = j + 1; + break; + } + } + /* + * If no more positions in range, the actual is the one. Asign, mark and end loop. + */ + } else { + if (verbose) + qDebug() << "which seems to be the best one for this dive, so pick it"; + copy_gps_location(gpsfix, dive); + changed++; + tracer = j; + break; + } + } + } else { + /* If position is out of SAME_GROUP range and in the future, mark position for + * next dive iteration and end the gps_location loop + */ + if (gpsfix->when >= dive->when + dive->duration.seconds + SAME_GROUP) { + tracer = j; + break; + } + } + } + } + } + return changed > 0; +} + +// TODO: This looks like should be ported to C code. or a big part of it. +bool DivelogsDeWebServices::prepare_dives_for_divelogs(const QString &tempfile, const bool selected) +{ + static const char errPrefix[] = "divelog.de-upload:"; + if (!amount_selected) { + report_error(tr("no dives were selected").toUtf8()); + return false; + } + + xsltStylesheetPtr xslt = NULL; + struct zip *zip; + + xslt = get_stylesheet("divelogs-export.xslt"); + if (!xslt) { + qDebug() << errPrefix << "missing stylesheet"; + report_error(tr("stylesheet to export to divelogs.de is not found").toUtf8()); + return false; + } + + + int error_code; + zip = zip_open(QFile::encodeName(QDir::toNativeSeparators(tempfile)), ZIP_CREATE, &error_code); + if (!zip) { + char buffer[1024]; + zip_error_to_str(buffer, sizeof buffer, error_code, errno); + report_error(tr("failed to create zip file for upload: %s").toUtf8(), buffer); + return false; + } + + /* walk the dive list in chronological order */ + int i; + struct dive *dive; + for_each_dive (i, dive) { + FILE *f; + char filename[PATH_MAX]; + int streamsize; + const char *membuf; + xmlDoc *transformed; + struct zip_source *s; + struct membuffer mb = { 0 }; + + /* + * Get the i'th dive in XML format so we can process it. + * We need to save to a file before we can reload it back into memory... + */ + if (selected && !dive->selected) + continue; + /* make sure the buffer is empty and add the dive */ + mb.len = 0; + + struct dive_site *ds = get_dive_site_by_uuid(dive->dive_site_uuid); + + if (ds) { + put_format(&mb, "<divelog><divesites><site uuid='%8x' name='", dive->dive_site_uuid); + put_quoted(&mb, ds->name, 1, 0); + put_format(&mb, "'"); + if (ds->latitude.udeg || ds->longitude.udeg) { + put_degrees(&mb, ds->latitude, " gps='", " "); + put_degrees(&mb, ds->longitude, "", "'"); + } + put_format(&mb, "/>\n</divesites>\n"); + } + + save_one_dive_to_mb(&mb, dive); + + if (ds) { + put_format(&mb, "</divelog>\n"); + } + membuf = mb_cstring(&mb); + streamsize = strlen(membuf); + /* + * Parse the memory buffer into XML document and + * transform it to divelogs.de format, finally dumping + * the XML into a character buffer. + */ + xmlDoc *doc = xmlReadMemory(membuf, streamsize, "divelog", NULL, 0); + if (!doc) { + qWarning() << errPrefix << "could not parse back into memory the XML file we've just created!"; + report_error(tr("internal error").toUtf8()); + goto error_close_zip; + } + free((void *)membuf); + + transformed = xsltApplyStylesheet(xslt, doc, NULL); + if (!transformed) { + qWarning() << errPrefix << "XSLT transform failed for dive: " << i; + report_error(tr("Conversion of dive %1 to divelogs.de format failed").arg(i).toUtf8()); + continue; + } + xmlDocDumpMemory(transformed, (xmlChar **)&membuf, &streamsize); + xmlFreeDoc(doc); + xmlFreeDoc(transformed); + + /* + * Save the XML document into a zip file. + */ + snprintf(filename, PATH_MAX, "%d.xml", i + 1); + s = zip_source_buffer(zip, membuf, streamsize, 1); + if (s) { + int64_t ret = zip_add(zip, filename, s); + if (ret == -1) + qDebug() << errPrefix << "failed to include dive:" << i; + } + } + xsltFreeStylesheet(xslt); + if (zip_close(zip)) { + int ze, se; +#if LIBZIP_VERSION_MAJOR >= 1 + zip_error_t *error = zip_get_error(zip); + ze = zip_error_code_zip(error); + se = zip_error_code_system(error); +#else + zip_error_get(zip, &ze, &se); +#endif + report_error(qPrintable(tr("error writing zip file: %s zip error %d system error %d - %s")), + qPrintable(QDir::toNativeSeparators(tempfile)), ze, se, zip_strerror(zip)); + return false; + } + return true; + +error_close_zip: + zip_close(zip); + QFile::remove(tempfile); + xsltFreeStylesheet(xslt); + return false; +} + +WebServices::WebServices(QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f), reply(0) +{ + ui.setupUi(this); + connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *))); + connect(ui.download, SIGNAL(clicked(bool)), this, SLOT(startDownload())); + connect(ui.upload, SIGNAL(clicked(bool)), this, SLOT(startUpload())); + connect(&timeout, SIGNAL(timeout()), this, SLOT(downloadTimedOut())); + ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); + timeout.setSingleShot(true); + defaultApplyText = ui.buttonBox->button(QDialogButtonBox::Apply)->text(); + userAgent = getUserAgent(); +} + +void WebServices::hidePassword() +{ + ui.password->hide(); + ui.passLabel->hide(); +} + +void WebServices::hideUpload() +{ + ui.upload->hide(); + ui.download->show(); +} + +void WebServices::hideDownload() +{ + ui.download->hide(); + ui.upload->show(); +} + +QNetworkAccessManager *WebServices::manager() +{ + static QNetworkAccessManager *manager = new QNetworkAccessManager(qApp); + return manager; +} + +void WebServices::downloadTimedOut() +{ + if (!reply) + return; + + reply->deleteLater(); + reply = NULL; + resetState(); + ui.status->setText(tr("Operation timed out")); +} + +void WebServices::updateProgress(qint64 current, qint64 total) +{ + if (!reply) + return; + if (total == -1) { + total = INT_MAX / 2 - 1; + } + if (total >= INT_MAX / 2) { + // over a gigabyte! + if (total >= Q_INT64_C(1) << 47) { + total >>= 16; + current >>= 16; + } + total >>= 16; + current >>= 16; + } + ui.progressBar->setRange(0, total); + ui.progressBar->setValue(current); + ui.status->setText(tr("Transferring data...")); + + // reset the timer: 30 seconds after we last got any data + timeout.start(); +} + +void WebServices::connectSignalsForDownload(QNetworkReply *reply) +{ + connect(reply, SIGNAL(finished()), this, SLOT(downloadFinished())); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), + this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(reply, SIGNAL(downloadProgress(qint64, qint64)), this, + SLOT(updateProgress(qint64, qint64))); + + timeout.start(30000); // 30s +} + +void WebServices::resetState() +{ + ui.download->setEnabled(true); + ui.upload->setEnabled(true); + ui.userID->setEnabled(true); + ui.password->setEnabled(true); + ui.progressBar->reset(); + ui.progressBar->setRange(0, 1); + ui.status->setText(QString()); + ui.buttonBox->button(QDialogButtonBox::Apply)->setText(defaultApplyText); +} + +// # +// # +// # Subsurface Web Service Implementation. +// # +// # + +SubsurfaceWebServices::SubsurfaceWebServices(QWidget *parent, Qt::WindowFlags f) : WebServices(parent, f) +{ + QSettings s; + if (!prefs.save_userid_local || !*prefs.userid) + ui.userID->setText(s.value("subsurface_webservice_uid").toString().toUpper()); + else + ui.userID->setText(prefs.userid); + hidePassword(); + hideUpload(); + ui.progressBar->setFormat(tr("Enter User ID and click Download")); + ui.progressBar->setRange(0, 1); + ui.progressBar->setValue(-1); + ui.progressBar->setAlignment(Qt::AlignCenter); + ui.saveUidLocal->setChecked(prefs.save_userid_local); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); +} + +void SubsurfaceWebServices::buttonClicked(QAbstractButton *button) +{ + ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); + switch (ui.buttonBox->buttonRole(button)) { + case QDialogButtonBox::ApplyRole: { + int i; + struct dive *d; + struct dive_site *ds; + bool changed = false; + clear_table(&gps_location_table); + QByteArray url = tr("Webservice").toLocal8Bit(); + parse_xml_buffer(url.data(), downloadedData.data(), downloadedData.length(), &gps_location_table, NULL); + // make sure we mark all the dive sites that were created + for (i = 0; i < gps_location_table.nr; i++) { + d = get_dive_from_table(i, &gps_location_table); + ds = get_dive_site_by_uuid(d->dive_site_uuid); + if (ds) + ds->notes = strdup("SubsurfaceWebservice"); + } + /* now merge the data in the gps_location table into the dive_table */ + if (merge_locations_into_dives()) { + changed = true; + mark_divelist_changed(true); + MainWindow::instance()->information()->updateDiveInfo(); + } + + /* store last entered uid in config */ + QSettings s; + QString qDialogUid = ui.userID->text().toUpper(); + bool qSaveUid = ui.saveUidLocal->checkState(); + set_save_userid_local(qSaveUid); + if (qSaveUid) { + QString qSettingUid = s.value("subsurface_webservice_uid").toString(); + QString qFileUid = QString(prefs.userid); + bool s_eq_d = (qSettingUid == qDialogUid); + bool d_eq_f = (qDialogUid == qFileUid); + if (!d_eq_f || s_eq_d) + s.setValue("subsurface_webservice_uid", qDialogUid); + set_userid(qDialogUid.toLocal8Bit().data()); + } else { + s.setValue("subsurface_webservice_uid", qDialogUid); + } + s.sync(); + hide(); + close(); + resetState(); + /* and now clean up and remove all the extra dive sites that were created */ + QSet<uint32_t> usedUuids; + for_each_dive(i, d) { + if (d->dive_site_uuid) + usedUuids.insert(d->dive_site_uuid); + } + for_each_dive_site(i, ds) { + if (!usedUuids.contains(ds->uuid) && same_string(ds->notes, "SubsurfaceWebservice")) { + delete_dive_site(ds->uuid); + i--; // otherwise we skip one site + } + } +#ifndef NO_MARBLE + // finally now that all the extra GPS fixes that weren't used have been deleted + // we can update the globe + if (changed) { + GlobeGPS::instance()->repopulateLabels(); + GlobeGPS::instance()->centerOnDiveSite(get_dive_site_by_uuid(current_dive->dive_site_uuid)); + } +#endif + + } break; + case QDialogButtonBox::RejectRole: + if (reply != NULL && reply->isOpen()) { + reply->abort(); + delete reply; + reply = NULL; + } + resetState(); + break; + case QDialogButtonBox::HelpRole: + QDesktopServices::openUrl(QUrl("http://api.hohndel.org")); + break; + default: + break; + } +} + +void SubsurfaceWebServices::startDownload() +{ + QUrl url("http://api.hohndel.org/api/dive/get/"); + QUrlQuery query; + query.addQueryItem("login", ui.userID->text().toUpper()); + url.setQuery(query); + + QNetworkRequest request; + request.setUrl(url); + request.setRawHeader("Accept", "text/xml"); + request.setRawHeader("User-Agent", userAgent.toUtf8()); + reply = manager()->get(request); + ui.status->setText(tr("Connecting...")); + ui.progressBar->setEnabled(true); + ui.progressBar->setRange(0, 0); // this makes the progressbar do an 'infinite spin' + ui.download->setEnabled(false); + ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); + connectSignalsForDownload(reply); +} + +void SubsurfaceWebServices::downloadFinished() +{ + if (!reply) + return; + + ui.progressBar->setRange(0, 1); + ui.progressBar->setValue(1); + ui.progressBar->setFormat("%p%"); + downloadedData = reply->readAll(); + + ui.download->setEnabled(true); + ui.status->setText(tr("Download finished")); + + uint resultCode = download_dialog_parse_response(downloadedData); + setStatusText(resultCode); + if (resultCode == DD_STATUS_OK) { + ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); + } + reply->deleteLater(); + reply = NULL; +} + +void SubsurfaceWebServices::downloadError(QNetworkReply::NetworkError) +{ + resetState(); + ui.status->setText(tr("Download error: %1").arg(reply->errorString())); + reply->deleteLater(); + reply = NULL; +} + +void SubsurfaceWebServices::setStatusText(int status) +{ + QString text; + switch (status) { + case DD_STATUS_ERROR_CONNECT: + text = tr("Connection error: "); + break; + case DD_STATUS_ERROR_ID: + text = tr("Invalid user identifier!"); + break; + case DD_STATUS_ERROR_PARSE: + text = tr("Cannot parse response!"); + break; + case DD_STATUS_OK: + text = tr("Download successful"); + break; + } + ui.status->setText(text); +} + +//TODO: C-Code. +/* requires that there is a <download> or <error> tag under the <root> tag */ +void SubsurfaceWebServices::download_dialog_traverse_xml(xmlNodePtr node, unsigned int *download_status) +{ + xmlNodePtr cur_node; + for (cur_node = node; cur_node; cur_node = cur_node->next) { + if ((!strcmp((const char *)cur_node->name, (const char *)"download")) && + (!strcmp((const char *)xmlNodeGetContent(cur_node), (const char *)"ok"))) { + *download_status = DD_STATUS_OK; + return; + } else if (!strcmp((const char *)cur_node->name, (const char *)"error")) { + *download_status = DD_STATUS_ERROR_ID; + return; + } + } +} + +// TODO: C-Code +unsigned int SubsurfaceWebServices::download_dialog_parse_response(const QByteArray &xml) +{ + xmlNodePtr root; + xmlDocPtr doc = xmlParseMemory(xml.data(), xml.length()); + unsigned int status = DD_STATUS_ERROR_PARSE; + + if (!doc) + return DD_STATUS_ERROR_PARSE; + root = xmlDocGetRootElement(doc); + if (!root) { + status = DD_STATUS_ERROR_PARSE; + goto end; + } + if (root->children) + download_dialog_traverse_xml(root->children, &status); +end: + xmlFreeDoc(doc); + return status; +} + +// # +// # +// # Divelogs DE Web Service Implementation. +// # +// # + +struct DiveListResult { + QString errorCondition; + QString errorDetails; + QByteArray idList; // comma-separated, suitable to be sent in the fetch request + int idCount; +}; + +static DiveListResult parseDiveLogsDeDiveList(const QByteArray &xmlData) +{ + /* XML format seems to be: + * <DiveDateReader version="1.0"> + * <DiveDates> + * <date diveLogsId="nnn" lastModified="YYYY-MM-DD hh:mm:ss">DD.MM.YYYY hh:mm</date> + * [repeat <date></date>] + * </DiveDates> + * </DiveDateReader> + */ + QXmlStreamReader reader(xmlData); + const QString invalidXmlError = QObject::tr("Invalid response from server"); + bool seenDiveDates = false; + DiveListResult result; + result.idCount = 0; + + if (reader.readNextStartElement() && reader.name() != "DiveDateReader") { + result.errorCondition = invalidXmlError; + result.errorDetails = + QObject::tr("Expected XML tag 'DiveDateReader', got instead '%1") + .arg(reader.name().toString()); + goto out; + } + + while (reader.readNextStartElement()) { + if (reader.name() != "DiveDates") { + if (reader.name() == "Login") { + QString status = reader.readElementText(); + // qDebug() << "Login status:" << status; + + // Note: there has to be a better way to determine a successful login... + if (status == "failed") { + result.errorCondition = "Login failed"; + goto out; + } + } else { + // qDebug() << "Skipping" << reader.name(); + } + continue; + } + + // process <DiveDates> + seenDiveDates = true; + while (reader.readNextStartElement()) { + if (reader.name() != "date") { + // qDebug() << "Skipping" << reader.name(); + continue; + } + QStringRef id = reader.attributes().value("divelogsId"); + // qDebug() << "Found" << reader.name() << "with id =" << id; + if (!id.isEmpty()) { + result.idList += id.toLatin1(); + result.idList += ','; + ++result.idCount; + } + + reader.skipCurrentElement(); + } + } + + // chop the ending comma, if any + result.idList.chop(1); + + if (!seenDiveDates) { + result.errorCondition = invalidXmlError; + result.errorDetails = QObject::tr("Expected XML tag 'DiveDates' not found"); + } + +out: + if (reader.hasError()) { + // if there was an XML error, overwrite the result or other error conditions + result.errorCondition = invalidXmlError; + result.errorDetails = QObject::tr("Malformed XML response. Line %1: %2") + .arg(reader.lineNumber()) + .arg(reader.errorString()); + } + return result; +} + +DivelogsDeWebServices *DivelogsDeWebServices::instance() +{ + static DivelogsDeWebServices *self = new DivelogsDeWebServices(MainWindow::instance()); + self->setAttribute(Qt::WA_QuitOnClose, false); + return self; +} + +void DivelogsDeWebServices::downloadDives() +{ + uploadMode = false; + resetState(); + hideUpload(); + exec(); +} + +void DivelogsDeWebServices::prepareDivesForUpload(bool selected) +{ + /* generate a random filename and create/open that file with zip_open */ + QString filename = QDir::tempPath() + "/import-" + QString::number(qrand() % 99999999) + ".dld"; + if (prepare_dives_for_divelogs(filename, selected)) { + QFile f(filename); + if (f.open(QIODevice::ReadOnly)) { + uploadDives((QIODevice *)&f); + f.close(); + f.remove(); + return; + } else { + report_error("Failed to open upload file %s\n", qPrintable(filename)); + } + } else { + report_error("Failed to create upload file %s\n", qPrintable(filename)); + } + MainWindow::instance()->getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); +} + +void DivelogsDeWebServices::uploadDives(QIODevice *dldContent) +{ + QHttpMultiPart mp(QHttpMultiPart::FormDataType); + QHttpPart part; + QFile *f = (QFile *)dldContent; + QFileInfo fi(*f); + QString args("form-data; name=\"userfile\"; filename=\"" + fi.absoluteFilePath() + "\""); + part.setRawHeader("Content-Disposition", args.toLatin1()); + part.setBodyDevice(dldContent); + mp.append(part); + + multipart = ∓ + hideDownload(); + resetState(); + uploadMode = true; + ui.buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(true); + ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); + ui.buttonBox->button(QDialogButtonBox::Apply)->setText(tr("Done")); + exec(); + + multipart = NULL; + if (reply != NULL && reply->isOpen()) { + reply->abort(); + delete reply; + reply = NULL; + } +} + +DivelogsDeWebServices::DivelogsDeWebServices(QWidget *parent, Qt::WindowFlags f) : WebServices(parent, f), + multipart(NULL), + uploadMode(false) +{ + QSettings s; + ui.userID->setText(s.value("divelogde_user").toString()); + ui.password->setText(s.value("divelogde_pass").toString()); + ui.saveUidLocal->hide(); + hideUpload(); + QShortcut *close = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(close, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quit = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quit, SIGNAL(activated()), parent, SLOT(close())); +} + +void DivelogsDeWebServices::startUpload() +{ + QSettings s; + s.setValue("divelogde_user", ui.userID->text()); + s.setValue("divelogde_pass", ui.password->text()); + s.sync(); + + ui.status->setText(tr("Uploading dive list...")); + ui.progressBar->setRange(0, 0); // this makes the progressbar do an 'infinite spin' + ui.upload->setEnabled(false); + ui.userID->setEnabled(false); + ui.password->setEnabled(false); + + QNetworkRequest request; + request.setUrl(QUrl("https://divelogs.de/DivelogsDirectImport.php")); + request.setRawHeader("Accept", "text/xml, application/xml"); + request.setRawHeader("User-Agent", userAgent.toUtf8()); + + QHttpPart part; + part.setRawHeader("Content-Disposition", "form-data; name=\"user\""); + part.setBody(ui.userID->text().toUtf8()); + multipart->append(part); + + part.setRawHeader("Content-Disposition", "form-data; name=\"pass\""); + part.setBody(ui.password->text().toUtf8()); + multipart->append(part); + + reply = manager()->post(request, multipart); + connect(reply, SIGNAL(finished()), this, SLOT(uploadFinished())); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, + SLOT(uploadError(QNetworkReply::NetworkError))); + connect(reply, SIGNAL(uploadProgress(qint64, qint64)), this, + SLOT(updateProgress(qint64, qint64))); + + timeout.start(30000); // 30s +} + +void DivelogsDeWebServices::startDownload() +{ + ui.status->setText(tr("Downloading dive list...")); + ui.progressBar->setRange(0, 0); // this makes the progressbar do an 'infinite spin' + ui.download->setEnabled(false); + ui.userID->setEnabled(false); + ui.password->setEnabled(false); + + QNetworkRequest request; + request.setUrl(QUrl("https://divelogs.de/xml_available_dives.php")); + request.setRawHeader("Accept", "text/xml, application/xml"); + request.setRawHeader("User-Agent", userAgent.toUtf8()); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + QUrlQuery body; + body.addQueryItem("user", ui.userID->text()); + body.addQueryItem("pass", ui.password->text()); + + reply = manager()->post(request, body.query(QUrl::FullyEncoded).toLatin1()); + connect(reply, SIGNAL(finished()), this, SLOT(listDownloadFinished())); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), + this, SLOT(downloadError(QNetworkReply::NetworkError))); + + timeout.start(30000); // 30s +} + +void DivelogsDeWebServices::listDownloadFinished() +{ + if (!reply) + return; + QByteArray xmlData = reply->readAll(); + reply->deleteLater(); + reply = NULL; + + // parse the XML data we downloaded + DiveListResult diveList = parseDiveLogsDeDiveList(xmlData); + if (!diveList.errorCondition.isEmpty()) { + // error condition + resetState(); + ui.status->setText(diveList.errorCondition); + return; + } + + ui.status->setText(tr("Downloading %1 dives...").arg(diveList.idCount)); + + QNetworkRequest request; + request.setUrl(QUrl("https://divelogs.de/DivelogsDirectExport.php")); + request.setRawHeader("Accept", "application/zip, */*"); + request.setRawHeader("User-Agent", userAgent.toUtf8()); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + QUrlQuery body; + body.addQueryItem("user", ui.userID->text()); + body.addQueryItem("pass", ui.password->text()); + body.addQueryItem("ids", diveList.idList); + + reply = manager()->post(request, body.query(QUrl::FullyEncoded).toLatin1()); + connect(reply, SIGNAL(readyRead()), this, SLOT(saveToZipFile())); + connectSignalsForDownload(reply); +} + +void DivelogsDeWebServices::saveToZipFile() +{ + if (!zipFile.isOpen()) { + zipFile.setFileTemplate(QDir::tempPath() + "/import-XXXXXX.dld"); + zipFile.open(); + } + + zipFile.write(reply->readAll()); +} + +void DivelogsDeWebServices::downloadFinished() +{ + if (!reply) + return; + + ui.download->setEnabled(true); + ui.status->setText(tr("Download finished - %1").arg(reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString())); + reply->deleteLater(); + reply = NULL; + + int errorcode; + zipFile.seek(0); +#if defined(Q_OS_UNIX) && defined(LIBZIP_VERSION_MAJOR) + int duppedfd = dup(zipFile.handle()); + struct zip *zip = NULL; + if (duppedfd >= 0) { + zip = zip_fdopen(duppedfd, 0, &errorcode); + if (!zip) + ::close(duppedfd); + } else { + QMessageBox::critical(this, tr("Problem with download"), + tr("The archive could not be opened:\n")); + return; + } +#else + struct zip *zip = zip_open(QFile::encodeName(zipFile.fileName()), 0, &errorcode); +#endif + if (!zip) { + char buf[512]; + zip_error_to_str(buf, sizeof(buf), errorcode, errno); + QMessageBox::critical(this, tr("Corrupted download"), + tr("The archive could not be opened:\n%1").arg(QString::fromLocal8Bit(buf))); + zipFile.close(); + return; + } + // now allow the user to cancel or accept + ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); + + zip_close(zip); + zipFile.close(); +#if defined(Q_OS_UNIX) && defined(LIBZIP_VERSION_MAJOR) + ::close(duppedfd); +#endif +} + +void DivelogsDeWebServices::uploadFinished() +{ + if (!reply) + return; + + ui.progressBar->setRange(0, 1); + ui.upload->setEnabled(true); + ui.userID->setEnabled(true); + ui.password->setEnabled(true); + ui.buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false); + ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); + ui.buttonBox->button(QDialogButtonBox::Apply)->setText(tr("Done")); + ui.status->setText(tr("Upload finished")); + + // check what the server sent us: it might contain + // an error condition, such as a failed login + QByteArray xmlData = reply->readAll(); + reply->deleteLater(); + reply = NULL; + char *resp = xmlData.data(); + if (resp) { + char *parsed = strstr(resp, "<Login>"); + if (parsed) { + if (strstr(resp, "<Login>succeeded</Login>")) { + if (strstr(resp, "<FileCopy>failed</FileCopy>")) { + ui.status->setText(tr("Upload failed")); + return; + } + ui.status->setText(tr("Upload successful")); + return; + } + ui.status->setText(tr("Login failed")); + return; + } + ui.status->setText(tr("Cannot parse response")); + } +} + +void DivelogsDeWebServices::setStatusText(int status) +{ +} + +void DivelogsDeWebServices::downloadError(QNetworkReply::NetworkError) +{ + resetState(); + ui.status->setText(tr("Error: %1").arg(reply->errorString())); + reply->deleteLater(); + reply = NULL; +} + +void DivelogsDeWebServices::uploadError(QNetworkReply::NetworkError error) +{ + downloadError(error); +} + +void DivelogsDeWebServices::buttonClicked(QAbstractButton *button) +{ + ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); + switch (ui.buttonBox->buttonRole(button)) { + case QDialogButtonBox::ApplyRole: { + /* in 'uploadMode' button is called 'Done' and closes the dialog */ + if (uploadMode) { + hide(); + close(); + resetState(); + break; + } + /* parse file and import dives */ + parse_file(QFile::encodeName(zipFile.fileName())); + process_dives(true, false); + MainWindow::instance()->refreshDisplay(); + + /* store last entered user/pass in config */ + QSettings s; + s.setValue("divelogde_user", ui.userID->text()); + s.setValue("divelogde_pass", ui.password->text()); + s.sync(); + hide(); + close(); + resetState(); + } break; + case QDialogButtonBox::RejectRole: + // these two seem to be causing a crash: + // reply->deleteLater(); + resetState(); + break; + case QDialogButtonBox::HelpRole: + QDesktopServices::openUrl(QUrl("http://divelogs.de")); + break; + default: + break; + } +} + +UserSurveyServices::UserSurveyServices(QWidget *parent, Qt::WindowFlags f) : WebServices(parent, f) +{ + +} + +QNetworkReply* UserSurveyServices::sendSurvey(QString values) +{ + QNetworkRequest request; + request.setUrl(QString("http://subsurface-divelog.org/survey?%1").arg(values)); + request.setRawHeader("Accept", "text/xml"); + request.setRawHeader("User-Agent", userAgent.toUtf8()); + reply = manager()->get(request); + return reply; +} + +CloudStorageAuthenticate::CloudStorageAuthenticate(QObject *parent) : + QObject(parent), + reply(NULL) +{ + userAgent = getUserAgent(); +} + +#define CLOUDURL QString(prefs.cloud_base_url) +#define CLOUDBACKENDSTORAGE CLOUDURL + "/storage" +#define CLOUDBACKENDVERIFY CLOUDURL + "/verify" +#define CLOUDBACKENDUPDATE CLOUDURL + "/update" + +QNetworkReply* CloudStorageAuthenticate::backend(QString email, QString password, QString pin, QString newpasswd) +{ + QString payload(email + " " + password); + QUrl requestUrl; + if (pin == "" && newpasswd == "") { + requestUrl = QUrl(CLOUDBACKENDSTORAGE); + } else if (newpasswd != "") { + requestUrl = QUrl(CLOUDBACKENDUPDATE); + payload += " " + newpasswd; + } else { + requestUrl = QUrl(CLOUDBACKENDVERIFY); + payload += " " + pin; + } + QNetworkRequest *request = new QNetworkRequest(requestUrl); + request->setRawHeader("Accept", "text/xml, text/plain"); + request->setRawHeader("User-Agent", userAgent.toUtf8()); + request->setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); + reply = WebServices::manager()->post(*request, qPrintable(payload)); + connect(reply, SIGNAL(finished()), this, SLOT(uploadFinished())); + connect(reply, SIGNAL(sslErrors(QList<QSslError>)), this, SLOT(sslErrors(QList<QSslError>))); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, + SLOT(uploadError(QNetworkReply::NetworkError))); + return reply; +} + +void CloudStorageAuthenticate::uploadFinished() +{ + static QString myLastError; + + QString cloudAuthReply(reply->readAll()); + qDebug() << "Completed connection with cloud storage backend, response" << cloudAuthReply; + if (cloudAuthReply == "[VERIFIED]" || cloudAuthReply == "[OK]") { + prefs.cloud_verification_status = CS_VERIFIED; + NotificationWidget *nw = MainWindow::instance()->getNotificationWidget(); + if (nw->getNotificationText() == myLastError) + nw->hideNotification(); + myLastError.clear(); + } else if (cloudAuthReply == "[VERIFY]") { + prefs.cloud_verification_status = CS_NEED_TO_VERIFY; + } else if (cloudAuthReply == "[PASSWDCHANGED]") { + free(prefs.cloud_storage_password); + prefs.cloud_storage_password = prefs.cloud_storage_newpassword; + prefs.cloud_storage_newpassword = NULL; + emit passwordChangeSuccessful(); + return; + } else { + prefs.cloud_verification_status = CS_INCORRECT_USER_PASSWD; + myLastError = cloudAuthReply; + report_error("%s", qPrintable(cloudAuthReply)); + MainWindow::instance()->getNotificationWidget()->showNotification(get_error_string(), KMessageWidget::Error); + } + emit finishedAuthenticate(); +} + +void CloudStorageAuthenticate::uploadError(QNetworkReply::NetworkError error) +{ + qDebug() << "Received error response from cloud storage backend:" << reply->errorString(); +} + +void CloudStorageAuthenticate::sslErrors(QList<QSslError> errorList) +{ + if (verbose) { + qDebug() << "Received error response trying to set up https connection with cloud storage backend:"; + Q_FOREACH (QSslError err, errorList) { + qDebug() << err.errorString(); + } + } + QSslConfiguration conf = reply->sslConfiguration(); + QSslCertificate cert = conf.peerCertificate(); + QByteArray hexDigest = cert.digest().toHex(); + if (reply->url().toString().contains(prefs.cloud_base_url) && + hexDigest == "13ff44c62996cfa5cd69d6810675490e") { + if (verbose) + qDebug() << "Overriding SSL check as I recognize the certificate digest" << hexDigest; + reply->ignoreSslErrors(); + } else { + if (verbose) + qDebug() << "got invalid SSL certificate with hex digest" << hexDigest; + } +} diff --git a/desktop-widgets/subsurfacewebservices.h b/desktop-widgets/subsurfacewebservices.h new file mode 100644 index 000000000..2b454ebc7 --- /dev/null +++ b/desktop-widgets/subsurfacewebservices.h @@ -0,0 +1,142 @@ +#ifndef SUBSURFACEWEBSERVICES_H +#define SUBSURFACEWEBSERVICES_H + +#include <QDialog> +#include <QNetworkReply> +#include <QTemporaryFile> +#include <QTimer> +#include <libxml/tree.h> + +#include "ui_webservices.h" + +class QAbstractButton; +class QHttpMultiPart; + +class WebServices : public QDialog { + Q_OBJECT +public: + explicit WebServices(QWidget *parent = 0, Qt::WindowFlags f = 0); + void hidePassword(); + void hideUpload(); + void hideDownload(); + + static QNetworkAccessManager *manager(); + +private +slots: + virtual void startDownload() = 0; + virtual void startUpload() = 0; + virtual void buttonClicked(QAbstractButton *button) = 0; + virtual void downloadTimedOut(); + +protected +slots: + void updateProgress(qint64 current, qint64 total); + +protected: + void resetState(); + void connectSignalsForDownload(QNetworkReply *reply); + void connectSignalsForUpload(); + + Ui::WebServices ui; + QNetworkReply *reply; + QTimer timeout; + QByteArray downloadedData; + QString defaultApplyText; + QString userAgent; +}; + +class SubsurfaceWebServices : public WebServices { + Q_OBJECT +public: + explicit SubsurfaceWebServices(QWidget *parent = 0, Qt::WindowFlags f = 0); + +private +slots: + void startDownload(); + void buttonClicked(QAbstractButton *button); + void downloadFinished(); + void downloadError(QNetworkReply::NetworkError error); + void startUpload() + { + } /*no op*/ +private: + void setStatusText(int status); + void download_dialog_traverse_xml(xmlNodePtr node, unsigned int *download_status); + unsigned int download_dialog_parse_response(const QByteArray &length); +}; + +class DivelogsDeWebServices : public WebServices { + Q_OBJECT +public: + static DivelogsDeWebServices *instance(); + void downloadDives(); + void prepareDivesForUpload(bool selected); + +private +slots: + void startDownload(); + void buttonClicked(QAbstractButton *button); + void saveToZipFile(); + void listDownloadFinished(); + void downloadFinished(); + void uploadFinished(); + void downloadError(QNetworkReply::NetworkError error); + void uploadError(QNetworkReply::NetworkError error); + void startUpload(); + +private: + void uploadDives(QIODevice *dldContent); + explicit DivelogsDeWebServices(QWidget *parent = 0, Qt::WindowFlags f = 0); + void setStatusText(int status); + bool prepare_dives_for_divelogs(const QString &filename, bool selected); + void download_dialog_traverse_xml(xmlNodePtr node, unsigned int *download_status); + unsigned int download_dialog_parse_response(const QByteArray &length); + + QHttpMultiPart *multipart; + QTemporaryFile zipFile; + bool uploadMode; +}; + +class UserSurveyServices : public WebServices { + Q_OBJECT +public: + QNetworkReply* sendSurvey(QString values); + explicit UserSurveyServices(QWidget *parent = 0, Qt::WindowFlags f = 0); +private +slots: + // need to declare them as no ops or Qt4 is unhappy + virtual void startDownload() { } + virtual void startUpload() { } + virtual void buttonClicked(QAbstractButton *button) { } +}; + +class CloudStorageAuthenticate : public QObject { + Q_OBJECT +public: + QNetworkReply* backend(QString email, QString password, QString pin = "", QString newpasswd = ""); + explicit CloudStorageAuthenticate(QObject *parent); +signals: + void finishedAuthenticate(); + void passwordChangeSuccessful(); +private +slots: + void uploadError(QNetworkReply::NetworkError error); + void sslErrors(QList<QSslError> errorList); + void uploadFinished(); +private: + QNetworkReply *reply; + QString userAgent; + +}; + +#ifdef __cplusplus +extern "C" { +#endif +extern void set_save_userid_local(short value); +extern void set_userid(char *user_id); +#ifdef __cplusplus +} +#endif + +#endif // SUBSURFACEWEBSERVICES_H diff --git a/desktop-widgets/tableview.cpp b/desktop-widgets/tableview.cpp new file mode 100644 index 000000000..40d5199ec --- /dev/null +++ b/desktop-widgets/tableview.cpp @@ -0,0 +1,145 @@ +#include "tableview.h" +#include "modeldelegates.h" + +#include <QPushButton> +#include <QSettings> + +TableView::TableView(QWidget *parent) : QGroupBox(parent) +{ + ui.setupUi(this); + ui.tableView->setItemDelegate(new DiveListDelegate(this)); + + QFontMetrics fm(defaultModelFont()); + int text_ht = fm.height(); + + metrics.icon = &defaultIconMetrics(); + + metrics.rm_col_width = metrics.icon->sz_small + 2*metrics.icon->spacing; + metrics.header_ht = text_ht + 10; // TODO DPI + + /* We want to get rid of the margin around the table, but + * we must be careful with some styles (e.g. GTK+) where the top + * margin is actually used to hold the label. We thus check the + * rectangles for the label and contents to make sure they do not + * overlap, and adjust the top contentsMargin accordingly + */ + + // start by setting all the margins at zero + QMargins margins; + + // grab the label and contents dimensions and positions + QStyleOptionGroupBox option; + initStyleOption(&option); + QRect labelRect = style()->subControlRect(QStyle::CC_GroupBox, &option, QStyle::SC_GroupBoxLabel, this); + QRect contentsRect = style()->subControlRect(QStyle::CC_GroupBox, &option, QStyle::SC_GroupBoxContents, this); + + /* we need to ensure that the bottom of the label is higher + * than the top of the contents */ + int delta = contentsRect.top() - labelRect.bottom(); + const int min_gap = metrics.icon->spacing; + if (delta <= min_gap) { + margins.setTop(min_gap - delta); + } + layout()->setContentsMargins(margins); + + QIcon plusIcon(":plus"); + plusBtn = new QPushButton(plusIcon, QString(), this); + plusBtn->setFlat(true); + + /* now determine the icon and button size. Since the button will be + * placed in the label, make sure that we do not overflow, as it might + * get clipped + */ + int iconSize = metrics.icon->sz_small; + int btnSize = iconSize + 2*min_gap; + if (btnSize > labelRect.height()) { + btnSize = labelRect.height(); + iconSize = btnSize - 2*min_gap; + } + plusBtn->setIconSize(QSize(iconSize, iconSize)); + plusBtn->resize(btnSize, btnSize); + connect(plusBtn, SIGNAL(clicked(bool)), this, SIGNAL(addButtonClicked())); +} + +TableView::~TableView() +{ + QSettings s; + s.beginGroup(objectName()); + // remove the old default + bool oldDefault = (ui.tableView->columnWidth(0) == 30); + for (int i = 1; oldDefault && i < ui.tableView->model()->columnCount(); i++) { + if (ui.tableView->columnWidth(i) != 80) + oldDefault = false; + } + if (oldDefault) { + s.remove(""); + } else { + for (int i = 0; i < ui.tableView->model()->columnCount(); i++) { + if (ui.tableView->columnWidth(i) == defaultColumnWidth(i)) + s.remove(QString("colwidth%1").arg(i)); + else + s.setValue(QString("colwidth%1").arg(i), ui.tableView->columnWidth(i)); + } + } + s.endGroup(); +} + +void TableView::setBtnToolTip(const QString &tooltip) +{ + plusBtn->setToolTip(tooltip); +} + +void TableView::setModel(QAbstractItemModel *model) +{ + ui.tableView->setModel(model); + connect(ui.tableView, SIGNAL(clicked(QModelIndex)), model, SLOT(remove(QModelIndex))); + + QSettings s; + s.beginGroup(objectName()); + const int columnCount = ui.tableView->model()->columnCount(); + for (int i = 0; i < columnCount; i++) { + QVariant width = s.value(QString("colwidth%1").arg(i), defaultColumnWidth(i)); + ui.tableView->setColumnWidth(i, width.toInt()); + } + s.endGroup(); + + ui.tableView->horizontalHeader()->setMinimumHeight(metrics.header_ht); +} + +void TableView::fixPlusPosition() +{ + QStyleOptionGroupBox option; + initStyleOption(&option); + QRect labelRect = style()->subControlRect(QStyle::CC_GroupBox, &option, QStyle::SC_GroupBoxLabel, this); + QRect contentsRect = style()->subControlRect(QStyle::CC_GroupBox, &option, QStyle::QStyle::SC_GroupBoxFrame, this); + plusBtn->setGeometry( contentsRect.width() - plusBtn->width(), labelRect.y(), plusBtn->width(), labelRect.height()); +} + +// We need to manually position the 'plus' on cylinder and weight. +void TableView::resizeEvent(QResizeEvent *event) +{ + fixPlusPosition(); + QWidget::resizeEvent(event); +} + +void TableView::showEvent(QShowEvent *event) +{ + QWidget::showEvent(event); + fixPlusPosition(); +} + +void TableView::edit(const QModelIndex &index) +{ + ui.tableView->edit(index); +} + +int TableView::defaultColumnWidth(int col) +{ + QString text = ui.tableView->model()->headerData(col, Qt::Horizontal).toString(); + return text.isEmpty() ? metrics.rm_col_width : defaultModelFontMetrics().width(text) + 4; // add small margin +} + +QTableView *TableView::view() +{ + return ui.tableView; +} diff --git a/desktop-widgets/tableview.h b/desktop-widgets/tableview.h new file mode 100644 index 000000000..f72b256ea --- /dev/null +++ b/desktop-widgets/tableview.h @@ -0,0 +1,54 @@ +#ifndef TABLEVIEW_H +#define TABLEVIEW_H + +/* This TableView is prepared to have the CSS, + * the methods to restore / save the state of + * the column widths and the 'plus' button. + */ +#include <QWidget> + +#include "ui_tableview.h" + +#include "metrics.h" + +class QPushButton; +class QAbstractItemModel; +class QModelIndex; +class QTableView; + +class TableView : public QGroupBox { + Q_OBJECT + + struct TableMetrics { + const IconMetrics* icon; // icon metrics + int rm_col_width; // column width of REMOVE column + int header_ht; // height of the header + }; +public: + TableView(QWidget *parent = 0); + virtual ~TableView(); + /* The model is expected to have a 'remove' slot, that takes a QModelIndex as parameter. + * It's also expected to have the column '1' as a trash icon. I most probably should create a + * proxy model and add that column, will mark that as TODO. see? marked. + */ + void setModel(QAbstractItemModel *model); + void setBtnToolTip(const QString &tooltip); + void fixPlusPosition(); + void edit(const QModelIndex &index); + int defaultColumnWidth(int col); // default column width for column col + QTableView *view(); + +protected: + virtual void showEvent(QShowEvent *); + virtual void resizeEvent(QResizeEvent *); + +signals: + void addButtonClicked(); + +private: + Ui::TableView ui; + QPushButton *plusBtn; + TableMetrics metrics; +}; + +#endif // TABLEVIEW_H diff --git a/desktop-widgets/tableview.ui b/desktop-widgets/tableview.ui new file mode 100644 index 000000000..73867231e --- /dev/null +++ b/desktop-widgets/tableview.ui @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>TableView</class> + <widget class="QGroupBox" name="TableView"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>GroupBox</string> + </property> + <property name="title"> + <string>GroupBox</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTableView" name="tableView"/> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/desktop-widgets/tagwidget.cpp b/desktop-widgets/tagwidget.cpp new file mode 100644 index 000000000..3b61b492a --- /dev/null +++ b/desktop-widgets/tagwidget.cpp @@ -0,0 +1,210 @@ +#include "tagwidget.h" +#include "mainwindow.h" +#include "maintab.h" +#include <QCompleter> + +TagWidget::TagWidget(QWidget *parent) : GroupedLineEdit(parent), m_completer(NULL), lastFinishedTag(false) +{ + connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(reparse())); + connect(this, SIGNAL(textChanged()), this, SLOT(reparse())); + + QColor textColor = palette().color(QPalette::Text); + qreal h, s, l, a; + textColor.getHslF(&h, &s, &l, &a); + // I use dark themes + if (l <= 0.3) { // very dark text. get a brigth background + addColor(QColor(Qt::red).lighter(120)); + addColor(QColor(Qt::green).lighter(120)); + addColor(QColor(Qt::blue).lighter(120)); + } else if (l <= 0.6) { // moderated dark text. get a somewhat bright background + addColor(QColor(Qt::red).lighter(60)); + addColor(QColor(Qt::green).lighter(60)); + addColor(QColor(Qt::blue).lighter(60)); + } else { + addColor(QColor(Qt::red).darker(120)); + addColor(QColor(Qt::green).darker(120)); + addColor(QColor(Qt::blue).darker(120)); + } // light text. get a dark background. + setFocusPolicy(Qt::StrongFocus); +} + +void TagWidget::setCompleter(QCompleter *completer) +{ + m_completer = completer; + m_completer->setWidget(this); + connect(m_completer, SIGNAL(activated(QString)), this, SLOT(completionSelected(QString))); + connect(m_completer, SIGNAL(highlighted(QString)), this, SLOT(completionHighlighted(QString))); +} + +QPair<int, int> TagWidget::getCursorTagPosition() +{ + int i = 0, start = 0, end = 0; + /* Parse string near cursor */ + i = cursorPosition(); + while (--i > 0) { + if (text().at(i) == ',') { + if (i > 0 && text().at(i - 1) != '\\') { + i++; + break; + } + } + } + start = i; + while (++i < text().length()) { + if (text().at(i) == ',') { + if (i > 0 && text().at(i - 1) != '\\') + break; + } + } + end = i; + if (start < 0 || end < 0) { + start = 0; + end = 0; + } + return qMakePair(start, end); +} + +void TagWidget::highlight() +{ + removeAllBlocks(); + int lastPos = 0; + Q_FOREACH (const QString& s, text().split(QChar(','), QString::SkipEmptyParts)) { + QString trimmed = s.trimmed(); + if (trimmed.isEmpty()) + continue; + int start = text().indexOf(trimmed, lastPos); + addBlock(start, trimmed.size() + start); + lastPos = trimmed.size() + start; + } +} + +void TagWidget::reparse() +{ + highlight(); + QPair<int, int> pos = getCursorTagPosition(); + QString currentText; + if (pos.first >= 0 && pos.second > 0) + currentText = text().mid(pos.first, pos.second - pos.first).trimmed(); + + /* + * Do not show the completer when not in edit mode - basically + * this returns when we are accepting or discarding the changes. + */ + if (MainWindow::instance()->information()->isEditing() == false || currentText.length() == 0) { + return; + } + + if (m_completer) { + m_completer->setCompletionPrefix(currentText); + if (m_completer->completionCount() == 1) { + if (m_completer->currentCompletion() == currentText) { + QAbstractItemView *popup = m_completer->popup(); + if (popup) + popup->hide(); + } else { + m_completer->complete(); + } + } else { + m_completer->complete(); + } + } +} + +void TagWidget::completionSelected(const QString &completion) +{ + completionHighlighted(completion); + emit textChanged(); +} + +void TagWidget::completionHighlighted(const QString &completion) +{ + QPair<int, int> pos = getCursorTagPosition(); + setText(text().remove(pos.first, pos.second - pos.first).insert(pos.first, completion)); + setCursorPosition(pos.first + completion.length()); +} + +void TagWidget::setCursorPosition(int position) +{ + blockSignals(true); + GroupedLineEdit::setCursorPosition(position); + blockSignals(false); +} + +void TagWidget::setText(const QString &text) +{ + blockSignals(true); + GroupedLineEdit::setText(text); + blockSignals(false); + highlight(); +} + +void TagWidget::clear() +{ + blockSignals(true); + GroupedLineEdit::clear(); + blockSignals(false); +} + +void TagWidget::keyPressEvent(QKeyEvent *e) +{ + QPair<int, int> pos; + QAbstractItemView *popup; + bool finishedTag = false; + switch (e->key()) { + case Qt::Key_Escape: + pos = getCursorTagPosition(); + if (pos.first >= 0 && pos.second > 0) { + setText(text().remove(pos.first, pos.second - pos.first)); + setCursorPosition(pos.first); + } + popup = m_completer->popup(); + if (popup) + popup->hide(); + return; + case Qt::Key_Return: + case Qt::Key_Enter: + case Qt::Key_Tab: + /* + * Fake the QLineEdit behaviour by simply + * closing the QAbstractViewitem + */ + if (m_completer) { + popup = m_completer->popup(); + if (popup) + popup->hide(); + } + finishedTag = true; + break; + case Qt::Key_Comma: { /* if this is the last key, and the previous string is empty, ignore the comma. */ + QString temp = text(); + if (temp.split(QChar(',')).last().trimmed().isEmpty()){ + e->ignore(); + return; + } + } + } + if (e->key() == Qt::Key_Tab && lastFinishedTag) { // if we already end in comma, go to next/prev field + MainWindow::instance()->information()->nextInputField(e); // by sending the key event to the MainTab widget + } else if (e->key() == Qt::Key_Tab || e->key() == Qt::Key_Return) { // otherwise let's pretend this is a comma instead + QKeyEvent fakeEvent(e->type(), Qt::Key_Comma, e->modifiers(), QString(",")); + keyPressEvent(&fakeEvent); + } else { + GroupedLineEdit::keyPressEvent(e); + } + lastFinishedTag = finishedTag; +} + +void TagWidget::wheelEvent(QWheelEvent *event) +{ + if (hasFocus()) { + GroupedLineEdit::wheelEvent(event); + } +} + +void TagWidget::fixPopupPosition(int delta) +{ + if(m_completer->popup()->isVisible()){ + QRect toGlobal = m_completer->popup()->geometry(); + m_completer->popup()->setGeometry(toGlobal.x(), toGlobal.y() + delta +10, toGlobal.width(), toGlobal.height()); + } +} diff --git a/desktop-widgets/tagwidget.h b/desktop-widgets/tagwidget.h new file mode 100644 index 000000000..6a16129f3 --- /dev/null +++ b/desktop-widgets/tagwidget.h @@ -0,0 +1,34 @@ +#ifndef TAGWIDGET_H +#define TAGWIDGET_H + +#include "groupedlineedit.h" +#include <QPair> + +class QCompleter; + +class TagWidget : public GroupedLineEdit { + Q_OBJECT +public: + explicit TagWidget(QWidget *parent = 0); + void setCompleter(QCompleter *completer); + QPair<int, int> getCursorTagPosition(); + void highlight(); + void setText(const QString &text); + void clear(); + void setCursorPosition(int position); + void wheelEvent(QWheelEvent *event); + void fixPopupPosition(int delta); +public +slots: + void reparse(); + void completionSelected(const QString &text); + void completionHighlighted(const QString &text); + +protected: + void keyPressEvent(QKeyEvent *e); +private: + QCompleter *m_completer; + bool lastFinishedTag; +}; + +#endif // TAGWIDGET_H diff --git a/desktop-widgets/templateedit.cpp b/desktop-widgets/templateedit.cpp new file mode 100644 index 000000000..4964016b9 --- /dev/null +++ b/desktop-widgets/templateedit.cpp @@ -0,0 +1,227 @@ +#include "templateedit.h" +#include "printoptions.h" +#include "printer.h" +#include "ui_templateedit.h" + +#include <QMessageBox> +#include <QColorDialog> + +TemplateEdit::TemplateEdit(QWidget *parent, struct print_options *printOptions, struct template_options *templateOptions) : + QDialog(parent), + ui(new Ui::TemplateEdit) +{ + ui->setupUi(this); + this->templateOptions = templateOptions; + newTemplateOptions = *templateOptions; + this->printOptions = printOptions; + + // restore the settings and init the UI + ui->fontSelection->setCurrentIndex(templateOptions->font_index); + ui->fontsize->setValue(templateOptions->font_size); + ui->colorpalette->setCurrentIndex(templateOptions->color_palette_index); + ui->linespacing->setValue(templateOptions->line_spacing); + + grantlee_template = TemplateLayout::readTemplate(printOptions->p_template); + if (printOptions->type == print_options::DIVELIST) + grantlee_template = TemplateLayout::readTemplate(printOptions->p_template); + else if (printOptions->type == print_options::STATISTICS) + grantlee_template = TemplateLayout::readTemplate(QString::fromUtf8("statistics") + QDir::separator() + printOptions->p_template); + + // gui + btnGroup = new QButtonGroup; + btnGroup->addButton(ui->editButton1, 1); + btnGroup->addButton(ui->editButton2, 2); + btnGroup->addButton(ui->editButton3, 3); + btnGroup->addButton(ui->editButton4, 4); + btnGroup->addButton(ui->editButton5, 5); + btnGroup->addButton(ui->editButton6, 6); + connect(btnGroup, SIGNAL(buttonClicked(QAbstractButton*)), this, SLOT(colorSelect(QAbstractButton*))); + + ui->plainTextEdit->setPlainText(grantlee_template); + editingCustomColors = false; + updatePreview(); +} + +TemplateEdit::~TemplateEdit() +{ + delete btnGroup; + delete ui; +} + +void TemplateEdit::updatePreview() +{ + // update Qpixmap preview + int width = ui->label->width(); + int height = ui->label->height(); + QPixmap map(width * 2, height * 2); + map.fill(QColor::fromRgb(255, 255, 255)); + Printer printer(&map, printOptions, &newTemplateOptions, Printer::PREVIEW); + printer.previewOnePage(); + ui->label->setPixmap(map.scaled(width, height, Qt::IgnoreAspectRatio)); + + // update colors tab + ui->colorLable1->setStyleSheet("QLabel { background-color : \"" + newTemplateOptions.color_palette.color1.name() + "\";}"); + ui->colorLable2->setStyleSheet("QLabel { background-color : \"" + newTemplateOptions.color_palette.color2.name() + "\";}"); + ui->colorLable3->setStyleSheet("QLabel { background-color : \"" + newTemplateOptions.color_palette.color3.name() + "\";}"); + ui->colorLable4->setStyleSheet("QLabel { background-color : \"" + newTemplateOptions.color_palette.color4.name() + "\";}"); + ui->colorLable5->setStyleSheet("QLabel { background-color : \"" + newTemplateOptions.color_palette.color5.name() + "\";}"); + ui->colorLable6->setStyleSheet("QLabel { background-color : \"" + newTemplateOptions.color_palette.color6.name() + "\";}"); + + ui->colorLable1->setText(newTemplateOptions.color_palette.color1.name()); + ui->colorLable2->setText(newTemplateOptions.color_palette.color2.name()); + ui->colorLable3->setText(newTemplateOptions.color_palette.color3.name()); + ui->colorLable4->setText(newTemplateOptions.color_palette.color4.name()); + ui->colorLable5->setText(newTemplateOptions.color_palette.color5.name()); + ui->colorLable6->setText(newTemplateOptions.color_palette.color6.name()); + + // update critical UI elements + ui->colorpalette->setCurrentIndex(newTemplateOptions.color_palette_index); + + // update grantlee template string + grantlee_template = TemplateLayout::readTemplate(printOptions->p_template); + if (printOptions->type == print_options::DIVELIST) + grantlee_template = TemplateLayout::readTemplate(printOptions->p_template); + else if (printOptions->type == print_options::STATISTICS) + grantlee_template = TemplateLayout::readTemplate(QString::fromUtf8("statistics") + QDir::separator() + printOptions->p_template); +} + +void TemplateEdit::on_fontsize_valueChanged(int font_size) +{ + newTemplateOptions.font_size = font_size; + updatePreview(); +} + +void TemplateEdit::on_linespacing_valueChanged(double line_spacing) +{ + newTemplateOptions.line_spacing = line_spacing; + updatePreview(); +} + +void TemplateEdit::on_fontSelection_currentIndexChanged(int index) +{ + newTemplateOptions.font_index = index; + updatePreview(); +} + +void TemplateEdit::on_colorpalette_currentIndexChanged(int index) +{ + newTemplateOptions.color_palette_index = index; + switch (newTemplateOptions.color_palette_index) { + case SSRF_COLORS: // subsurface derived default colors + newTemplateOptions.color_palette = ssrf_colors; + break; + case ALMOND: // almond + newTemplateOptions.color_palette = almond_colors; + break; + case BLUESHADES: // blueshades + newTemplateOptions.color_palette = blueshades_colors; + break; + case CUSTOM: // custom + if (!editingCustomColors) + newTemplateOptions.color_palette = custom_colors; + else + editingCustomColors = false; + break; + } + updatePreview(); +} + +void TemplateEdit::saveSettings() +{ + if ((*templateOptions) != newTemplateOptions || grantlee_template.compare(ui->plainTextEdit->toPlainText())) { + QMessageBox msgBox(this); + QString message = tr("Do you want to save your changes?"); + bool templateChanged = false; + if (grantlee_template.compare(ui->plainTextEdit->toPlainText())) + templateChanged = true; + msgBox.setText(message); + msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Cancel); + if (msgBox.exec() == QMessageBox::Save) { + memcpy(templateOptions, &newTemplateOptions, sizeof(struct template_options)); + if (templateChanged) { + TemplateLayout::writeTemplate(printOptions->p_template, ui->plainTextEdit->toPlainText()); + if (printOptions->type == print_options::DIVELIST) + TemplateLayout::writeTemplate(printOptions->p_template, ui->plainTextEdit->toPlainText()); + else if (printOptions->type == print_options::STATISTICS) + TemplateLayout::writeTemplate(QString::fromUtf8("statistics") + QDir::separator() + printOptions->p_template, ui->plainTextEdit->toPlainText()); + } + if (templateOptions->color_palette_index == CUSTOM) + custom_colors = templateOptions->color_palette; + } + } +} + +void TemplateEdit::on_buttonBox_clicked(QAbstractButton *button) +{ + QDialogButtonBox::StandardButton standardButton = ui->buttonBox->standardButton(button); + switch (standardButton) { + case QDialogButtonBox::Ok: + saveSettings(); + break; + case QDialogButtonBox::Cancel: + break; + case QDialogButtonBox::Apply: + saveSettings(); + updatePreview(); + break; + default: + ; + } +} + +void TemplateEdit::colorSelect(QAbstractButton *button) +{ + editingCustomColors = true; + // reset custom colors palette + switch (newTemplateOptions.color_palette_index) { + case SSRF_COLORS: // subsurface derived default colors + newTemplateOptions.color_palette = ssrf_colors; + break; + case ALMOND: // almond + newTemplateOptions.color_palette = almond_colors; + break; + case BLUESHADES: // blueshades + newTemplateOptions.color_palette = blueshades_colors; + break; + default: + break; + } + + //change selected color + QColor color; + switch (btnGroup->id(button)) { + case 1: + color = QColorDialog::getColor(newTemplateOptions.color_palette.color1, this); + if (color.isValid()) + newTemplateOptions.color_palette.color1 = color; + break; + case 2: + color = QColorDialog::getColor(newTemplateOptions.color_palette.color2, this); + if (color.isValid()) + newTemplateOptions.color_palette.color2 = color; + break; + case 3: + color = QColorDialog::getColor(newTemplateOptions.color_palette.color3, this); + if (color.isValid()) + newTemplateOptions.color_palette.color3 = color; + break; + case 4: + color = QColorDialog::getColor(newTemplateOptions.color_palette.color4, this); + if (color.isValid()) + newTemplateOptions.color_palette.color4 = color; + break; + case 5: + color = QColorDialog::getColor(newTemplateOptions.color_palette.color5, this); + if (color.isValid()) + newTemplateOptions.color_palette.color5 = color; + break; + case 6: + color = QColorDialog::getColor(newTemplateOptions.color_palette.color6, this); + if (color.isValid()) + newTemplateOptions.color_palette.color6 = color; + break; + } + newTemplateOptions.color_palette_index = CUSTOM; + updatePreview(); +} diff --git a/desktop-widgets/templateedit.h b/desktop-widgets/templateedit.h new file mode 100644 index 000000000..5e548ae19 --- /dev/null +++ b/desktop-widgets/templateedit.h @@ -0,0 +1,44 @@ +#ifndef TEMPLATEEDIT_H +#define TEMPLATEEDIT_H + +#include <QDialog> +#include "templatelayout.h" + +namespace Ui { +class TemplateEdit; +} + +class TemplateEdit : public QDialog +{ + Q_OBJECT + +public: + explicit TemplateEdit(QWidget *parent, struct print_options *printOptions, struct template_options *templateOptions); + ~TemplateEdit(); +private slots: + void on_fontsize_valueChanged(int font_size); + + void on_linespacing_valueChanged(double line_spacing); + + void on_fontSelection_currentIndexChanged(int index); + + void on_colorpalette_currentIndexChanged(int index); + + void on_buttonBox_clicked(QAbstractButton *button); + + void colorSelect(QAbstractButton *button); + +private: + Ui::TemplateEdit *ui; + QButtonGroup *btnGroup; + bool editingCustomColors; + struct template_options *templateOptions; + struct template_options newTemplateOptions; + struct print_options *printOptions; + QString grantlee_template; + void saveSettings(); + void updatePreview(); + +}; + +#endif // TEMPLATEEDIT_H diff --git a/desktop-widgets/templateedit.ui b/desktop-widgets/templateedit.ui new file mode 100644 index 000000000..60a0fc7e6 --- /dev/null +++ b/desktop-widgets/templateedit.ui @@ -0,0 +1,577 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>TemplateEdit</class> + <widget class="QDialog" name="TemplateEdit"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>770</width> + <height>433</height> + </rect> + </property> + <property name="windowTitle"> + <string>Edit template</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_8"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_10"> + <item> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <spacer name="verticalSpacer_4"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Preview</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>180</width> + <height>240</height> + </size> + </property> + <property name="baseSize"> + <size> + <width>180</width> + <height>254</height> + </size> + </property> + <property name="styleSheet"> + <string notr="true"/> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_3"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="autoFillBackground"> + <bool>false</bool> + </property> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="style"> + <attribute name="title"> + <string>Style</string> + </attribute> + <layout class="QHBoxLayout" name="horizontalLayout_11"> + <item> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <widget class="QLabel" name="fontselection_label"> + <property name="text"> + <string>Font</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="fontSelection"> + <item> + <property name="text"> + <string>Arial</string> + </property> + </item> + <item> + <property name="text"> + <string>Impact</string> + </property> + </item> + <item> + <property name="text"> + <string>Georgia</string> + </property> + </item> + <item> + <property name="text"> + <string>Courier</string> + </property> + </item> + <item> + <property name="text"> + <string>Verdana</string> + </property> + </item> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QLabel" name="fontsize_label"> + <property name="text"> + <string>Font size</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="fontsize"> + <property name="minimum"> + <number>9</number> + </property> + <property name="maximum"> + <number>18</number> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="colorpalette_label"> + <property name="text"> + <string>Color palette</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="colorpalette"> + <item> + <property name="text"> + <string>Default</string> + </property> + </item> + <item> + <property name="text"> + <string>Almond</string> + </property> + </item> + <item> + <property name="text"> + <string>Shades of blue</string> + </property> + </item> + <item> + <property name="text"> + <string>Custom</string> + </property> + </item> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QLabel" name="linespacing_label"> + <property name="text"> + <string>Line spacing</string> + </property> + </widget> + </item> + <item> + <widget class="QDoubleSpinBox" name="linespacing"> + <property name="minimum"> + <double>1.000000000000000</double> + </property> + <property name="maximum"> + <double>3.000000000000000</double> + </property> + <property name="singleStep"> + <double>0.250000000000000</double> + </property> + <property name="value"> + <double>1.250000000000000</double> + </property> + </widget> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + <widget class="QWidget" name="template_2"> + <attribute name="title"> + <string>Template</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QPlainTextEdit" name="plainTextEdit"> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAsNeeded</enum> + </property> + <property name="lineWrapMode"> + <enum>QPlainTextEdit::NoWrap</enum> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="color_tab"> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>16777215</height> + </size> + </property> + <attribute name="title"> + <string>Colors</string> + </attribute> + <layout class="QHBoxLayout" name="horizontalLayout_12"> + <item> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_5"> + <item> + <widget class="QLabel" name="label_3"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Background</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="colorLable1"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>color1</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="editButton1"> + <property name="text"> + <string>Edit</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_8"> + <item> + <widget class="QLabel" name="label_9"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Table cells 1</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="colorLable2"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>color2</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="editButton2"> + <property name="text"> + <string>Edit</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_13"> + <item> + <widget class="QLabel" name="label_6"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Table cells 2</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="colorLable3"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>color3</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="editButton3"> + <property name="text"> + <string>Edit</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_7"> + <item> + <widget class="QLabel" name="label_7"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Text 1</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="colorLable4"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>color4</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="editButton4"> + <property name="text"> + <string>Edit</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_9"> + <item> + <widget class="QLabel" name="label_11"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Text 2</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="colorLable5"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>color5</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="editButton5"> + <property name="text"> + <string>Edit</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_6"> + <item> + <widget class="QLabel" name="label_4"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Borders</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="colorLable6"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>color6</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="editButton6"> + <property name="text"> + <string>Edit</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>TemplateEdit</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>TemplateEdit</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/desktop-widgets/templatelayout.cpp b/desktop-widgets/templatelayout.cpp new file mode 100644 index 000000000..a376459a6 --- /dev/null +++ b/desktop-widgets/templatelayout.cpp @@ -0,0 +1,182 @@ +#include <string> + +#include "templatelayout.h" +#include "helpers.h" +#include "display.h" + +QList<QString> grantlee_templates, grantlee_statistics_templates; + +int getTotalWork(print_options *printOptions) +{ + if (printOptions->print_selected) { + // return the correct number depending on all/selected dives + // but don't return 0 as we might divide by this number + return amount_selected ? amount_selected : 1; + } + int dives = 0, i; + struct dive *dive; + for_each_dive (i, dive) { + dives++; + } + return dives; +} + +void find_all_templates() +{ + grantlee_templates.clear(); + grantlee_statistics_templates.clear(); + QDir dir(getPrintingTemplatePathUser()); + QFileInfoList list = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); + foreach (QFileInfo finfo, list) { + QString filename = finfo.fileName(); + if (filename.at(filename.size() - 1) != '~') { + grantlee_templates.append(finfo.fileName()); + } + } + // find statistics templates + dir.setPath(getPrintingTemplatePathUser() + QDir::separator() + "statistics"); + list = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); + foreach (QFileInfo finfo, list) { + QString filename = finfo.fileName(); + if (filename.at(filename.size() - 1) != '~') { + grantlee_statistics_templates.append(finfo.fileName()); + } + } +} + +TemplateLayout::TemplateLayout(print_options *PrintOptions, template_options *templateOptions) : + m_engine(NULL) +{ + this->PrintOptions = PrintOptions; + this->templateOptions = templateOptions; +} + +TemplateLayout::~TemplateLayout() +{ + delete m_engine; +} + +QString TemplateLayout::generate() +{ + int progress = 0; + int totalWork = getTotalWork(PrintOptions); + + QString htmlContent; + m_engine = new Grantlee::Engine(this); + + QSharedPointer<Grantlee::FileSystemTemplateLoader> m_templateLoader = + QSharedPointer<Grantlee::FileSystemTemplateLoader>(new Grantlee::FileSystemTemplateLoader()); + m_templateLoader->setTemplateDirs(QStringList() << getPrintingTemplatePathUser()); + m_engine->addTemplateLoader(m_templateLoader); + + Grantlee::registerMetaType<Dive>(); + Grantlee::registerMetaType<template_options>(); + Grantlee::registerMetaType<print_options>(); + + QVariantList diveList; + + struct dive *dive; + int i; + for_each_dive (i, dive) { + //TODO check for exporting selected dives only + if (!dive->selected && PrintOptions->print_selected) + continue; + Dive d(dive); + diveList.append(QVariant::fromValue(d)); + progress++; + emit progressUpdated(progress * 100.0 / totalWork); + } + Grantlee::Context c; + c.insert("dives", diveList); + c.insert("template_options", QVariant::fromValue(*templateOptions)); + c.insert("print_options", QVariant::fromValue(*PrintOptions)); + + Grantlee::Template t = m_engine->loadByName(PrintOptions->p_template); + if (!t || t->error()) { + qDebug() << "Can't load template"; + return htmlContent; + } + + htmlContent = t->render(&c); + + if (t->error()) { + qDebug() << "Can't render template"; + return htmlContent; + } + return htmlContent; +} + +QString TemplateLayout::generateStatistics() +{ + QString htmlContent; + m_engine = new Grantlee::Engine(this); + + QSharedPointer<Grantlee::FileSystemTemplateLoader> m_templateLoader = + QSharedPointer<Grantlee::FileSystemTemplateLoader>(new Grantlee::FileSystemTemplateLoader()); + m_templateLoader->setTemplateDirs(QStringList() << getPrintingTemplatePathUser() + QDir::separator() + QString("statistics")); + m_engine->addTemplateLoader(m_templateLoader); + + Grantlee::registerMetaType<YearInfo>(); + Grantlee::registerMetaType<template_options>(); + Grantlee::registerMetaType<print_options>(); + + QVariantList years; + + int i = 0; + while (stats_yearly != NULL && stats_yearly[i].period) { + YearInfo year(stats_yearly[i]); + years.append(QVariant::fromValue(year)); + i++; + } + + Grantlee::Context c; + c.insert("years", years); + c.insert("template_options", QVariant::fromValue(*templateOptions)); + c.insert("print_options", QVariant::fromValue(*PrintOptions)); + + Grantlee::Template t = m_engine->loadByName(PrintOptions->p_template); + if (!t || t->error()) { + qDebug() << "Can't load template"; + return htmlContent; + } + + htmlContent = t->render(&c); + + if (t->error()) { + qDebug() << "Can't render template"; + return htmlContent; + } + + emit progressUpdated(100); + return htmlContent; +} + +QString TemplateLayout::readTemplate(QString template_name) +{ + QFile qfile(getPrintingTemplatePathUser() + QDir::separator() + template_name); + if (qfile.open(QFile::ReadOnly | QFile::Text)) { + QTextStream in(&qfile); + return in.readAll(); + } + return ""; +} + +void TemplateLayout::writeTemplate(QString template_name, QString grantlee_template) +{ + QFile qfile(getPrintingTemplatePathUser() + QDir::separator() + template_name); + if (qfile.open(QFile::ReadWrite | QFile::Text)) { + qfile.write(grantlee_template.toUtf8().data()); + qfile.resize(qfile.pos()); + qfile.close(); + } +} + +YearInfo::YearInfo() +{ + +} + +YearInfo::~YearInfo() +{ + +} diff --git a/desktop-widgets/templatelayout.h b/desktop-widgets/templatelayout.h new file mode 100644 index 000000000..a2868e7ff --- /dev/null +++ b/desktop-widgets/templatelayout.h @@ -0,0 +1,168 @@ +#ifndef TEMPLATELAYOUT_H +#define TEMPLATELAYOUT_H + +#include <grantlee_templates.h> +#include "mainwindow.h" +#include "printoptions.h" +#include "statistics.h" +#include "qthelper.h" +#include "helpers.h" + +int getTotalWork(print_options *printOptions); +void find_all_templates(); + +extern QList<QString> grantlee_templates, grantlee_statistics_templates; + +class TemplateLayout : public QObject { + Q_OBJECT +public: + TemplateLayout(print_options *PrintOptions, template_options *templateOptions); + ~TemplateLayout(); + QString generate(); + QString generateStatistics(); + static QString readTemplate(QString template_name); + static void writeTemplate(QString template_name, QString grantlee_template); + +private: + Grantlee::Engine *m_engine; + print_options *PrintOptions; + template_options *templateOptions; + +signals: + void progressUpdated(int value); +}; + +class YearInfo { +public: + stats_t *year; + YearInfo(stats_t& year) + :year(&year) + { + + } + YearInfo(); + ~YearInfo(); +}; + +Q_DECLARE_METATYPE(Dive) +Q_DECLARE_METATYPE(template_options) +Q_DECLARE_METATYPE(print_options) +Q_DECLARE_METATYPE(YearInfo) + +GRANTLEE_BEGIN_LOOKUP(Dive) +if (property == "number") + return object.number(); +else if (property == "id") + return object.id(); +else if (property == "date") + return object.date(); +else if (property == "time") + return object.time(); +else if (property == "location") + return object.location(); +else if (property == "duration") + return object.duration(); +else if (property == "depth") + return object.depth(); +else if (property == "divemaster") + return object.divemaster(); +else if (property == "buddy") + return object.buddy(); +else if (property == "airTemp") + return object.airTemp(); +else if (property == "waterTemp") + return object.waterTemp(); +else if (property == "notes") + return object.notes(); +else if (property == "rating") + return object.rating(); +else if (property == "sac") + return object.sac(); +else if (property == "tags") + return object.tags(); +else if (property == "gas") + return object.gas(); +GRANTLEE_END_LOOKUP + +GRANTLEE_BEGIN_LOOKUP(template_options) +if (property == "font") { + switch (object.font_index) { + case 0: + return "Arial, Helvetica, sans-serif"; + case 1: + return "Impact, Charcoal, sans-serif"; + case 2: + return "Georgia, serif"; + case 3: + return "Courier, monospace"; + case 4: + return "Verdana, Geneva, sans-serif"; + } +} else if (property == "borderwidth") { + return object.border_width; +} else if (property == "font_size") { + return object.font_size / 9.0; +} else if (property == "line_spacing") { + return object.line_spacing; +} else if (property == "color1") { + return object.color_palette.color1.name(); +} else if (property == "color2") { + return object.color_palette.color2.name(); +} else if (property == "color3") { + return object.color_palette.color3.name(); +} else if (property == "color4") { + return object.color_palette.color4.name(); +} else if (property == "color5") { + return object.color_palette.color5.name(); +} else if (property == "color6") { + return object.color_palette.color6.name(); +} +GRANTLEE_END_LOOKUP + +GRANTLEE_BEGIN_LOOKUP(print_options) +if (property == "grayscale") { + if (object.color_selected) { + return ""; + } else { + return "-webkit-filter: grayscale(100%)"; + } +} +GRANTLEE_END_LOOKUP + +GRANTLEE_BEGIN_LOOKUP(YearInfo) +if (property == "year") { + return object.year->period; +} else if (property == "dives") { + return object.year->selection_size; +} else if (property == "min_temp") { + const char *unit; + double temp = get_temp_units(object.year->min_temp, &unit); + return object.year->min_temp == 0 ? "0" : QString::number(temp, 'g', 2) + unit; +} else if (property == "max_temp") { + const char *unit; + double temp = get_temp_units(object.year->max_temp, &unit); + return object.year->max_temp == 0 ? "0" : QString::number(temp, 'g', 2) + unit; +} else if (property == "total_time") { + return get_time_string(object.year->total_time.seconds, 0); +} else if (property == "avg_time") { + return get_minutes(object.year->total_time.seconds / object.year->selection_size); +} else if (property == "shortest_time") { + return get_minutes(object.year->shortest_time.seconds); +} else if (property == "longest_time") { + return get_minutes(object.year->longest_time.seconds); +} else if (property == "avg_depth") { + return get_depth_string(object.year->avg_depth); +} else if (property == "min_depth") { + return get_depth_string(object.year->min_depth); +} else if (property == "max_depth") { + return get_depth_string(object.year->max_depth); +} else if (property == "avg_sac") { + return get_volume_string(object.year->avg_sac); +} else if (property == "min_sac") { + return get_volume_string(object.year->min_sac); +} else if (property == "max_sac") { + return get_volume_string(object.year->max_sac); +} +GRANTLEE_END_LOOKUP + +#endif diff --git a/desktop-widgets/undocommands.cpp b/desktop-widgets/undocommands.cpp new file mode 100644 index 000000000..0fd182cb3 --- /dev/null +++ b/desktop-widgets/undocommands.cpp @@ -0,0 +1,156 @@ +#include "undocommands.h" +#include "mainwindow.h" +#include "divelist.h" + +UndoDeleteDive::UndoDeleteDive(QList<dive *> deletedDives) : diveList(deletedDives) +{ + setText("delete dive"); + if (diveList.count() > 1) + setText(QString("delete %1 dives").arg(QString::number(diveList.count()))); +} + +void UndoDeleteDive::undo() +{ + // first bring back the trip(s) + Q_FOREACH(struct dive_trip *trip, tripList) + insert_trip(&trip); + + // now walk the list of deleted dives + for (int i = 0; i < diveList.count(); i++) { + struct dive *d = diveList.at(i); + // we adjusted the divetrip to point to the "new" divetrip + if (d->divetrip) { + struct dive_trip *trip = d->divetrip; + tripflag_t tripflag = d->tripflag; // this gets overwritten in add_dive_to_trip() + d->divetrip = NULL; + d->next = NULL; + d->pprev = NULL; + add_dive_to_trip(d, trip); + d->tripflag = tripflag; + } + record_dive(diveList.at(i)); + } + mark_divelist_changed(true); + MainWindow::instance()->refreshDisplay(); +} + +void UndoDeleteDive::redo() +{ + QList<struct dive*> newList; + for (int i = 0; i < diveList.count(); i++) { + // make a copy of the dive before deleting it + struct dive* d = alloc_dive(); + copy_dive(diveList.at(i), d); + newList.append(d); + // check for trip - if this is the last dive in the trip + // the trip will get deleted, so we need to remember it as well + if (d->divetrip && d->divetrip->nrdives == 1) { + struct dive_trip *undo_trip = (struct dive_trip *)calloc(1, sizeof(struct dive_trip)); + *undo_trip = *d->divetrip; + undo_trip->location = copy_string(d->divetrip->location); + undo_trip->notes = copy_string(d->divetrip->notes); + undo_trip->nrdives = 0; + undo_trip->next = NULL; + undo_trip->dives = NULL; + // update all the dives who were in this trip to point to the copy of the + // trip that we are about to delete implicitly when deleting its last dive below + Q_FOREACH(struct dive *inner_dive, newList) + if (inner_dive->divetrip == d->divetrip) + inner_dive->divetrip = undo_trip; + d->divetrip = undo_trip; + tripList.append(undo_trip); + } + //delete the dive + delete_single_dive(get_divenr(diveList.at(i))); + } + mark_divelist_changed(true); + MainWindow::instance()->refreshDisplay(); + diveList.clear(); + diveList = newList; +} + + +UndoShiftTime::UndoShiftTime(QList<int> changedDives, int amount) + : diveList(changedDives), timeChanged(amount) +{ + setText("shift time"); +} + +void UndoShiftTime::undo() +{ + for (int i = 0; i < diveList.count(); i++) { + struct dive* d = get_dive_by_uniq_id(diveList.at(i)); + d->when -= timeChanged; + } + mark_divelist_changed(true); + MainWindow::instance()->refreshDisplay(); +} + +void UndoShiftTime::redo() +{ + for (int i = 0; i < diveList.count(); i++) { + struct dive* d = get_dive_by_uniq_id(diveList.at(i)); + d->when += timeChanged; + } + mark_divelist_changed(true); + MainWindow::instance()->refreshDisplay(); +} + + +UndoRenumberDives::UndoRenumberDives(QMap<int, QPair<int, int> > originalNumbers) +{ + oldNumbers = originalNumbers; + if (oldNumbers.count() > 1) + setText(QString("renumber %1 dives").arg(QString::number(oldNumbers.count()))); + else + setText("renumber dive"); +} + +void UndoRenumberDives::undo() +{ + foreach (int key, oldNumbers.keys()) { + struct dive* d = get_dive_by_uniq_id(key); + d->number = oldNumbers.value(key).first; + } + mark_divelist_changed(true); + MainWindow::instance()->refreshDisplay(); +} + +void UndoRenumberDives::redo() +{ + foreach (int key, oldNumbers.keys()) { + struct dive* d = get_dive_by_uniq_id(key); + d->number = oldNumbers.value(key).second; + } + mark_divelist_changed(true); + MainWindow::instance()->refreshDisplay(); +} + + +UndoRemoveDivesFromTrip::UndoRemoveDivesFromTrip(QMap<dive *, dive_trip *> removedDives) +{ + divesToUndo = removedDives; + setText("remove dive(s) from trip"); +} + +void UndoRemoveDivesFromTrip::undo() +{ + QMapIterator<dive*, dive_trip*> i(divesToUndo); + while (i.hasNext()) { + i.next(); + add_dive_to_trip(i.key (), i.value()); + } + mark_divelist_changed(true); + MainWindow::instance()->refreshDisplay(); +} + +void UndoRemoveDivesFromTrip::redo() +{ + QMapIterator<dive*, dive_trip*> i(divesToUndo); + while (i.hasNext()) { + i.next(); + remove_dive_from_trip(i.key(), false); + } + mark_divelist_changed(true); + MainWindow::instance()->refreshDisplay(); +} diff --git a/desktop-widgets/undocommands.h b/desktop-widgets/undocommands.h new file mode 100644 index 000000000..8e359db51 --- /dev/null +++ b/desktop-widgets/undocommands.h @@ -0,0 +1,50 @@ +#ifndef UNDOCOMMANDS_H +#define UNDOCOMMANDS_H + +#include <QUndoCommand> +#include <QMap> +#include "dive.h" + +class UndoDeleteDive : public QUndoCommand { +public: + UndoDeleteDive(QList<struct dive*> deletedDives); + virtual void undo(); + virtual void redo(); + +private: + QList<struct dive*> diveList; + QList<struct dive_trip*> tripList; +}; + +class UndoShiftTime : public QUndoCommand { +public: + UndoShiftTime(QList<int> changedDives, int amount); + virtual void undo(); + virtual void redo(); + +private: + QList<int> diveList; + int timeChanged; +}; + +class UndoRenumberDives : public QUndoCommand { +public: + UndoRenumberDives(QMap<int, QPair<int, int> > originalNumbers); + virtual void undo(); + virtual void redo(); + +private: + QMap<int,QPair<int, int> > oldNumbers; +}; + +class UndoRemoveDivesFromTrip : public QUndoCommand { +public: + UndoRemoveDivesFromTrip(QMap<struct dive*, dive_trip*> removedDives); + virtual void undo(); + virtual void redo(); + +private: + QMap<struct dive*, dive_trip*> divesToUndo; +}; + +#endif // UNDOCOMMANDS_H diff --git a/desktop-widgets/updatemanager.cpp b/desktop-widgets/updatemanager.cpp new file mode 100644 index 000000000..0760d6407 --- /dev/null +++ b/desktop-widgets/updatemanager.cpp @@ -0,0 +1,154 @@ +#include "updatemanager.h" +#include "helpers.h" +#include <QtNetwork> +#include <QMessageBox> +#include <QUuid> +#include "subsurfacewebservices.h" +#include "version.h" +#include "mainwindow.h" + +UpdateManager::UpdateManager(QObject *parent) : + QObject(parent), + isAutomaticCheck(false) +{ + // is this the first time this version was run? + QSettings settings; + settings.beginGroup("UpdateManager"); + if (settings.contains("DontCheckForUpdates") && settings.value("DontCheckForUpdates") == "TRUE") + return; + if (settings.contains("LastVersionUsed")) { + // we have checked at least once before + if (settings.value("LastVersionUsed").toString() != subsurface_git_version()) { + // we have just updated - wait two weeks before you check again + settings.setValue("LastVersionUsed", QString(subsurface_git_version())); + settings.setValue("NextCheck", QDateTime::currentDateTime().addDays(14).toString(Qt::ISODate)); + } else { + // is it time to check again? + QString nextCheckString = settings.value("NextCheck").toString(); + QDateTime nextCheck = QDateTime::fromString(nextCheckString, Qt::ISODate); + if (nextCheck > QDateTime::currentDateTime()) + return; + } + } + settings.setValue("LastVersionUsed", QString(subsurface_git_version())); + settings.setValue("NextCheck", QDateTime::currentDateTime().addDays(14).toString(Qt::ISODate)); + checkForUpdates(true); +} + +void UpdateManager::checkForUpdates(bool automatic) +{ + QString os; + +#if defined(Q_OS_WIN) + os = "win"; +#elif defined(Q_OS_MAC) + os = "osx"; +#elif defined(Q_OS_LINUX) + os = "linux"; +#else + os = "unknown"; +#endif + isAutomaticCheck = automatic; + QString version = subsurface_canonical_version(); + QString uuidString = getUUID(); + QString url = QString("http://subsurface-divelog.org/updatecheck.html?os=%1&version=%2&uuid=%3").arg(os, version, uuidString); + QNetworkRequest request; + request.setUrl(url); + request.setRawHeader("Accept", "text/xml"); + QString userAgent = getUserAgent(); + request.setRawHeader("User-Agent", userAgent.toUtf8()); + connect(SubsurfaceWebServices::manager()->get(request), SIGNAL(finished()), this, SLOT(requestReceived()), Qt::UniqueConnection); +} + +QString UpdateManager::getUUID() +{ + QString uuidString; + QSettings settings; + settings.beginGroup("UpdateManager"); + if (settings.contains("UUID")) { + uuidString = settings.value("UUID").toString(); + } else { + QUuid uuid = QUuid::createUuid(); + uuidString = uuid.toString(); + settings.setValue("UUID", uuidString); + } + uuidString.replace("{", "").replace("}", ""); + return uuidString; +} + +void UpdateManager::requestReceived() +{ + bool haveNewVersion = false; + QMessageBox msgbox; + QString msgTitle = tr("Check for updates."); + QString msgText = "<h3>" + tr("Subsurface was unable to check for updates.") + "</h3>"; + + QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender()); + if (reply->error() != QNetworkReply::NoError) { + //Network Error + msgText = msgText + "<br/><b>" + tr("The following error occurred:") + "</b><br/>" + reply->errorString() + + "<br/><br/><b>" + tr("Please check your internet connection.") + "</b>"; + } else { + //No network error + QString responseBody(reply->readAll()); + QString responseLink; + if (responseBody.contains('"')) + responseLink = responseBody.split("\"").at(1); + + msgbox.setIcon(QMessageBox::Information); + if (responseBody == "OK") { + msgText = tr("You are using the latest version of Subsurface."); + } else if (responseBody.startsWith("[\"http")) { + haveNewVersion = true; + msgText = tr("A new version of Subsurface is available.<br/>Click on:<br/><a href=\"%1\">%1</a><br/> to download it.") + .arg(responseLink); + } else if (responseBody.startsWith("Latest version")) { + // the webservice backend doesn't localize - but it's easy enough to just replace the + // strings that it is likely to send back + haveNewVersion = true; + msgText = QString("<b>") + tr("A new version of Subsurface is available.") + QString("</b><br/><br/>") + + tr("Latest version is %1, please check %2 our download page %3 for information in how to update.") + .arg(responseLink).arg("<a href=\"http://subsurface-divelog.org/download\">").arg("</a>"); + } else { + // the webservice backend doesn't localize - but it's easy enough to just replace the + // strings that it is likely to send back + if (!responseBody.contains("latest development") && + !responseBody.contains("newer") && + !responseBody.contains("beta", Qt::CaseInsensitive)) + haveNewVersion = true; + if (responseBody.contains("Newest release version is ")) + responseBody.replace("Newest release version is ", tr("Newest release version is ")); + msgText = tr("The server returned the following information:").append("<br/><br/>").append(responseBody); + msgbox.setIcon(QMessageBox::Warning); + } + } +#ifndef SUBSURFACE_MOBILE + if (haveNewVersion || !isAutomaticCheck) { + msgbox.setWindowTitle(msgTitle); + msgbox.setWindowIcon(QIcon(":/subsurface-icon")); + msgbox.setText(msgText); + msgbox.setTextFormat(Qt::RichText); + msgbox.exec(); + } + if (isAutomaticCheck) { + QSettings settings; + settings.beginGroup("UpdateManager"); + if (!settings.contains("DontCheckForUpdates")) { + // we allow an opt out of future checks + QMessageBox response(MainWindow::instance()); + QString message = tr("Subsurface is checking every two weeks if a new version is available. If you don't want Subsurface to continue checking, please click Decline."); + response.addButton(tr("Decline"), QMessageBox::RejectRole); + response.addButton(tr("Accept"), QMessageBox::AcceptRole); + response.setText(message); + response.setWindowTitle(tr("Automatic check for updates")); + response.setIcon(QMessageBox::Question); + response.setWindowModality(Qt::WindowModal); + int ret = response.exec(); + if (ret == QMessageBox::Accepted) + settings.setValue("DontCheckForUpdates", "FALSE"); + else + settings.setValue("DontCheckForUpdates", "TRUE"); + } + } +#endif +} diff --git a/desktop-widgets/updatemanager.h b/desktop-widgets/updatemanager.h new file mode 100644 index 000000000..f91c82dc8 --- /dev/null +++ b/desktop-widgets/updatemanager.h @@ -0,0 +1,24 @@ +#ifndef UPDATEMANAGER_H +#define UPDATEMANAGER_H + +#include <QObject> + +class QNetworkAccessManager; +class QNetworkReply; + +class UpdateManager : public QObject { + Q_OBJECT +public: + explicit UpdateManager(QObject *parent = 0); + void checkForUpdates(bool automatic = false); + static QString getUUID(); + +public +slots: + void requestReceived(); + +private: + bool isAutomaticCheck; +}; + +#endif // UPDATEMANAGER_H diff --git a/desktop-widgets/urldialog.ui b/desktop-widgets/urldialog.ui new file mode 100644 index 000000000..397f90a64 --- /dev/null +++ b/desktop-widgets/urldialog.ui @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>URLDialog</class> + <widget class="QDialog" name="URLDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>397</width> + <height>103</height> + </rect> + </property> + <property name="windowTitle"> + <string>Dialog</string> + </property> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="geometry"> + <rect> + <x>40</x> + <y>60</y> + <width>341</width> + <height>32</height> + </rect> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + <widget class="QPlainTextEdit" name="urlField"> + <property name="geometry"> + <rect> + <x>10</x> + <y>30</y> + <width>371</width> + <height>21</height> + </rect> + </property> + </widget> + <widget class="QLabel" name="label"> + <property name="geometry"> + <rect> + <x>10</x> + <y>10</y> + <width>151</width> + <height>16</height> + </rect> + </property> + <property name="text"> + <string>Enter URL for images</string> + </property> + </widget> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>URLDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>URLDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/desktop-widgets/usermanual.cpp b/desktop-widgets/usermanual.cpp new file mode 100644 index 000000000..6b676f16b --- /dev/null +++ b/desktop-widgets/usermanual.cpp @@ -0,0 +1,151 @@ +#include <QDesktopServices> +#include <QShortcut> +#include <QFile> + +#include "usermanual.h" +#include "mainwindow.h" +#include "helpers.h" + +SearchBar::SearchBar(QWidget *parent): QWidget(parent) +{ + ui.setupUi(this); + #if defined(Q_OS_MAC) || defined(Q_OS_WIN) + ui.findNext->setIcon(QIcon(":icons/subsurface/32x32/actions/go-down.png")); + ui.findPrev->setIcon(QIcon(":icons/subsurface/32x32/actions/go-up.png")); + ui.findClose->setIcon(QIcon(":icons/subsurface/32x32/actions/window-close.png")); + #endif + + connect(ui.findNext, SIGNAL(pressed()), this, SIGNAL(searchNext())); + connect(ui.findPrev, SIGNAL(pressed()), this, SIGNAL(searchPrev())); + connect(ui.searchEdit, SIGNAL(textChanged(QString)), this, SIGNAL(searchTextChanged(QString))); + connect(ui.searchEdit, SIGNAL(textChanged(QString)), this, SLOT(enableButtons(QString))); + connect(ui.findClose, SIGNAL(pressed()), this, SLOT(hide())); +} + +void SearchBar::setVisible(bool visible) +{ + QWidget::setVisible(visible); + ui.searchEdit->setFocus(); +} + +void SearchBar::enableButtons(const QString &s) +{ + ui.findPrev->setEnabled(s.length()); + ui.findNext->setEnabled(s.length()); +} + +UserManual::UserManual(QWidget *parent) : QWidget(parent) +{ + QShortcut *closeKey = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(closeKey, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quitKey = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quitKey, SIGNAL(activated()), qApp, SLOT(quit())); + + QAction *actionShowSearch = new QAction(this); + actionShowSearch->setShortcut(Qt::CTRL + Qt::Key_F); + actionShowSearch->setShortcutContext(Qt::WindowShortcut); + addAction(actionShowSearch); + + QAction *actionHideSearch = new QAction(this); + actionHideSearch->setShortcut(Qt::Key_Escape); + actionHideSearch->setShortcutContext(Qt::WindowShortcut); + addAction(actionHideSearch); + + setWindowTitle(tr("User manual")); + setWindowIcon(QIcon(":/subsurface-icon")); + + userManual = new QWebView(this); + QString colorBack = palette().highlight().color().name(QColor::HexRgb); + QString colorText = palette().highlightedText().color().name(QColor::HexRgb); + userManual->setStyleSheet(QString("QWebView { selection-background-color: %1; selection-color: %2; }") + .arg(colorBack).arg(colorText)); + userManual->page()->setLinkDelegationPolicy(QWebPage::DelegateExternalLinks); + QString searchPath = getSubsurfaceDataPath("Documentation"); + if (searchPath.size()) { + // look for localized versions of the manual first + QString lang = uiLanguage(NULL); + QString prefix = searchPath.append("/user-manual"); + QFile manual(prefix + "_" + lang + ".html"); + if (!manual.exists()) + manual.setFileName(prefix + "_" + lang.left(2) + ".html"); + if (!manual.exists()) + manual.setFileName(prefix + ".html"); + if (!manual.exists()) { + userManual->setHtml(tr("Cannot find the Subsurface manual")); + } else { + QString urlString = QString("file:///") + manual.fileName(); + userManual->setUrl(QUrl(urlString, QUrl::TolerantMode)); + } + } else { + userManual->setHtml(tr("Cannot find the Subsurface manual")); + } + + searchBar = new SearchBar(this); + searchBar->hide(); + connect(actionShowSearch, SIGNAL(triggered(bool)), searchBar, SLOT(show())); + connect(actionHideSearch, SIGNAL(triggered(bool)), searchBar, SLOT(hide())); + connect(userManual, SIGNAL(linkClicked(QUrl)), this, SLOT(linkClickedSlot(QUrl))); + connect(searchBar, SIGNAL(searchTextChanged(QString)), this, SLOT(searchTextChanged(QString))); + connect(searchBar, SIGNAL(searchNext()), this, SLOT(searchNext())); + connect(searchBar, SIGNAL(searchPrev()), this, SLOT(searchPrev())); + + QVBoxLayout *vboxLayout = new QVBoxLayout(); + userManual->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding); + vboxLayout->addWidget(userManual); + vboxLayout->addWidget(searchBar); + setLayout(vboxLayout); +} + +void UserManual::search(QString text, QWebPage::FindFlags flags = 0) +{ + if (userManual->findText(text, QWebPage::FindWrapsAroundDocument | flags) || text.length() == 0) { + searchBar->setStyleSheet(""); + } else { + searchBar->setStyleSheet("QLineEdit{background: red;}"); + } +} + +void UserManual::searchTextChanged(const QString& text) +{ + mLastText = text; + search(text); +} + +void UserManual::searchNext() +{ + search(mLastText); +} + +void UserManual::searchPrev() +{ + search(mLastText, QWebPage::FindBackward); +} + +void UserManual::linkClickedSlot(const QUrl& url) +{ + QDesktopServices::openUrl(url); +} + +#ifdef Q_OS_MAC +void UserManual::showEvent(QShowEvent *e) { + filterAction = NULL; + closeAction = NULL; + MainWindow *m = MainWindow::instance(); + Q_FOREACH (QObject *o, m->children()) { + if (o->objectName() == "actionFilterTags") { + filterAction = qobject_cast<QAction*>(o); + filterAction->setShortcut(QKeySequence()); + } else if (o->objectName() == "actionClose") { + closeAction = qobject_cast<QAction*>(o); + closeAction->setShortcut(QKeySequence()); + } + } +} +void UserManual::hideEvent(QHideEvent *e) { + if (closeAction != NULL) + closeAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_W)); + if (filterAction != NULL) + filterAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_F)); + closeAction = filterAction = NULL; +} +#endif diff --git a/desktop-widgets/usermanual.h b/desktop-widgets/usermanual.h new file mode 100644 index 000000000..5101a3c3b --- /dev/null +++ b/desktop-widgets/usermanual.h @@ -0,0 +1,50 @@ +#ifndef USERMANUAL_H +#define USERMANUAL_H + +#include <QWebView> + +#include "ui_searchbar.h" + +class SearchBar : public QWidget{ + Q_OBJECT +public: + SearchBar(QWidget *parent = 0); +signals: + void searchTextChanged(const QString& s); + void searchNext(); + void searchPrev(); +protected: + void setVisible(bool visible); +private slots: + void enableButtons(const QString& s); +private: + Ui::SearchBar ui; +}; + +class UserManual : public QWidget { + Q_OBJECT + +public: + explicit UserManual(QWidget *parent = 0); + +#ifdef Q_OS_MAC +protected: + void showEvent(QShowEvent *e); + void hideEvent(QHideEvent *e); + QAction *closeAction; + QAction *filterAction; +#endif + +private +slots: + void searchTextChanged(const QString& s); + void searchNext(); + void searchPrev(); + void linkClickedSlot(const QUrl& url); +private: + QWebView *userManual; + SearchBar *searchBar; + QString mLastText; + void search(QString, QWebPage::FindFlags); +}; +#endif // USERMANUAL_H diff --git a/desktop-widgets/usersurvey.cpp b/desktop-widgets/usersurvey.cpp new file mode 100644 index 000000000..05da582a1 --- /dev/null +++ b/desktop-widgets/usersurvey.cpp @@ -0,0 +1,133 @@ +#include <QShortcut> +#include <QMessageBox> +#include <QSettings> + +#include "usersurvey.h" +#include "ui_usersurvey.h" +#include "version.h" +#include "subsurfacewebservices.h" +#include "updatemanager.h" + +#include "helpers.h" +#include "subsurfacesysinfo.h" + +UserSurvey::UserSurvey(QWidget *parent) : QDialog(parent), + ui(new Ui::UserSurvey) +{ + ui->setupUi(this); + ui->buttonBox->buttons().first()->setText(tr("Send")); + this->adjustSize(); + QShortcut *closeKey = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); + connect(closeKey, SIGNAL(activated()), this, SLOT(close())); + QShortcut *quitKey = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q), this); + connect(quitKey, SIGNAL(activated()), parent, SLOT(close())); + + os = QString("ssrfVers=%1").arg(subsurface_version()); + os.append(QString("&prettyOsName=%1").arg(SubsurfaceSysInfo::prettyOsName())); + QString arch = SubsurfaceSysInfo::buildCpuArchitecture(); + os.append(QString("&appCpuArch=%1").arg(arch)); + if (arch == "i386") { + QString osArch = SubsurfaceSysInfo::currentCpuArchitecture(); + os.append(QString("&osCpuArch=%1").arg(osArch)); + } + os.append(QString("&uiLang=%1").arg(uiLanguage(NULL))); + os.append(QString("&uuid=%1").arg(UpdateManager::getUUID())); + ui->system->setPlainText(getVersion()); +} + +QString UserSurvey::getVersion() +{ + QString arch; + // fill in the system data + QString sysInfo = QString("Subsurface %1").arg(subsurface_version()); + sysInfo.append(tr("\nOperating system: %1").arg(SubsurfaceSysInfo::prettyOsName())); + arch = SubsurfaceSysInfo::buildCpuArchitecture(); + sysInfo.append(tr("\nCPU architecture: %1").arg(arch)); + if (arch == "i386") + sysInfo.append(tr("\nOS CPU architecture: %1").arg(SubsurfaceSysInfo::currentCpuArchitecture())); + sysInfo.append(tr("\nLanguage: %1").arg(uiLanguage(NULL))); + return sysInfo; +} + +UserSurvey::~UserSurvey() +{ + delete ui; +} + +#define ADD_OPTION(_name) values.append(ui->_name->isChecked() ? "&" #_name "=1" : "&" #_name "=0") + +void UserSurvey::on_buttonBox_accepted() +{ + // now we need to collect the data and submit it + QString values = os; + ADD_OPTION(recreational); + ADD_OPTION(tech); + ADD_OPTION(planning); + ADD_OPTION(download); + ADD_OPTION(divecomputer); + ADD_OPTION(manual); + ADD_OPTION(companion); + values.append(QString("&suggestion=%1").arg(ui->suggestions->toPlainText())); + UserSurveyServices uss(this); + connect(uss.sendSurvey(values), SIGNAL(finished()), SLOT(requestReceived())); + hide(); +} + +void UserSurvey::on_buttonBox_rejected() +{ + QMessageBox response(this); + response.setText(tr("Should we ask you later?")); + response.addButton(tr("Don't ask me again"), QMessageBox::RejectRole); + response.addButton(tr("Ask later"), QMessageBox::AcceptRole); + response.setWindowTitle(tr("Ask again?")); // Not displayed on MacOSX as described in Qt API + response.setIcon(QMessageBox::Question); + response.setWindowModality(Qt::WindowModal); + switch (response.exec()) { + case QDialog::Accepted: + // nothing to do here, we'll just ask again the next time they start + break; + case QDialog::Rejected: + QSettings s; + s.beginGroup("UserSurvey"); + s.setValue("SurveyDone", "declined"); + break; + } + hide(); +} + +void UserSurvey::requestReceived() +{ + QMessageBox msgbox; + QString msgTitle = tr("Submit user survey."); + QString msgText = "<h3>" + tr("Subsurface was unable to submit the user survey.") + "</h3>"; + + QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender()); + if (reply->error() != QNetworkReply::NoError) { + //Network Error + msgText = msgText + "<br/><b>" + tr("The following error occurred:") + "</b><br/>" + reply->errorString() + + "<br/><br/><b>" + tr("Please check your internet connection.") + "</b>"; + } else { + //No network error + QString response(reply->readAll()); + QString responseBody = response.split("\"").at(1); + + msgbox.setIcon(QMessageBox::Information); + + if (responseBody == "OK") { + msgText = tr("Survey successfully submitted."); + QSettings s; + s.beginGroup("UserSurvey"); + s.setValue("SurveyDone", "submitted"); + } else { + msgText = tr("There was an error while trying to check for updates.<br/><br/>%1").arg(responseBody); + msgbox.setIcon(QMessageBox::Warning); + } + } + + msgbox.setWindowTitle(msgTitle); + msgbox.setWindowIcon(QIcon(":/subsurface-icon")); + msgbox.setText(msgText); + msgbox.setTextFormat(Qt::RichText); + msgbox.exec(); + reply->deleteLater(); +} diff --git a/desktop-widgets/usersurvey.h b/desktop-widgets/usersurvey.h new file mode 100644 index 000000000..1dd5aaab3 --- /dev/null +++ b/desktop-widgets/usersurvey.h @@ -0,0 +1,30 @@ +#ifndef USERSURVEY_H +#define USERSURVEY_H + +#include <QDialog> +class QNetworkAccessManager; +class QNetworkReply; + +namespace Ui { + class UserSurvey; +} + +class UserSurvey : public QDialog { + Q_OBJECT + +public: + explicit UserSurvey(QWidget *parent = 0); + ~UserSurvey(); + static QString getVersion(); + +private +slots: + void on_buttonBox_accepted(); + void on_buttonBox_rejected(); + void requestReceived(); + +private: + Ui::UserSurvey *ui; + QString os; +}; +#endif // USERSURVEY_H diff --git a/desktop-widgets/usersurvey.ui b/desktop-widgets/usersurvey.ui new file mode 100644 index 000000000..c118fe89d --- /dev/null +++ b/desktop-widgets/usersurvey.ui @@ -0,0 +1,301 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>UserSurvey</class> + <widget class="QDialog" name="UserSurvey"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>538</width> + <height>714</height> + </rect> + </property> + <property name="windowTitle"> + <string>User survey</string> + </property> + <widget class="QLabel" name="label"> + <property name="geometry"> + <rect> + <x>180</x> + <y>10</y> + <width>241</width> + <height>21</height> + </rect> + </property> + <property name="font"> + <font> + <pointsize>11</pointsize> + </font> + </property> + <property name="text"> + <string>Subsurface user survey</string> + </property> + </widget> + <widget class="QLabel" name="label_2"> + <property name="geometry"> + <rect> + <x>9</x> + <y>36</y> + <width>521</width> + <height>91</height> + </rect> + </property> + <property name="text"> + <string><html><head/><body><p>We would love to learn more about our users, their preferences and their usage habits. Please spare a minute to fill out this form and submit it to the Subsurface team.</p></body></html></string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + <widget class="QCheckBox" name="tech"> + <property name="geometry"> + <rect> + <x>10</x> + <y>190</y> + <width>161</width> + <height>26</height> + </rect> + </property> + <property name="text"> + <string>Technical diver</string> + </property> + </widget> + <widget class="QCheckBox" name="recreational"> + <property name="geometry"> + <rect> + <x>180</x> + <y>190</y> + <width>181</width> + <height>26</height> + </rect> + </property> + <property name="text"> + <string>Recreational diver</string> + </property> + </widget> + <widget class="QCheckBox" name="planning"> + <property name="geometry"> + <rect> + <x>380</x> + <y>190</y> + <width>141</width> + <height>26</height> + </rect> + </property> + <property name="text"> + <string>Dive planner</string> + </property> + </widget> + <widget class="QCheckBox" name="download"> + <property name="geometry"> + <rect> + <x>10</x> + <y>270</y> + <width>481</width> + <height>26</height> + </rect> + </property> + <property name="text"> + <string>Supported dive computer</string> + </property> + </widget> + <widget class="QCheckBox" name="divecomputer"> + <property name="geometry"> + <rect> + <x>10</x> + <y>300</y> + <width>491</width> + <height>26</height> + </rect> + </property> + <property name="text"> + <string>Other software/sources</string> + </property> + </widget> + <widget class="QCheckBox" name="manual"> + <property name="geometry"> + <rect> + <x>10</x> + <y>330</y> + <width>501</width> + <height>26</height> + </rect> + </property> + <property name="text"> + <string>Manually entering dives</string> + </property> + </widget> + <widget class="QCheckBox" name="companion"> + <property name="geometry"> + <rect> + <x>10</x> + <y>360</y> + <width>491</width> + <height>26</height> + </rect> + </property> + <property name="text"> + <string>Android/iPhone companion app</string> + </property> + </widget> + <widget class="QLabel" name="label_3"> + <property name="geometry"> + <rect> + <x>9</x> + <y>410</y> + <width>501</width> + <height>21</height> + </rect> + </property> + <property name="text"> + <string>Any suggestions? (in English)</string> + </property> + </widget> + <widget class="QPlainTextEdit" name="suggestions"> + <property name="geometry"> + <rect> + <x>10</x> + <y>440</y> + <width>521</width> + <height>71</height> + </rect> + </property> + </widget> + <widget class="QLabel" name="label_4"> + <property name="geometry"> + <rect> + <x>9</x> + <y>520</y> + <width>511</width> + <height>51</height> + </rect> + </property> + <property name="text"> + <string>The following information about your system will also be submitted.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + <widget class="QPlainTextEdit" name="system"> + <property name="geometry"> + <rect> + <x>9</x> + <y>590</y> + <width>521</width> + <height>71</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="geometry"> + <rect> + <x>350</x> + <y>670</y> + <width>176</width> + <height>31</height> + </rect> + </property> + <property name="focusPolicy"> + <enum>Qt::TabFocus</enum> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set> + </property> + </widget> + <widget class="QLabel" name="label_5"> + <property name="geometry"> + <rect> + <x>10</x> + <y>160</y> + <width>451</width> + <height>21</height> + </rect> + </property> + <property name="text"> + <string>What kind of diver are you?</string> + </property> + </widget> + <widget class="Line" name="line"> + <property name="geometry"> + <rect> + <x>10</x> + <y>140</y> + <width>511</width> + <height>20</height> + </rect> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + <widget class="Line" name="line_2"> + <property name="geometry"> + <rect> + <x>10</x> + <y>220</y> + <width>511</width> + <height>20</height> + </rect> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + <widget class="QLabel" name="label_6"> + <property name="geometry"> + <rect> + <x>10</x> + <y>240</y> + <width>491</width> + <height>21</height> + </rect> + </property> + <property name="text"> + <string>Where are you importing data from?</string> + </property> + </widget> + <widget class="Line" name="line_3"> + <property name="geometry"> + <rect> + <x>10</x> + <y>390</y> + <width>511</width> + <height>20</height> + </rect> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </widget> + <tabstops> + <tabstop>tech</tabstop> + <tabstop>recreational</tabstop> + <tabstop>planning</tabstop> + <tabstop>download</tabstop> + <tabstop>divecomputer</tabstop> + <tabstop>manual</tabstop> + <tabstop>companion</tabstop> + <tabstop>suggestions</tabstop> + <tabstop>system</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui> diff --git a/desktop-widgets/webservices.ui b/desktop-widgets/webservices.ui new file mode 100644 index 000000000..bcca35f1e --- /dev/null +++ b/desktop-widgets/webservices.ui @@ -0,0 +1,156 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>WebServices</class> + <widget class="QDialog" name="WebServices"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>425</width> + <height>169</height> + </rect> + </property> + <property name="windowTitle"> + <string>Web service connection</string> + </property> + <property name="windowIcon"> + <iconset> + <normalon>:/subsurface-icon</normalon> + </iconset> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="3" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Status:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="userID"> + <property name="placeholderText"> + <string>Enter your ID here</string> + </property> + <property name="minimumSize"> + <size> + <width>420</width> + </size> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QPushButton" name="download"> + <property name="text"> + <string>Download</string> + </property> + </widget> + </item> + <item row="5" column="0" colspan="3"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Help</set> + </property> + </widget> + </item> + <item row="2" column="0" colspan="3"> + <widget class="QProgressBar" name="progressBar"> + <property name="value"> + <number>0</number> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>User ID</string> + </property> + </widget> + </item> + <item row="3" column="1" colspan="2"> + <widget class="QLabel" name="status"> + <property name="text"> + <string/> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item row="4" column="0" colspan="4"> + <widget class="QCheckBox" name="saveUidLocal"> + <property name="text"> + <string>Save user ID locally?</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="passLabel"> + <property name="text"> + <string>Password</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="password"> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QPushButton" name="upload"> + <property name="text"> + <string>Upload</string> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>userID</tabstop> + <tabstop>password</tabstop> + <tabstop>download</tabstop> + <tabstop>upload</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources> + <include location="../subsurface.qrc"/> + </resources> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>WebServices</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>WebServices</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> |