diff --git a/client/client.pro b/client/client.pro index 93b43650..ff423075 100644 --- a/client/client.pro +++ b/client/client.pro @@ -66,13 +66,18 @@ win32 { OTHER_FILES += platform_win/vpnclient.rc RC_FILE = platform_win/vpnclient.rc - HEADERS += - SOURCES += + HEADERS += \ + ui/framelesswindow.h \ + + SOURCES += \ + ui/framelesswindow.cpp VERSION = 1.0.0.0 QMAKE_TARGET_COMPANY = "AmneziaVPN" QMAKE_TARGET_PRODUCT = "AmneziaVPN" + + LIBS += \ -luser32 \ -lrasapi32 \ @@ -87,4 +92,9 @@ win32 { macx { ICON = $$PWD/images/app.icns + + HEADERS += ui/macos_util.h + SOURCES += ui/macos_util.mm + + LIBS += -framework Cocoa } diff --git a/client/ui/framelesswindow.cpp b/client/ui/framelesswindow.cpp new file mode 100644 index 00000000..612f2e01 --- /dev/null +++ b/client/ui/framelesswindow.cpp @@ -0,0 +1,306 @@ +// This code is a part of Qt-Nice-Frameless-Window +// https://github.com/Bringer-of-Light/Qt-Nice-Frameless-Window +// Licensed by MIT License - https://github.com/Bringer-of-Light/Qt-Nice-Frameless-Window/blob/master/LICENSE + + +#include "framelesswindow.h" +#include +#include +#include +#ifdef Q_OS_WIN + +#include +#include +#include +#include +#include // Fixes error C2504: 'IUnknown' : base class undefined +#include +#include +#pragma comment (lib,"Dwmapi.lib") // Adds missing library, fixes error LNK2019: unresolved external symbol __imp__DwmExtendFrameIntoClientArea +#pragma comment (lib,"user32.lib") + +CFramelessWindow::CFramelessWindow(QWidget *parent) + : QMainWindow(parent), + m_titlebar(Q_NULLPTR), + m_borderWidth(5), + m_bJustMaximized(false), + m_bResizeable(true) +{ +// setWindowFlag(Qt::Window,true); +// setWindowFlag(Qt::FramelessWindowHint, true); +// setWindowFlag(Qt::WindowSystemMenuHint, true); +// setWindowFlag() is not avaliable before Qt v5.9, so we should use setWindowFlags instead + + setWindowFlags(windowFlags() | Qt::Window | Qt::FramelessWindowHint | Qt::WindowSystemMenuHint); + + setResizeable(m_bResizeable); +} + +void CFramelessWindow::setResizeable(bool resizeable) +{ + bool visible = isVisible(); + m_bResizeable = resizeable; + if (m_bResizeable){ + setWindowFlags(windowFlags() | Qt::WindowMaximizeButtonHint); +// setWindowFlag(Qt::WindowMaximizeButtonHint); + + //此行代码可以带回Aero效果,同时也带回了标题栏和边框,在nativeEvent()会再次去掉标题栏 + // + //this line will get titlebar/thick frame/Aero back, which is exactly what we want + //we will get rid of titlebar and thick frame again in nativeEvent() later + HWND hwnd = (HWND)this->winId(); + DWORD style = ::GetWindowLong(hwnd, GWL_STYLE); + ::SetWindowLong(hwnd, GWL_STYLE, style | WS_MAXIMIZEBOX | WS_THICKFRAME | WS_CAPTION); + }else{ + setWindowFlags(windowFlags() & ~Qt::WindowMaximizeButtonHint); +// setWindowFlag(Qt::WindowMaximizeButtonHint,false); + + HWND hwnd = (HWND)this->winId(); + DWORD style = ::GetWindowLong(hwnd, GWL_STYLE); + ::SetWindowLong(hwnd, GWL_STYLE, style & ~WS_MAXIMIZEBOX & ~WS_CAPTION); + } + + //保留一个像素的边框宽度,否则系统不会绘制边框阴影 + // + //we better left 1 piexl width of border untouch, so OS can draw nice shadow around it + const MARGINS shadow = { 1, 1, 1, 1 }; + DwmExtendFrameIntoClientArea(HWND(winId()), &shadow); + + setVisible(visible); +} + +void CFramelessWindow::setResizeableAreaWidth(int width) +{ + if (1 > width) width = 1; + m_borderWidth = width; +} + +void CFramelessWindow::setTitleBar(QWidget* titlebar) +{ + m_titlebar = titlebar; + if (!titlebar) return; + connect(titlebar, SIGNAL(destroyed(QObject*)), this, SLOT(onTitleBarDestroyed())); +} + +void CFramelessWindow::onTitleBarDestroyed() +{ + if (m_titlebar == QObject::sender()) + { + m_titlebar = Q_NULLPTR; + } +} + +void CFramelessWindow::addIgnoreWidget(QWidget* widget) +{ + if (!widget) return; + if (m_whiteList.contains(widget)) return; + m_whiteList.append(widget); +} + +bool CFramelessWindow::nativeEvent(const QByteArray &eventType, void *message, long *result) +{ + //Workaround for known bug -> check Qt forum : https://forum.qt.io/topic/93141/qtablewidget-itemselectionchanged/13 + #if (QT_VERSION == QT_VERSION_CHECK(5, 11, 1)) + MSG* msg = *reinterpret_cast(message); + #else + MSG* msg = reinterpret_cast(message); + #endif + + switch (msg->message) + { + case WM_NCCALCSIZE: + { + NCCALCSIZE_PARAMS& params = *reinterpret_cast(msg->lParam); + if (params.rgrc[0].top != 0) + params.rgrc[0].top -= 1; + + //this kills the window frame and title bar we added with WS_THICKFRAME and WS_CAPTION + *result = WVR_REDRAW; + return true; + } + case WM_NCHITTEST: + { + *result = 0; + + const LONG border_width = m_borderWidth; + RECT winrect; + GetWindowRect(HWND(winId()), &winrect); + + long x = GET_X_LPARAM(msg->lParam); + long y = GET_Y_LPARAM(msg->lParam); + + if(m_bResizeable) + { + + bool resizeWidth = minimumWidth() != maximumWidth(); + bool resizeHeight = minimumHeight() != maximumHeight(); + + if(resizeWidth) + { + //left border + if (x >= winrect.left && x < winrect.left + border_width) + { + *result = HTLEFT; + } + //right border + if (x < winrect.right && x >= winrect.right - border_width) + { + *result = HTRIGHT; + } + } + if(resizeHeight) + { + //bottom border + if (y < winrect.bottom && y >= winrect.bottom - border_width) + { + *result = HTBOTTOM; + } + //top border + if (y >= winrect.top && y < winrect.top + border_width) + { + *result = HTTOP; + } + } + if(resizeWidth && resizeHeight) + { + //bottom left corner + if (x >= winrect.left && x < winrect.left + border_width && + y < winrect.bottom && y >= winrect.bottom - border_width) + { + *result = HTBOTTOMLEFT; + } + //bottom right corner + if (x < winrect.right && x >= winrect.right - border_width && + y < winrect.bottom && y >= winrect.bottom - border_width) + { + *result = HTBOTTOMRIGHT; + } + //top left corner + if (x >= winrect.left && x < winrect.left + border_width && + y >= winrect.top && y < winrect.top + border_width) + { + *result = HTTOPLEFT; + } + //top right corner + if (x < winrect.right && x >= winrect.right - border_width && + y >= winrect.top && y < winrect.top + border_width) + { + *result = HTTOPRIGHT; + } + } + } + if (0!=*result) return true; + + //*result still equals 0, that means the cursor locate OUTSIDE the frame area + //but it may locate in titlebar area + if (!m_titlebar) return false; + + //support highdpi + double dpr = this->devicePixelRatioF(); + QPoint pos = m_titlebar->mapFromGlobal(QPoint(x/dpr,y/dpr)); + + if (!m_titlebar->rect().contains(pos)) return false; + QWidget* child = m_titlebar->childAt(pos); + if (!child) + { + *result = HTCAPTION; + return true; + }else{ + if (m_whiteList.contains(child)) + { + *result = HTCAPTION; + return true; + } + } + return false; + } //end case WM_NCHITTEST + case WM_GETMINMAXINFO: + { + if (::IsZoomed(msg->hwnd)) { + RECT frame = { 0, 0, 0, 0 }; + AdjustWindowRectEx(&frame, WS_OVERLAPPEDWINDOW, FALSE, 0); + + //record frame area data + double dpr = this->devicePixelRatioF(); + + m_frames.setLeft(abs(frame.left)/dpr+0.5); + m_frames.setTop(abs(frame.bottom)/dpr+0.5); + m_frames.setRight(abs(frame.right)/dpr+0.5); + m_frames.setBottom(abs(frame.bottom)/dpr+0.5); + + QMainWindow::setContentsMargins(m_frames.left()+m_margins.left(), \ + m_frames.top()+m_margins.top(), \ + m_frames.right()+m_margins.right(), \ + m_frames.bottom()+m_margins.bottom()); + m_bJustMaximized = true; + }else { + if (m_bJustMaximized) + { + QMainWindow::setContentsMargins(m_margins); + m_frames = QMargins(); + m_bJustMaximized = false; + } + } + return false; + } + default: + return QMainWindow::nativeEvent(eventType, message, result); + } +} + +void CFramelessWindow::setContentsMargins(const QMargins &margins) +{ + QMainWindow::setContentsMargins(margins+m_frames); + m_margins = margins; +} +void CFramelessWindow::setContentsMargins(int left, int top, int right, int bottom) +{ + QMainWindow::setContentsMargins(left+m_frames.left(),\ + top+m_frames.top(), \ + right+m_frames.right(), \ + bottom+m_frames.bottom()); + m_margins.setLeft(left); + m_margins.setTop(top); + m_margins.setRight(right); + m_margins.setBottom(bottom); +} +QMargins CFramelessWindow::contentsMargins() const +{ + QMargins margins = QMainWindow::contentsMargins(); + margins -= m_frames; + return margins; +} +void CFramelessWindow::getContentsMargins(int *left, int *top, int *right, int *bottom) const +{ + QMainWindow::getContentsMargins(left,top,right,bottom); + if (!(left&&top&&right&&bottom)) return; + if (isMaximized()) + { + *left -= m_frames.left(); + *top -= m_frames.top(); + *right -= m_frames.right(); + *bottom -= m_frames.bottom(); + } +} +QRect CFramelessWindow::contentsRect() const +{ + QRect rect = QMainWindow::contentsRect(); + int width = rect.width(); + int height = rect.height(); + rect.setLeft(rect.left() - m_frames.left()); + rect.setTop(rect.top() - m_frames.top()); + rect.setWidth(width); + rect.setHeight(height); + return rect; +} +void CFramelessWindow::showFullScreen() +{ + if (isMaximized()) + { + QMainWindow::setContentsMargins(m_margins); + m_frames = QMargins(); + } + QMainWindow::showFullScreen(); +} + +#endif //Q_OS_WIN diff --git a/client/ui/framelesswindow.h b/client/ui/framelesswindow.h new file mode 100644 index 00000000..ca8a69f6 --- /dev/null +++ b/client/ui/framelesswindow.h @@ -0,0 +1,158 @@ +// This code is a part of Qt-Nice-Frameless-Window +// https://github.com/Bringer-of-Light/Qt-Nice-Frameless-Window +// Licensed by MIT License - https://github.com/Bringer-of-Light/Qt-Nice-Frameless-Window/blob/master/LICENSE + + +#ifndef CFRAMELESSWINDOW_H +#define CFRAMELESSWINDOW_H +#include "qsystemdetection.h" +#include +#include + +//A nice frameless window for both Windows and OS X +//Author: Bringer-of-Light +//Github: https://github.com/Bringer-of-Light/Qt-Nice-Frameless-Window +// Usage: use "CFramelessWindow" as base class instead of "QMainWindow", and enjoy +#ifdef Q_OS_WIN +#include +#include +#include +#include +class CFramelessWindow : public QMainWindow +{ + Q_OBJECT +public: + explicit CFramelessWindow(QWidget *parent = 0); +public: + + //设置是否可以通过鼠标调整窗口大小 + //if resizeable is set to false, then the window can not be resized by mouse + //but still can be resized programtically + void setResizeable(bool resizeable=true); + bool isResizeable(){return m_bResizeable;} + + //设置可调整大小区域的宽度,在此区域内,可以使用鼠标调整窗口大小 + //set border width, inside this aera, window can be resized by mouse + void setResizeableAreaWidth(int width = 5); +protected: + //设置一个标题栏widget,此widget会被当做标题栏对待 + //set a widget which will be treat as SYSTEM titlebar + void setTitleBar(QWidget* titlebar); + + //在标题栏控件内,也可以有子控件如标签控件“label1”,此label1遮盖了标题栏,导致不能通过label1拖动窗口 + //要解决此问题,使用addIgnoreWidget(label1) + //generally, we can add widget say "label1" on titlebar, and it will cover the titlebar under it + //as a result, we can not drag and move the MainWindow with this "label1" again + //we can fix this by add "label1" to a ignorelist, just call addIgnoreWidget(label1) + void addIgnoreWidget(QWidget* widget); + + bool nativeEvent(const QByteArray &eventType, void *message, long *result); +private slots: + void onTitleBarDestroyed(); +public: + void setContentsMargins(const QMargins &margins); + void setContentsMargins(int left, int top, int right, int bottom); + QMargins contentsMargins() const; + QRect contentsRect() const; + void getContentsMargins(int *left, int *top, int *right, int *bottom) const; +public slots: + void showFullScreen(); +private: + QWidget* m_titlebar; + QList m_whiteList; + int m_borderWidth; + + QMargins m_margins; + QMargins m_frames; + bool m_bJustMaximized; + + bool m_bResizeable; +}; + +#elif defined Q_OS_MAC +#include +#include +#include +class CFramelessWindow : public QMainWindow +{ + Q_OBJECT +public: + explicit CFramelessWindow(QWidget *parent = 0); +private: + void initUI(); +public: + //设置可拖动区域的高度,在此区域内,可以通过鼠标拖动窗口, 0表示整个窗口都可拖动 + //In draggable area, window can be moved by mouse, (height = 0) means that the whole window is draggable + void setDraggableAreaHeight(int height = 0); + + //只有OS X10.10及以后系统,才支持OS X原生样式包括:三个系统按钮、窗口圆角、窗口阴影 + //类初始化完成后,可以通过此函数查看是否已经启用了原生样式。如果未启动,需要自定义关闭按钮、最小化按钮、最大化按钮 + //Native style(three system button/ round corner/ drop shadow) works only on OS X 10.10 or later + //after init, we should check whether NativeStyle is OK with this function + //if NOT ok, we should implement close button/ min button/ max button ourself + bool isNativeStyleOK() {return m_bNativeSystemBtn;} + + //如果设置setCloseBtnQuit(false),那么点击关闭按钮后,程序不会退出,而是会隐藏,只有在OS X 10.10 及以后系统中有效 + //if setCloseBtnQuit(false), then when close button is clicked, the application will hide itself instead of quit + //be carefull, after you set this to false, you can NOT change it to true again + //this function should be called inside of the constructor function of derived classes, and can NOT be called more than once + //only works for OS X 10.10 or later + void setCloseBtnQuit(bool bQuit = true); + + //启用或禁用关闭按钮,只有在isNativeStyleOK()返回true的情况下才有效 + //enable or disable Close button, only worked if isNativeStyleOK() returns true + void setCloseBtnEnabled(bool bEnable = true); + + //启用或禁用最小化按钮,只有在isNativeStyleOK()返回true的情况下才有效 + //enable or disable Miniaturize button, only worked if isNativeStyleOK() returns true + void setMinBtnEnabled(bool bEnable = true); + + //启用或禁用zoom(最大化)按钮,只有在isNativeStyleOK()返回true的情况下才有效 + //enable or disable Zoom button(fullscreen button), only worked if isNativeStyleOK() returns true + void setZoomBtnEnabled(bool bEnable = true); + + bool isCloseBtnEnabled() {return m_bIsCloseBtnEnabled;} + bool isMinBtnEnabled() {return m_bIsMinBtnEnabled;} + bool isZoomBtnEnabled() {return m_bIsZoomBtnEnabled;} +protected: + void mousePressEvent(QMouseEvent *event); + void mouseReleaseEvent(QMouseEvent *event); + void mouseMoveEvent(QMouseEvent *event); +private: + int m_draggableHeight; + bool m_bWinMoving; + bool m_bMousePressed; + QPoint m_MousePos; + QPoint m_WindowPos; + bool m_bCloseBtnQuit; + bool m_bNativeSystemBtn; + bool m_bIsCloseBtnEnabled, m_bIsMinBtnEnabled, m_bIsZoomBtnEnabled; + + //=============================================== + //TODO + //下面的代码是试验性质的 + //tentative code + + //窗口从全屏状态恢复正常大小时,标题栏又会出现,原因未知。 + //默认情况下,系统的最大化按钮(zoom button)是进入全屏,为了避免标题栏重新出现的问题, + //以上代码已经重新定义了系统zoom button的行为,是其功能变为最大化而不是全屏 + //以下代码尝试,每次窗口从全屏状态恢复正常大小时,都再次进行设置,以消除标题栏 + //after the window restore from fullscreen mode, the titlebar will show again, it looks like a BUG + //on OS X 10.10 and later, click the system green button (zoom button) will make the app become fullscreen + //so we have override it's action to "maximized" in the CFramelessWindow Constructor function + //but we may try something else such as delete the titlebar again and again... +private: + bool m_bTitleBarVisible; + + void setTitlebarVisible(bool bTitlebarVisible = false); + bool isTitlebarVisible() {return m_bTitleBarVisible;} +private slots: + void onRestoreFromFullScreen(); +signals: + void restoreFromFullScreen(); +protected: + void resizeEvent(QResizeEvent *event); +}; +#endif + +#endif // CFRAMELESSWINDOW_H diff --git a/client/ui/macos_util.h b/client/ui/macos_util.h new file mode 100644 index 00000000..831f1f88 --- /dev/null +++ b/client/ui/macos_util.h @@ -0,0 +1,8 @@ +#ifndef OSXUTIL_H +#define OSXUTIL_H +#include +#include + +void fixWidget(QWidget *widget); + +#endif diff --git a/client/ui/macos_util.mm b/client/ui/macos_util.mm new file mode 100644 index 00000000..9f433747 --- /dev/null +++ b/client/ui/macos_util.mm @@ -0,0 +1,48 @@ +#include +#include +#include "macos_util.h" + +//this Objective-c class is used to override the action of system close button and zoom button +//https://stackoverflow.com/questions/27643659/setting-c-function-as-selector-for-nsbutton-produces-no-results +@interface ButtonPasser : NSObject{ +} +@property(readwrite) QMainWindow* window; ++ (void)closeButtonAction:(id)sender; +- (void)zoomButtonAction:(id)sender; +@end + +@implementation ButtonPasser{ +} ++ (void)closeButtonAction:(id)sender +{ + Q_UNUSED(sender); + ProcessSerialNumber pn; + GetFrontProcess (&pn); + ShowHideProcess(&pn,false); +} +- (void)zoomButtonAction:(id)sender +{ + Q_UNUSED(sender); + if (0 == self.window) return; + if (self.window->isMaximized()) self.window->showNormal(); + else self.window->showMaximized(); +} +@end + +void fixWidget(QWidget *widget) +{ + NSView *view = (NSView *)widget->winId(); + if (0 == view) return; + NSWindow *window = view.window; + if (0 == window) return; + + //override the action of close button + //https://stackoverflow.com/questions/27643659/setting-c-function-as-selector-for-nsbutton-produces-no-results + //https://developer.apple.com/library/content/documentation/General/Conceptual/CocoaEncyclopedia/Target-Action/Target-Action.html +// NSButton *closeButton = [window standardWindowButton:NSWindowCloseButton]; +// [closeButton setTarget:[ButtonPasser class]]; +// [closeButton setAction:@selector(closeButtonAction:)]; + + [[window standardWindowButton:NSWindowZoomButton] setHidden:YES]; + [[window standardWindowButton:NSWindowMiniaturizeButton] setHidden:YES]; +} diff --git a/client/ui/mainwindow.cpp b/client/ui/mainwindow.cpp index f95090c2..cc13775f 100644 --- a/client/ui/mainwindow.cpp +++ b/client/ui/mainwindow.cpp @@ -14,8 +14,16 @@ #include "utils.h" #include "vpnconnection.h" +#ifdef Q_OS_MAC +#include "ui/macos_util.h" +#endif + MainWindow::MainWindow(QWidget *parent) : +#ifdef Q_OS_WIN + CFramelessWindow(parent), +#else QMainWindow(parent), +#endif ui(new Ui::MainWindow), m_settings(new Settings), m_vpnConnection(nullptr) @@ -23,12 +31,15 @@ MainWindow::MainWindow(QWidget *parent) : ui->setupUi(this); ui->widget_tittlebar->installEventFilter(this); - setWindowFlags(Qt:: ToolTip | Qt::CustomizeWindowHint | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); - setAttribute(Qt::WA_TranslucentBackground); - ui->stackedWidget_main->setSpeed(200); ui->stackedWidget_main->setAnimation(QEasingCurve::Linear); +#ifdef Q_OS_MAC + ui->widget_tittlebar->hide(); + ui->stackedWidget_main->move(0,0); + fixWidget(this); +#endif + // Post initialization if (m_settings->haveAuthData()) { diff --git a/client/ui/mainwindow.h b/client/ui/mainwindow.h index 47d16c32..f5042181 100644 --- a/client/ui/mainwindow.h +++ b/client/ui/mainwindow.h @@ -3,6 +3,7 @@ #include +#include "framelesswindow.h" #include "vpnprotocol.h" class Settings; @@ -15,7 +16,12 @@ class MainWindow; /** * @brief The MainWindow class - Main application window */ +#ifdef Q_OS_WIN +class MainWindow : public CFramelessWindow +#else class MainWindow : public QMainWindow +#endif + { Q_OBJECT diff --git a/client/ui/mainwindow.ui b/client/ui/mainwindow.ui index 7c8fd627..54ff2142 100644 --- a/client/ui/mainwindow.ui +++ b/client/ui/mainwindow.ui @@ -6,8 +6,8 @@ 0 0 - 390 - 685 + 380 + 670 @@ -169,8 +169,8 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { - 5 - 5 + 0 + 0 380 670