Improved Mac experience; QDoubleSpinBox for BitcoinAmountField
authorp2k <patrick.p2k.schneider@gmail.com>
Fri, 7 Oct 2011 11:21:45 +0000 (13:21 +0200)
committerWladimir J. van der Laan <laanwj@gmail.com>
Sun, 9 Oct 2011 19:19:44 +0000 (21:19 +0200)
Now it can't be told if this is was a Windows App before. All Mac design principles are fulfilled and some cosmetics have been applied to suit the native look and feel. The biggest change there is the proper use of the Dock icon which takes the role of the Tray icon on Mac.

The QDoubleSpinBox improves entering of Bitcoin amounts, no two separate fields are required anymore. All functionality and validation effects have been retained; pressing the comma key will be internally translated to a period to keep it consistent throughout the application and eases entering in countries which use the comma as decimal separator.

Additionally, Notificator now supports Growl, Mac's native notification system. This is provided via Apple Script in order to avoid linking to Growl on compile time. Other changes involve encapsulation of Toolbar and Menubar creation, loading of Qt's own translation and some clean up.

18 files changed:
bitcoin-qt.pro
src/qt/addressbookpage.cpp
src/qt/bitcoin.cpp
src/qt/bitcoinamountfield.cpp
src/qt/bitcoinamountfield.h
src/qt/bitcoingui.cpp
src/qt/bitcoingui.h
src/qt/forms/sendcoinsdialog.ui
src/qt/forms/sendcoinsentry.ui
src/qt/macdockiconhandler.h [new file with mode: 0644]
src/qt/macdockiconhandler.mm [new file with mode: 0644]
src/qt/notificator.cpp
src/qt/notificator.h
src/qt/optionsdialog.cpp
src/qt/overviewpage.cpp
src/qt/sendcoinsdialog.cpp
src/qt/sendcoinsentry.cpp
src/qt/transactionview.cpp

index 1f65d05..5b9d748 100644 (file)
@@ -221,6 +221,9 @@ windows:LIBS += -lws2_32 -lgdi32
 windows:DEFINES += WIN32
 windows:RC_FILE = src/qt/res/bitcoin-qt.rc
 
+macx:HEADERS += src/qt/macdockiconhandler.h
+macx:OBJECTIVE_SOURCES += src/qt/macdockiconhandler.mm
+macx:LIBS += -framework Foundation -framework ApplicationServices -framework AppKit
 macx:DEFINES += MAC_OSX MSG_NOSIGNAL=0 BOOST_FILESYSTEM_VERSION=3
 macx:ICON = src/qt/res/icons/bitcoin.icns
 macx:TARGET = "Bitcoin Qt"
index ee64cc2..6be59a0 100644 (file)
@@ -18,6 +18,13 @@ AddressBookPage::AddressBookPage(Mode mode, Tabs tab, QWidget *parent) :
     tab(tab)
 {
     ui->setupUi(this);
+
+#ifdef Q_WS_MAC // Icons on push buttons are very uncommon on Mac
+    ui->newAddressButton->setIcon(QIcon());
+    ui->copyToClipboard->setIcon(QIcon());
+    ui->deleteButton->setIcon(QIcon());
+#endif
+
     switch(mode)
     {
     case ForSending:
index 91881c3..c8e3324 100644 (file)
@@ -16,6 +16,7 @@
 #include <QLocale>
 #include <QTranslator>
 #include <QSplashScreen>
+#include <QLibraryInfo>
 
 // Need a global reference for the notifications to find the GUI
 BitcoinGUI *guiref;
@@ -119,11 +120,17 @@ int main(int argc, char *argv[])
 
     // Load language file for system locale
     QString locale = QLocale::system().name();
+    QTranslator qtTranslator;
+    qtTranslator.load(QLibraryInfo::location(QLibraryInfo::TranslationsPath) + "/qt_" + locale);
+    if (!qtTranslator.isEmpty())
+        app.installTranslator(&qtTranslator);
     QTranslator translator;
     translator.load(":/translations/"+locale);
     if (!translator.isEmpty())
         app.installTranslator(&translator);
 
+    app.setApplicationName(QApplication::translate("main", "Bitcoin Qt"));
+
     QSplashScreen splash(QPixmap(":/images/splash"), 0);
     splash.show();
     splash.setAutoFillBackground(true);
index f1edc62..19cd565 100644 (file)
@@ -1,33 +1,30 @@
 #include "bitcoinamountfield.h"
-#include "qvalidatedlineedit.h"
 #include "qvaluecombobox.h"
 #include "bitcoinunits.h"
 
+#include "guiconstants.h"
+
 #include <QLabel>
 #include <QLineEdit>
 #include <QRegExpValidator>
 #include <QHBoxLayout>
 #include <QKeyEvent>
+#include <QDoubleSpinBox>
 #include <QComboBox>
+#include <QApplication>
+#include <qmath.h>
 
 BitcoinAmountField::BitcoinAmountField(QWidget *parent):
-        QWidget(parent), amount(0), decimals(0), currentUnit(-1)
+        QWidget(parent), amount(0), currentUnit(-1)
 {
-    amount = new QValidatedLineEdit(this);
-    amount->setValidator(new QRegExpValidator(QRegExp("[0-9]*"), this));
-    amount->setAlignment(Qt::AlignRight|Qt::AlignVCenter);
+    amount = new QDoubleSpinBox(this);
+    amount->setLocale(QLocale::c());
+    amount->setDecimals(8);
     amount->installEventFilter(this);
-    amount->setMaximumWidth(75);
-    decimals = new QValidatedLineEdit(this);
-    decimals->setValidator(new QRegExpValidator(QRegExp("[0-9]+"), this));
-    decimals->setAlignment(Qt::AlignLeft|Qt::AlignVCenter);
-    decimals->setMaximumWidth(75);
+    amount->setMaximumWidth(170);
 
     QHBoxLayout *layout = new QHBoxLayout(this);
-    layout->setSpacing(0);
     layout->addWidget(amount);
-    layout->addWidget(new QLabel(QString("<b>.</b>")));
-    layout->addWidget(decimals);
     unit = new QValueComboBox(this);
     unit->setModel(new BitcoinUnits(this));
     layout->addWidget(unit);
@@ -40,8 +37,7 @@ BitcoinAmountField::BitcoinAmountField(QWidget *parent):
     setFocusProxy(amount);
 
     // If one if the widgets changes, the combined content changes as well
-    connect(amount, SIGNAL(textChanged(QString)), this, SIGNAL(textChanged()));
-    connect(decimals, SIGNAL(textChanged(QString)), this, SIGNAL(textChanged()));
+    connect(amount, SIGNAL(valueChanged(QString)), this, SIGNAL(textChanged()));
     connect(unit, SIGNAL(currentIndexChanged(int)), this, SLOT(unitChanged(int)));
 
     // Set default based on configuration
@@ -50,79 +46,72 @@ BitcoinAmountField::BitcoinAmountField(QWidget *parent):
 
 void BitcoinAmountField::setText(const QString &text)
 {
-    const QStringList parts = text.split(QString("."));
-    if(parts.size() == 2)
-    {
-        amount->setText(parts[0]);
-        decimals->setText(parts[1]);
-    }
+    if (text.isEmpty())
+        amount->clear();
     else
-    {
-        amount->setText(QString());
-        decimals->setText(QString());
-    }
+        amount->setValue(text.toDouble());
 }
 
 void BitcoinAmountField::clear()
 {
     amount->clear();
-    decimals->clear();
     unit->setCurrentIndex(0);
 }
 
 bool BitcoinAmountField::validate()
 {
     bool valid = true;
-    if(decimals->text().isEmpty())
-    {
-        decimals->setValid(false);
+    if (amount->value() == 0.0)
         valid = false;
-    }
-    if(!BitcoinUnits::parse(currentUnit, text(), 0))
-    {
-        setValid(false);
+    if (valid && !BitcoinUnits::parse(currentUnit, text(), 0))
         valid = false;
-    }
+
+    setValid(valid);
 
     return valid;
 }
 
 void BitcoinAmountField::setValid(bool valid)
 {
-    amount->setValid(valid);
-    decimals->setValid(valid);
+    if (valid)
+        amount->setStyleSheet("");
+    else
+        amount->setStyleSheet(STYLE_INVALID);
 }
 
 QString BitcoinAmountField::text() const
 {
-    if(decimals->text().isEmpty() && amount->text().isEmpty())
-    {
+    if (amount->text().isEmpty())
         return QString();
-    }
-    return amount->text() + QString(".") + decimals->text();
+    else
+        return amount->text();
 }
 
-// Intercept '.' and ',' keys, if pressed focus a specified widget
 bool BitcoinAmountField::eventFilter(QObject *object, QEvent *event)
 {
-    Q_UNUSED(object);
-    if(event->type() == QEvent::KeyPress)
+    if (event->type() == QEvent::FocusIn)
+    {
+        // Clear invalid flag on focus
+        setValid(true);
+    }
+    else if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease)
     {
         QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
-        if(keyEvent->key() == Qt::Key_Period || keyEvent->key() == Qt::Key_Comma)
+        if (keyEvent->key() == Qt::Key_Comma)
         {
-            decimals->setFocus();
-            decimals->selectAll();
+            // Translate a comma into a period
+            QKeyEvent periodKeyEvent(event->type(), Qt::Key_Period, keyEvent->modifiers(), ".", keyEvent->isAutoRepeat(), keyEvent->count());
+            qApp->sendEvent(object, &periodKeyEvent);
+            return true;
         }
     }
-    return false;
+    return QWidget::eventFilter(object, event);
 }
 
 QWidget *BitcoinAmountField::setupTabChain(QWidget *prev)
 {
     QWidget::setTabOrder(prev, amount);
-    QWidget::setTabOrder(amount, decimals);
-    return decimals;
+    return amount;
 }
 
 qint64 BitcoinAmountField::value(bool *valid_out) const
@@ -156,8 +145,8 @@ void BitcoinAmountField::unitChanged(int idx)
     currentUnit = newUnit;
 
     // Set max length after retrieving the value, to prevent truncation
-    amount->setMaxLength(BitcoinUnits::amountDigits(currentUnit));
-    decimals->setMaxLength(BitcoinUnits::decimals(currentUnit));
+    amount->setDecimals(BitcoinUnits::decimals(currentUnit));
+    amount->setMaximum(qPow(10, BitcoinUnits::amountDigits(currentUnit)) - qPow(10, -amount->decimals()));
 
     if(valid)
     {
index cc92159..8457a41 100644 (file)
@@ -4,7 +4,7 @@
 #include <QWidget>
 
 QT_BEGIN_NAMESPACE
-class QValidatedLineEdit;
+class QDoubleSpinBox;
 class QValueComboBox;
 QT_END_NAMESPACE
 
@@ -13,7 +13,7 @@ QT_END_NAMESPACE
 class BitcoinAmountField: public QWidget
 {
     Q_OBJECT
-    Q_PROPERTY(qint64 value READ value WRITE setValue NOTIFY textChanged USER true);
+    Q_PROPERTY(qint64 value READ value WRITE setValue NOTIFY textChanged USER true)
 public:
     explicit BitcoinAmountField(QWidget *parent = 0);
 
@@ -38,12 +38,11 @@ signals:
     void textChanged();
 
 protected:
-    // Intercept '.' and ',' keys, if pressed focus a specified widget
+    // Intercept focus-in event and ',' keypresses
     bool eventFilter(QObject *object, QEvent *event);
 
 private:
-    QValidatedLineEdit *amount;
-    QValidatedLineEdit *decimals;
+    QDoubleSpinBox *amount;
     QValueComboBox *unit;
     int currentUnit;
 
index 25261c7..beffa85 100644 (file)
 #include "askpassphrasedialog.h"
 #include "notificator.h"
 
+#ifdef Q_WS_MAC
+#include "macdockiconhandler.h"
+#endif
+
 #include <QApplication>
 #include <QMainWindow>
 #include <QMenuBar>
@@ -57,37 +61,26 @@ BitcoinGUI::BitcoinGUI(QWidget *parent):
 {
     resize(850, 550);
     setWindowTitle(tr("Bitcoin Wallet"));
+#ifndef Q_WS_MAC
     setWindowIcon(QIcon(":icons/bitcoin"));
+#else
+    setUnifiedTitleAndToolBarOnMac(true);
+    QApplication::setAttribute(Qt::AA_DontShowIconsInMenus);
+#endif
     // Accept D&D of URIs
     setAcceptDrops(true);
 
+    // Create actions for the toolbar, menu bar and tray/dock icon
     createActions();
 
-    // Menus
-    QMenu *file = menuBar()->addMenu(tr("&File"));
-    file->addAction(quitAction);
-    
-    QMenu *settings = menuBar()->addMenu(tr("&Settings"));
-    settings->addAction(encryptWalletAction);
-    settings->addAction(changePassphraseAction);
-    settings->addSeparator();
-    settings->addAction(optionsAction);
+    // Create application menu bar
+    createMenuBar();
 
-    QMenu *help = menuBar()->addMenu(tr("&Help"));
-    help->addAction(aboutAction);
-    
-    // Toolbars
-    QToolBar *toolbar = addToolBar(tr("Tabs toolbar"));
-    toolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
-    toolbar->addAction(overviewAction);
-    toolbar->addAction(sendCoinsAction);
-    toolbar->addAction(receiveCoinsAction);
-    toolbar->addAction(historyAction);
-    toolbar->addAction(addressBookAction);
+    // Create the toolbars
+    createToolBars();
 
-    QToolBar *toolbar2 = addToolBar(tr("Actions toolbar"));
-    toolbar2->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
-    toolbar2->addAction(exportAction);
+    // Create the tray icon (or setup the dock icon)
+    createTrayIcon();
 
     // Create tabs
     overviewPage = new OverviewPage();
@@ -146,8 +139,6 @@ BitcoinGUI::BitcoinGUI(QWidget *parent):
     statusBar()->addWidget(progressBar);
     statusBar()->addPermanentWidget(frameBlocks);
 
-    createTrayIcon();
-
     syncIconMovie = new QMovie(":/movies/update_spinner", "mng", this);
 
     // Clicking on a transaction on the overview page simply sends you to transaction history page
@@ -159,6 +150,13 @@ BitcoinGUI::BitcoinGUI(QWidget *parent):
     gotoOverviewPage();
 }
 
+BitcoinGUI::~BitcoinGUI()
+{
+#ifdef Q_WS_MAC
+    delete appMenuBar;
+#endif
+}
+
 void BitcoinGUI::createActions()
 {
     QActionGroup *tabGroup = new QActionGroup(this);
@@ -197,10 +195,13 @@ void BitcoinGUI::createActions()
     quitAction = new QAction(QIcon(":/icons/quit"), tr("E&xit"), this);
     quitAction->setToolTip(tr("Quit application"));
     quitAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q));
-    aboutAction = new QAction(QIcon(":/icons/bitcoin"), tr("&About"), this);
+    quitAction->setMenuRole(QAction::QuitRole);
+    aboutAction = new QAction(QIcon(":/icons/bitcoin"), tr("&About %1").arg(qApp->applicationName()), this);
     aboutAction->setToolTip(tr("Show information about Bitcoin"));
+    aboutAction->setMenuRole(QAction::AboutQtRole);
     optionsAction = new QAction(QIcon(":/icons/options"), tr("&Options..."), this);
     optionsAction->setToolTip(tr("Modify configuration options for bitcoin"));
+    optionsAction->setMenuRole(QAction::PreferencesRole);
     openBitcoinAction = new QAction(QIcon(":/icons/bitcoin"), tr("Open &Bitcoin"), this);
     openBitcoinAction->setToolTip(tr("Show the Bitcoin window"));
     exportAction = new QAction(QIcon(":/icons/export"), tr("&Export..."), this);
@@ -219,6 +220,45 @@ void BitcoinGUI::createActions()
     connect(changePassphraseAction, SIGNAL(triggered()), this, SLOT(changePassphrase()));
 }
 
+void BitcoinGUI::createMenuBar()
+{
+#ifdef Q_WS_MAC
+    // Create a decoupled menu bar on Mac which stays even if the window is closed
+    appMenuBar = new QMenuBar();
+#else
+    // Get the main window's menu bar on other platforms
+    appMenuBar = menuBar();
+#endif
+
+    // Configure the menus
+    QMenu *file = appMenuBar->addMenu(tr("&File"));
+    file->addAction(quitAction);
+
+    QMenu *settings = appMenuBar->addMenu(tr("&Settings"));
+    settings->addAction(encryptWalletAction);
+    settings->addAction(changePassphraseAction);
+    settings->addSeparator();
+    settings->addAction(optionsAction);
+
+    QMenu *help = appMenuBar->addMenu(tr("&Help"));
+    help->addAction(aboutAction);
+}
+
+void BitcoinGUI::createToolBars()
+{
+    QToolBar *toolbar = addToolBar(tr("Tabs toolbar"));
+    toolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+    toolbar->addAction(overviewAction);
+    toolbar->addAction(sendCoinsAction);
+    toolbar->addAction(receiveCoinsAction);
+    toolbar->addAction(historyAction);
+    toolbar->addAction(addressBookAction);
+
+    QToolBar *toolbar2 = addToolBar(tr("Actions toolbar"));
+    toolbar2->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+    toolbar2->addAction(exportAction);
+}
+
 void BitcoinGUI::setClientModel(ClientModel *clientModel)
 {
     this->clientModel = clientModel;
@@ -227,7 +267,11 @@ void BitcoinGUI::setClientModel(ClientModel *clientModel)
     {
         QString title_testnet = windowTitle() + QString(" ") + tr("[testnet]");
         setWindowTitle(title_testnet);
+#ifndef Q_WS_MAC
         setWindowIcon(QIcon(":icons/bitcoin_testnet"));
+#else
+        MacDockIconHandler::instance()->setIcon(QIcon(":icons/bitcoin_testnet"));
+#endif
         if(trayIcon)
         {
             trayIcon->setToolTip(title_testnet);
@@ -274,23 +318,39 @@ void BitcoinGUI::setWalletModel(WalletModel *walletModel)
 
 void BitcoinGUI::createTrayIcon()
 {
-    QMenu *trayIconMenu = new QMenu(this);
-    trayIconMenu->addAction(openBitcoinAction);
-    trayIconMenu->addAction(optionsAction);
-    trayIconMenu->addSeparator();
-    trayIconMenu->addAction(quitAction);
-
+    QMenu *trayIconMenu;
+#ifndef Q_WS_MAC
     trayIcon = new QSystemTrayIcon(this);
+    trayIconMenu = new QMenu(this);
     trayIcon->setContextMenu(trayIconMenu);
     trayIcon->setToolTip("Bitcoin client");
     trayIcon->setIcon(QIcon(":/icons/toolbar"));
     connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
             this, SLOT(trayIconActivated(QSystemTrayIcon::ActivationReason)));
     trayIcon->show();
+#else
+    // Note: On Mac, the dock icon is used to provide the tray's functionality.
+    MacDockIconHandler *dockIconHandler = MacDockIconHandler::instance();
+    connect(dockIconHandler, SIGNAL(dockIconClicked()), openBitcoinAction, SLOT(trigger()));
+    trayIconMenu = dockIconHandler->dockMenu();
+#endif
+
+    // Configuration of the tray icon (or dock icon) icon menu
+    trayIconMenu->addAction(openBitcoinAction);
+    trayIconMenu->addSeparator();
+    trayIconMenu->addAction(receiveCoinsAction);
+    trayIconMenu->addAction(sendCoinsAction);
+    trayIconMenu->addSeparator();
+    trayIconMenu->addAction(optionsAction);
+#ifndef Q_WS_MAC // This is built-in on Mac
+    trayIconMenu->addSeparator();
+    trayIconMenu->addAction(quitAction);
+#endif
 
     notificator = new Notificator(tr("bitcoin-qt"), trayIcon);
 }
 
+#ifndef Q_WS_MAC
 void BitcoinGUI::trayIconActivated(QSystemTrayIcon::ActivationReason reason)
 {
     if(reason == QSystemTrayIcon::Trigger)
@@ -300,6 +360,7 @@ void BitcoinGUI::trayIconActivated(QSystemTrayIcon::ActivationReason reason)
     }
 
 }
+#endif
 
 void BitcoinGUI::optionsClicked()
 {
@@ -403,9 +464,10 @@ void BitcoinGUI::error(const QString &title, const QString &message)
 
 void BitcoinGUI::changeEvent(QEvent *e)
 {
+#ifndef Q_WS_MAC // Ignored on Mac
     if (e->type() == QEvent::WindowStateChange)
     {
-        if(clientModel->getOptionsModel()->getMinimizeToTray())
+        if (clientModel->getOptionsModel()->getMinimizeToTray())
         {
             if (isMinimized())
             {
@@ -419,16 +481,19 @@ void BitcoinGUI::changeEvent(QEvent *e)
             }
         }
     }
+#endif
     QMainWindow::changeEvent(e);
 }
 
 void BitcoinGUI::closeEvent(QCloseEvent *event)
 {
+#ifndef Q_WS_MAC // Ignored on Mac
     if(!clientModel->getOptionsModel()->getMinimizeToTray() &&
        !clientModel->getOptionsModel()->getMinimizeOnClose())
     {
         qApp->quit();
     }
+#endif
     QMainWindow::closeEvent(event);
 }
 
@@ -480,6 +545,7 @@ void BitcoinGUI::incomingTransaction(const QModelIndex & parent, int start, int
 
 void BitcoinGUI::gotoOverviewPage()
 {
+    show();
     overviewAction->setChecked(true);
     centralWidget->setCurrentWidget(overviewPage);
 
@@ -489,6 +555,7 @@ void BitcoinGUI::gotoOverviewPage()
 
 void BitcoinGUI::gotoHistoryPage()
 {
+    show();
     historyAction->setChecked(true);
     centralWidget->setCurrentWidget(transactionsPage);
 
@@ -499,6 +566,7 @@ void BitcoinGUI::gotoHistoryPage()
 
 void BitcoinGUI::gotoAddressBookPage()
 {
+    show();
     addressBookAction->setChecked(true);
     centralWidget->setCurrentWidget(addressBookPage);
 
@@ -509,6 +577,7 @@ void BitcoinGUI::gotoAddressBookPage()
 
 void BitcoinGUI::gotoReceiveCoinsPage()
 {
+    show();
     receiveCoinsAction->setChecked(true);
     centralWidget->setCurrentWidget(receiveCoinsPage);
 
@@ -519,6 +588,7 @@ void BitcoinGUI::gotoReceiveCoinsPage()
 
 void BitcoinGUI::gotoSendCoinsPage()
 {
+    show();
     sendCoinsAction->setChecked(true);
     centralWidget->setCurrentWidget(sendCoinsPage);
 
index 5966135..a912192 100644 (file)
@@ -29,6 +29,8 @@ class BitcoinGUI : public QMainWindow
     Q_OBJECT
 public:
     explicit BitcoinGUI(QWidget *parent = 0);
+    ~BitcoinGUI();
+
     void setClientModel(ClientModel *clientModel);
     void setWalletModel(WalletModel *walletModel);
     
@@ -64,6 +66,7 @@ private:
     QLabel *progressBarLabel;
     QProgressBar *progressBar;
 
+    QMenuBar *appMenuBar;
     QAction *overviewAction;
     QAction *historyAction;
     QAction *quitAction;
@@ -84,6 +87,8 @@ private:
     QMovie *syncIconMovie;
 
     void createActions();
+    void createMenuBar();
+    void createToolBars();
     QWidget *createTabs();
     void createTrayIcon();
 
@@ -110,7 +115,9 @@ private slots:
     // Misc actions
     void optionsClicked();
     void aboutClicked();
+#ifndef Q_WS_MAC
     void trayIconActivated(QSystemTrayIcon::ActivationReason reason);
+#endif
     void incomingTransaction(const QModelIndex & parent, int start, int end);
     void encryptWallet(bool status);
     void changePassphrase();
index f9dd02f..e5e19e1 100644 (file)
@@ -58,9 +58,6 @@
    </item>
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout">
-     <property name="spacing">
-      <number>6</number>
-     </property>
      <item>
       <widget class="QPushButton" name="addButton">
        <property name="toolTip">
index 13593c2..0297d17 100644 (file)
@@ -83,7 +83,7 @@
     </widget>
    </item>
    <item row="3" column="1">
-    <layout class="QHBoxLayout" name="horizontalLayout_3">
+    <layout class="QHBoxLayout" name="payToLayout">
      <property name="spacing">
       <number>0</number>
      </property>
@@ -98,7 +98,7 @@
       </widget>
      </item>
      <item>
-      <widget class="QPushButton" name="addressBookButton">
+      <widget class="QToolButton" name="addressBookButton">
        <property name="toolTip">
         <string>Choose adress from address book</string>
        </property>
        <property name="shortcut">
         <string>Alt+A</string>
        </property>
-       <property name="autoDefault">
-        <bool>false</bool>
-       </property>
-       <property name="flat">
-        <bool>false</bool>
-       </property>
       </widget>
      </item>
      <item>
-      <widget class="QPushButton" name="pasteButton">
+      <widget class="QToolButton" name="pasteButton">
        <property name="toolTip">
         <string>Paste address from clipboard</string>
        </property>
        <property name="shortcut">
         <string>Alt+P</string>
        </property>
-       <property name="autoDefault">
-        <bool>false</bool>
-       </property>
       </widget>
      </item>
      <item>
-      <widget class="QPushButton" name="deleteButton">
+      <widget class="QToolButton" name="deleteButton">
        <property name="toolTip">
         <string>Remove this recipient</string>
        </property>
diff --git a/src/qt/macdockiconhandler.h b/src/qt/macdockiconhandler.h
new file mode 100644 (file)
index 0000000..cc78c7a
--- /dev/null
@@ -0,0 +1,37 @@
+#ifndef MACDOCKICONHANDLER_H
+#define MACDOCKICONHANDLER_H
+
+#include <QtCore/QObject>
+
+class QMenu;
+class QIcon;
+class QWidget;
+class objc_object;
+
+class MacDockIconHandler : public QObject
+{
+    Q_OBJECT
+public:
+    ~MacDockIconHandler();
+
+    QMenu *dockMenu();
+    void setIcon(const QIcon &icon);
+
+    static MacDockIconHandler *instance();
+
+    void handleDockIconClickEvent();
+
+signals:
+    void dockIconClicked();
+
+public slots:
+
+private:
+    MacDockIconHandler();
+
+    objc_object *m_dockIconClickEventHandler;
+    QWidget *m_dummyWidget;
+    QMenu *m_dockMenu;
+};
+
+#endif // MACDOCKICONCLICKHANDLER_H
diff --git a/src/qt/macdockiconhandler.mm b/src/qt/macdockiconhandler.mm
new file mode 100644 (file)
index 0000000..df56e69
--- /dev/null
@@ -0,0 +1,99 @@
+
+#include "macdockiconhandler.h"
+
+#include <QtGui/QMenu>
+#include <QtGui/QWidget>
+
+extern void qt_mac_set_dock_menu(QMenu*);
+
+#undef slots
+#include <Cocoa/Cocoa.h>
+
+@interface DockIconClickEventHandler : NSObject
+{
+    MacDockIconHandler* dockIconHandler;
+}
+
+@end
+
+@implementation DockIconClickEventHandler
+
+- (id)initWithDockIconHandler:(MacDockIconHandler *)aDockIconHandler
+{
+    self = [super init];
+    if (self) {
+        dockIconHandler = aDockIconHandler;
+
+        [[NSAppleEventManager sharedAppleEventManager]
+            setEventHandler:self
+                andSelector:@selector(handleDockClickEvent:withReplyEvent:)
+              forEventClass:kCoreEventClass
+                 andEventID:kAEReopenApplication];
+    }
+    return self;
+}
+
+- (void)handleDockClickEvent:(NSAppleEventDescriptor*)event withReplyEvent:(NSAppleEventDescriptor*)replyEvent
+{
+    Q_UNUSED(event)
+    Q_UNUSED(replyEvent)
+
+    if (dockIconHandler)
+        dockIconHandler->handleDockIconClickEvent();
+}
+
+@end
+
+MacDockIconHandler::MacDockIconHandler() : QObject()
+{
+    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+    this->m_dockIconClickEventHandler = [[DockIconClickEventHandler alloc] initWithDockIconHandler:this];
+
+    this->m_dummyWidget = new QWidget();
+    this->m_dockMenu = new QMenu(this->m_dummyWidget);
+    qt_mac_set_dock_menu(this->m_dockMenu);
+    [pool release];
+}
+
+MacDockIconHandler::~MacDockIconHandler()
+{
+    [this->m_dockIconClickEventHandler release];
+    delete this->m_dummyWidget;
+}
+
+QMenu *MacDockIconHandler::dockMenu()
+{
+    return this->m_dockMenu;
+}
+
+void MacDockIconHandler::setIcon(const QIcon &icon)
+{
+    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+    NSImage *image;
+    if (icon.isNull())
+        image = [[NSImage imageNamed:@"NSApplicationIcon"] retain];
+    else {
+        QSize size = icon.actualSize(QSize(128, 128));
+        QPixmap pixmap = icon.pixmap(size);
+        CGImageRef cgImage = pixmap.toMacCGImageRef();
+        image = [[NSImage alloc] initWithCGImage:cgImage size:NSZeroSize];
+        CFRelease(cgImage);
+    }
+
+    [NSApp setApplicationIconImage:image];
+    [image release];
+    [pool release];
+}
+
+MacDockIconHandler *MacDockIconHandler::instance()
+{
+    static MacDockIconHandler *s_instance = NULL;
+    if (!s_instance)
+        s_instance = new MacDockIconHandler();
+    return s_instance;
+}
+
+void MacDockIconHandler::handleDockIconClickEvent()
+{
+    emit this->dockIconClicked();
+}
index cf0c0a3..a2314ca 100644 (file)
@@ -8,12 +8,19 @@
 #include <QByteArray>
 #include <QSystemTrayIcon>
 #include <QMessageBox>
+#include <QTemporaryFile>
+#include <QImageWriter>
 
 #ifdef USE_DBUS
 #include <QtDBus/QtDBus>
 #include <stdint.h>
 #endif
 
+#ifdef Q_WS_MAC
+#include <ApplicationServices/ApplicationServices.h>
+extern bool qt_mac_execute_apple_script(const QString &script, AEDesc *ret);
+#endif
+
 // https://wiki.ubuntu.com/NotificationDevelopmentGuidelines recommends at least 128
 const int FREEDESKTOP_NOTIFICATION_ICON_SIZE = 128;
 
@@ -39,6 +46,19 @@ Notificator::Notificator(const QString &programName, QSystemTrayIcon *trayicon,
         mode = Freedesktop;
     }
 #endif
+#ifdef Q_WS_MAC
+    // Check if Growl is installed (based on Qt's tray icon implementation)
+    CFURLRef cfurl;
+    OSStatus status = LSGetApplicationForInfo(kLSUnknownType, kLSUnknownCreator, CFSTR("growlTicket"), kLSRolesAll, 0, &cfurl);
+    if (status != kLSApplicationNotFoundErr) {
+        CFBundleRef bundle = CFBundleCreate(0, cfurl);
+        CFRelease(cfurl);
+        if (CFStringCompare(CFBundleGetIdentifier(bundle), CFSTR("com.Growl.GrowlHelperApp"), kCFCompareCaseInsensitive | kCFCompareBackwards) == kCFCompareEqualTo) {
+            mode = Growl;
+        }
+        CFRelease(bundle);
+    }
+#endif
 }
 
 Notificator::~Notificator()
@@ -201,6 +221,54 @@ void Notificator::notifySystray(Class cls, const QString &title, const QString &
     trayIcon->showMessage(title, text, sicon, millisTimeout);
 }
 
+// Based on Qt's tray icon implementation
+#ifdef Q_WS_MAC
+void Notificator::notifyGrowl(Class cls, const QString &title, const QString &text, const QIcon &icon)
+{
+    const QString script(
+        "tell application \"GrowlHelperApp\"\n"
+        "  set the allNotificationsList to {\"Notification\"}\n" // -- Make a list of all the notification types (all)
+        "  set the enabledNotificationsList to {\"Notification\"}\n" // -- Make a list of the notifications (enabled)
+        "  register as application \"%1\" all notifications allNotificationsList default notifications enabledNotificationsList\n" // -- Register our script with Growl
+        "  notify with name \"Notification\" title \"%2\" description \"%3\" application name \"%1\"%4\n" // -- Send a Notification
+        "end tell"
+    );
+
+    QString notificationApp(QApplication::applicationName());
+    if (notificationApp.isEmpty())
+        notificationApp = "Application";
+
+    QPixmap notificationIconPixmap;
+    if (icon.isNull()) { // If no icon specified, set icon based on class
+        QStyle::StandardPixmap sicon = QStyle::SP_MessageBoxQuestion;
+        switch (cls)
+        {
+        case Information: sicon = QStyle::SP_MessageBoxInformation; break;
+        case Warning: sicon = QStyle::SP_MessageBoxWarning; break;
+        case Critical: sicon = QStyle::SP_MessageBoxCritical; break;
+        }
+        notificationIconPixmap = QApplication::style()->standardPixmap(sicon);
+    }
+    else {
+        QSize size = icon.actualSize(QSize(48, 48));
+        notificationIconPixmap = icon.pixmap(size);
+    }
+
+    QString notificationIcon;
+    QTemporaryFile notificationIconFile;
+    if (!notificationIconPixmap.isNull() && notificationIconFile.open()) {
+        QImageWriter writer(&notificationIconFile, "PNG");
+        if (writer.write(notificationIconPixmap.toImage()))
+            notificationIcon = QString(" image from location \"file://%1\"").arg(notificationIconFile.fileName());
+    }
+
+    QString quotedTitle(title), quotedText(text);
+    quotedTitle.replace("\\", "\\\\").replace("\"", "\\");
+    quotedText.replace("\\", "\\\\").replace("\"", "\\");
+    qt_mac_execute_apple_script(script.arg(notificationApp, quotedTitle, quotedText, notificationIcon), 0);
+}
+#endif
+
 void Notificator::notify(Class cls, const QString &title, const QString &text, const QIcon &icon, int millisTimeout)
 {
     switch(mode)
@@ -213,6 +281,11 @@ void Notificator::notify(Class cls, const QString &title, const QString &text, c
     case QSystemTray:
         notifySystray(cls, title, text, icon, millisTimeout);
         break;
+#ifdef Q_WS_MAC
+    case Growl:
+        notifyGrowl(cls, title, text, icon);
+        break;
+#endif
     default:
         if(cls == Critical)
         {
index 4217f7e..ed69ae5 100644 (file)
@@ -48,6 +48,7 @@ private:
         None,
         Freedesktop, // Use DBus org.freedesktop.Notifications
         QSystemTray, // Use QSystemTray::showMessage
+        Growl // Use the Growl notification system (Mac only)
     };
     QString programName;
     Mode mode;
@@ -58,6 +59,9 @@ private:
     void notifyDBus(Class cls, const QString &title, const QString &text, const QIcon &icon, int millisTimeout);
 #endif
     void notifySystray(Class cls, const QString &title, const QString &text, const QIcon &icon, int millisTimeout);
+#ifdef Q_WS_MAC
+    void notifyGrowl(Class cls, const QString &title, const QString &text, const QIcon &icon);
+#endif
 };
 
 #endif // NOTIFICATOR_H
index 7267e3d..ea3164e 100644 (file)
@@ -30,9 +30,13 @@ public:
     void setMapper(MonitoredDataMapper *mapper);
 private:
     QCheckBox *bitcoin_at_startup;
+#ifndef Q_WS_MAC
     QCheckBox *minimize_to_tray;
+#endif
     QCheckBox *map_port_upnp;
+#ifndef Q_WS_MAC
     QCheckBox *minimize_on_close;
+#endif
     QCheckBox *connect_socks4;
     QLineEdit *proxy_ip;
     QLineEdit *proxy_port;
@@ -167,17 +171,21 @@ MainOptionsPage::MainOptionsPage(QWidget *parent):
     bitcoin_at_startup->setToolTip(tr("Automatically start Bitcoin after the computer is turned on"));
     layout->addWidget(bitcoin_at_startup);
 
+#ifndef Q_WS_MAC
     minimize_to_tray = new QCheckBox(tr("&Minimize to the tray instead of the taskbar"));
     minimize_to_tray->setToolTip(tr("Show only a tray icon after minimizing the window"));
     layout->addWidget(minimize_to_tray);
+#endif
 
     map_port_upnp = new QCheckBox(tr("Map port using &UPnP"));
     map_port_upnp->setToolTip(tr("Automatically open the Bitcoin client port on the router. This only works when your router supports UPnP and it is enabled."));
     layout->addWidget(map_port_upnp);
 
+#ifndef Q_WS_MAC
     minimize_on_close = new QCheckBox(tr("M&inimize on close"));
     minimize_on_close->setToolTip(tr("Minimize instead of exit the application when the window is closed. When this option is enabled, the application will be closed only after selecting Quit in the menu."));
     layout->addWidget(minimize_on_close);
+#endif
 
     connect_socks4 = new QCheckBox(tr("&Connect through SOCKS4 proxy:"));
     connect_socks4->setToolTip(tr("Connect to the Bitcon network through a SOCKS4 proxy (e.g. when connecting through Tor)"));
@@ -239,9 +247,13 @@ void MainOptionsPage::setMapper(MonitoredDataMapper *mapper)
 {
     // Map model to widgets
     mapper->addMapping(bitcoin_at_startup, OptionsModel::StartAtStartup);
+#ifndef Q_WS_MAC
     mapper->addMapping(minimize_to_tray, OptionsModel::MinimizeToTray);
+#endif
     mapper->addMapping(map_port_upnp, OptionsModel::MapPortUPnP);
+#ifndef Q_WS_MAC
     mapper->addMapping(minimize_on_close, OptionsModel::MinimizeOnClose);
+#endif
     mapper->addMapping(connect_socks4, OptionsModel::ConnectSOCKS4);
     mapper->addMapping(proxy_ip, OptionsModel::ProxyIP);
     mapper->addMapping(proxy_port, OptionsModel::ProxyPort);
index f84a79f..6dedde0 100644 (file)
@@ -116,6 +116,7 @@ OverviewPage::OverviewPage(QWidget *parent) :
     ui->listTransactions->setIconSize(QSize(DECORATION_SIZE, DECORATION_SIZE));
     ui->listTransactions->setSelectionMode(QAbstractItemView::NoSelection);
     ui->listTransactions->setMinimumHeight(NUM_ITEMS * (DECORATION_SIZE + 2));
+    ui->listTransactions->setAttribute(Qt::WA_MacShowFocusRect, false);
 
     connect(ui->listTransactions, SIGNAL(clicked(QModelIndex)), this, SIGNAL(transactionClicked(QModelIndex)));
 }
index 58eb5c2..719cc51 100644 (file)
@@ -19,6 +19,12 @@ SendCoinsDialog::SendCoinsDialog(QWidget *parent) :
 {
     ui->setupUi(this);
 
+#ifdef Q_WS_MAC // Icons on push buttons are very uncommon on Mac
+    ui->addButton->setIcon(QIcon());
+    ui->clearButton->setIcon(QIcon());
+    ui->sendButton->setIcon(QIcon());
+#endif
+
     addEntry();
 
     connect(ui->addButton, SIGNAL(clicked()), this, SLOT(addEntry()));
index fccef23..1802095 100644 (file)
@@ -17,6 +17,10 @@ SendCoinsEntry::SendCoinsEntry(QWidget *parent) :
 {
     ui->setupUi(this);
 
+#ifdef Q_WS_MAC
+    ui->payToLayout->setSpacing(4);
+#endif
+
 #if QT_VERSION >= 0x040700
     ui->payTo->setPlaceholderText(tr("Enter a Bitcoin address (e.g. 1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L)"));
     ui->addAsLabel->setPlaceholderText(tr("Enter a label for this address to add it to your address book"));
index b2777b7..92dda57 100644 (file)
@@ -38,13 +38,20 @@ TransactionView::TransactionView(QWidget *parent) :
 
     QHBoxLayout *hlayout = new QHBoxLayout();
     hlayout->setContentsMargins(0,0,0,0);
+#ifdef Q_WS_MAC
+    hlayout->setSpacing(5);
+    hlayout->addSpacing(26);
+#else
     hlayout->setSpacing(0);
-
     hlayout->addSpacing(23);
+#endif
 
     dateWidget = new QComboBox(this);
-    dateWidget->setMaximumWidth(120);
-    dateWidget->setMinimumWidth(120);
+#ifdef Q_WS_MAC
+    dateWidget->setFixedWidth(121);
+#else
+    dateWidget->setFixedWidth(120);
+#endif
     dateWidget->addItem(tr("All"), All);
     dateWidget->addItem(tr("Today"), Today);
     dateWidget->addItem(tr("This week"), ThisWeek);
@@ -55,8 +62,11 @@ TransactionView::TransactionView(QWidget *parent) :
     hlayout->addWidget(dateWidget);
 
     typeWidget = new QComboBox(this);
-    typeWidget->setMaximumWidth(120);
-    typeWidget->setMinimumWidth(120);
+#ifdef Q_WS_MAC
+    typeWidget->setFixedWidth(121);
+#else
+    typeWidget->setFixedWidth(120);
+#endif
 
     typeWidget->addItem(tr("All"), TransactionFilterProxy::ALL_TYPES);
     typeWidget->addItem(tr("Received with"), TransactionFilterProxy::TYPE(TransactionRecord::RecvWithAddress) |
@@ -79,8 +89,11 @@ TransactionView::TransactionView(QWidget *parent) :
 #if QT_VERSION >= 0x040700
     amountWidget->setPlaceholderText(tr("Min amount"));
 #endif
-    amountWidget->setMaximumWidth(100);
-    amountWidget->setMinimumWidth(100);
+#ifdef Q_WS_MAC
+    amountWidget->setFixedWidth(97);
+#else
+    amountWidget->setFixedWidth(100);
+#endif
     amountWidget->setValidator(new QDoubleValidator(0, 1e20, 8, this));
     hlayout->addWidget(amountWidget);
 
@@ -96,7 +109,11 @@ TransactionView::TransactionView(QWidget *parent) :
     vlayout->setSpacing(0);
     int width = view->verticalScrollBar()->sizeHint().width();
     // Cover scroll bar width with spacing
+#ifdef Q_WS_MAC
+    hlayout->addSpacing(width+2);
+#else
     hlayout->addSpacing(width);
+#endif
     // Always show scroll bar
     view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
     view->setTabKeyNavigation(false);