From 4de26c757735b531e307bfd780939b14058c7f19 Mon Sep 17 00:00:00 2001 From: rankun Date: Tue, 3 Mar 2020 10:37:13 +0800 Subject: [PATCH 01/19] refactor: videoForm Device ToolForm --- QtScrcpy/device/device.cpp | 18 ++++--- QtScrcpy/device/device.h | 2 + QtScrcpy/device/ui/toolform.cpp | 84 +++++++++++++++++++++++--------- QtScrcpy/device/ui/toolform.h | 9 +++- QtScrcpy/device/ui/toolform.ui | 7 +++ QtScrcpy/device/ui/videoform.cpp | 63 ++++++++++++------------ QtScrcpy/device/ui/videoform.h | 17 +++---- 7 files changed, 127 insertions(+), 73 deletions(-) diff --git a/QtScrcpy/device/device.cpp b/QtScrcpy/device/device.cpp index 9b4f0e5..c152e06 100644 --- a/QtScrcpy/device/device.cpp +++ b/QtScrcpy/device/device.cpp @@ -35,13 +35,7 @@ Device::Device(DeviceParams params, QObject *parent) m_fileHandler = new FileHandler(this); m_controller = new Controller(params.gameScript, this); m_videoForm = new VideoForm(Config::getInstance().getSkin()); - m_videoForm->setSerial(m_params.serial); - if (m_controller) { - m_videoForm->setController(m_controller); - } - if (m_fileHandler) { - m_videoForm->setFileHandler(m_fileHandler); - } + m_videoForm->setDevice(this); m_videoForm->show(); } @@ -91,11 +85,21 @@ Controller *Device::getController() return m_controller; } +FileHandler *Device::getFileHandler() +{ + return m_fileHandler; +} + Server *Device::getServer() { return m_server; } +const QString &Device::getSerial() +{ + return m_params.serial; +} + void Device::updateScript(QString script) { if(m_controller){ diff --git a/QtScrcpy/device/device.h b/QtScrcpy/device/device.h index cfccc54..2fb8917 100644 --- a/QtScrcpy/device/device.h +++ b/QtScrcpy/device/device.h @@ -35,7 +35,9 @@ public: VideoForm *getVideoForm(); Controller *getController(); + FileHandler *getFileHandler(); Server *getServer(); + const QString &getSerial(); void updateScript(QString script); diff --git a/QtScrcpy/device/ui/toolform.cpp b/QtScrcpy/device/ui/toolform.cpp index ea09914..452d6d6 100644 --- a/QtScrcpy/device/ui/toolform.cpp +++ b/QtScrcpy/device/ui/toolform.cpp @@ -6,6 +6,7 @@ #include "toolform.h" #include "ui_toolform.h" #include "iconhelper.h" +#include "device.h" #include "videoform.h" #include "controller.h" #include "adbprocess.h" @@ -18,8 +19,6 @@ ToolForm::ToolForm(QWidget* adsorbWidget, AdsorbPositions adsorbPos) setWindowFlags(windowFlags() | Qt::FramelessWindowHint); //setWindowFlags(windowFlags() & ~Qt::WindowMinMaxButtonsHint); - m_videoForm = dynamic_cast(adsorbWidget); - initStyle(); } @@ -28,6 +27,11 @@ ToolForm::~ToolForm() delete ui; } +void ToolForm::setDevice(Device *device) +{ + m_device = device; +} + void ToolForm::initStyle() { IconHelper::Instance()->SetIcon(ui->fullScreenBtn, QChar(0xf0b2), 15); @@ -43,6 +47,19 @@ void ToolForm::initStyle() IconHelper::Instance()->SetIcon(ui->expandNotifyBtn, QChar(0xf103), 15); IconHelper::Instance()->SetIcon(ui->screenShotBtn, QChar(0xf0c4), 15); IconHelper::Instance()->SetIcon(ui->touchBtn, QChar(0xf111), 15); + IconHelper::Instance()->SetIcon(ui->groupControlBtn, QChar(0xf0c0), 15); +} + +void ToolForm::updateGroupControl() +{ + if (!m_device || !m_device->getVideoForm()) { + return; + } + if (m_device->getVideoForm()->mainControl()) { + ui->groupControlBtn->setStyleSheet("color: red"); + } else { + ui->groupControlBtn->setStyleSheet("color: #DCDCDC"); + } } void ToolForm::mousePressEvent(QMouseEvent *event) @@ -80,44 +97,50 @@ void ToolForm::hideEvent(QHideEvent *event) void ToolForm::on_fullScreenBtn_clicked() { - if (m_videoForm) { - m_videoForm->switchFullScreen(); + if (!m_device || !m_device->getVideoForm()) { + return; } + m_device->getVideoForm()->switchFullScreen(); } void ToolForm::on_returnBtn_clicked() { - if (m_videoForm && m_videoForm->getController()) { - m_videoForm->getController()->postGoBack(); + if (!m_device || !m_device->getController()) { + return; } + m_device->getController()->postGoBack(); } void ToolForm::on_homeBtn_clicked() { - if (m_videoForm && m_videoForm->getController()) { - m_videoForm->getController()->postGoHome(); + if (!m_device || !m_device->getController()) { + return; } + m_device->getController()->postGoHome(); } void ToolForm::on_menuBtn_clicked() { - if (m_videoForm && m_videoForm->getController()) { - m_videoForm->getController()->postGoMenu(); + if (!m_device || !m_device->getController()) { + return; } + m_device->getController()->postGoMenu(); } void ToolForm::on_appSwitchBtn_clicked() { - if (m_videoForm && m_videoForm->getController()) { - m_videoForm->getController()->postAppSwitch(); + if (!m_device || !m_device->getController()) { + return; } + m_device->getController()->postAppSwitch(); } void ToolForm::on_powerBtn_clicked() { - if (m_videoForm && m_videoForm->getController()) { - m_videoForm->getController()->postPower(); + if (!m_device || !m_device->getController()) { + return; } + m_device->getController()->postPower(); } void ToolForm::on_screenShotBtn_clicked() @@ -127,35 +150,39 @@ void ToolForm::on_screenShotBtn_clicked() void ToolForm::on_volumeUpBtn_clicked() { - if (m_videoForm && m_videoForm->getController()) { - m_videoForm->getController()->postVolumeUp(); + if (!m_device || !m_device->getController()) { + return; } + m_device->getController()->postVolumeUp(); } void ToolForm::on_volumeDownBtn_clicked() { - if (m_videoForm && m_videoForm->getController()) { - m_videoForm->getController()->postVolumeDown(); + if (!m_device || !m_device->getController()) { + return; } + m_device->getController()->postVolumeDown(); } void ToolForm::on_closeScreenBtn_clicked() { - if (m_videoForm && m_videoForm->getController()) { - m_videoForm->getController()->setScreenPowerMode(ControlMsg::SPM_OFF); + if (!m_device || !m_device->getController()) { + return; } + m_device->getController()->setScreenPowerMode(ControlMsg::SPM_OFF); } void ToolForm::on_expandNotifyBtn_clicked() { - if (m_videoForm && m_videoForm->getController()) { - m_videoForm->getController()->expandNotificationPanel(); + if (!m_device || !m_device->getController()) { + return; } + m_device->getController()->expandNotificationPanel(); } void ToolForm::on_touchBtn_clicked() { - if (!m_videoForm) { + if (!m_device) { return; } @@ -170,7 +197,16 @@ void ToolForm::on_touchBtn_clicked() sender()->deleteLater(); } }); - adb->setShowTouchesEnabled(m_videoForm->getSerial(), m_showTouch); + adb->setShowTouchesEnabled(m_device->getSerial(), m_showTouch); qInfo() << "show touch " << (m_showTouch ? "enable" : "disable"); } + +void ToolForm::on_groupControlBtn_clicked() +{ + if (!m_device || !m_device->getVideoForm()) { + return; + } + m_device->getVideoForm()->setMainControl(!m_device->getVideoForm()->mainControl()); + updateGroupControl(); +} diff --git a/QtScrcpy/device/ui/toolform.h b/QtScrcpy/device/ui/toolform.h index 4b63a35..678a984 100644 --- a/QtScrcpy/device/ui/toolform.h +++ b/QtScrcpy/device/ui/toolform.h @@ -10,7 +10,7 @@ namespace Ui { class ToolForm; } -class VideoForm; +class Device; class ToolForm : public MagneticWidget { Q_OBJECT @@ -19,6 +19,8 @@ public: explicit ToolForm(QWidget* adsorbWidget, AdsorbPositions adsorbPos); ~ToolForm(); + void setDevice(Device *device); + protected: void mousePressEvent(QMouseEvent *event); void mouseReleaseEvent(QMouseEvent *event); @@ -55,13 +57,16 @@ private slots: void on_touchBtn_clicked(); + void on_groupControlBtn_clicked(); + private: void initStyle(); + void updateGroupControl(); private: Ui::ToolForm *ui; QPoint m_dragPosition; - QPointer m_videoForm; + QPointer m_device; bool m_showTouch = false; }; diff --git a/QtScrcpy/device/ui/toolform.ui b/QtScrcpy/device/ui/toolform.ui index e2bd9fb..77a2a0e 100644 --- a/QtScrcpy/device/ui/toolform.ui +++ b/QtScrcpy/device/ui/toolform.ui @@ -20,6 +20,13 @@ 30 + + + + + + + diff --git a/QtScrcpy/device/ui/videoform.cpp b/QtScrcpy/device/ui/videoform.cpp index 82a51e2..4b5997e 100644 --- a/QtScrcpy/device/ui/videoform.cpp +++ b/QtScrcpy/device/ui/videoform.cpp @@ -15,6 +15,7 @@ #include "ui_videoform.h" #include "iconhelper.h" #include "toolform.h" +#include "device.h" #include "controller.h" #include "filehandler.h" #include "config.h" @@ -98,6 +99,7 @@ void VideoForm::showToolForm(bool show) { if (!m_toolForm) { m_toolForm = new ToolForm(this, ToolForm::AP_OUTSIDE_RIGHT); + m_toolForm->setDevice(m_device); connect(m_toolForm, &ToolForm::screenshot, this, &VideoForm::screenshot); } m_toolForm->move(pos().x() + geometry().width(), pos().y() + 30); @@ -253,39 +255,38 @@ void VideoForm::staysOnTop(bool top) } } -Controller *VideoForm::getController() +Device *VideoForm::getDevice() { - return m_controller; + return m_device; } -void VideoForm::setFileHandler(FileHandler *fileHandler) +void VideoForm::setMainControl(bool mainControl) { - m_fileHandler = fileHandler; + if (m_mainControl == mainControl) { + return; + } + m_mainControl = mainControl; + emit mainControlChange(this, m_mainControl); } -void VideoForm::setSerial(const QString &serial) +bool VideoForm::mainControl() { - m_serial = serial; + return m_mainControl; } -const QString &VideoForm::getSerial() +void VideoForm::setDevice(Device *device) { - return m_serial; -} - -void VideoForm::setController(Controller *controller) -{ - m_controller = controller; + m_device = device; } void VideoForm::mousePressEvent(QMouseEvent *event) { if (m_videoWidget->geometry().contains(event->pos())) { - if (!m_controller) { + if (!m_device || !m_device->getController()) { return; } event->setLocalPos(m_videoWidget->mapFrom(this, event->localPos().toPoint())); - m_controller->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); + m_device->getController()->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); } else { if (event->button() == Qt::LeftButton) { m_dragPosition = event->globalPos() - frameGeometry().topLeft(); @@ -297,7 +298,7 @@ void VideoForm::mousePressEvent(QMouseEvent *event) void VideoForm::mouseReleaseEvent(QMouseEvent *event) { if (m_dragPosition.isNull()) { - if (!m_controller) { + if (!m_device || !m_device->getController()) { return; } event->setLocalPos(m_videoWidget->mapFrom(this, event->localPos().toPoint())); @@ -316,7 +317,7 @@ void VideoForm::mouseReleaseEvent(QMouseEvent *event) local.setY(m_videoWidget->height()); } event->setLocalPos(local); - m_controller->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); + m_device->getController()->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); } else { m_dragPosition = QPoint(0, 0); } @@ -325,11 +326,11 @@ void VideoForm::mouseReleaseEvent(QMouseEvent *event) void VideoForm::mouseMoveEvent(QMouseEvent *event) { if (m_videoWidget->geometry().contains(event->pos())) { - if (!m_controller) { + if (!m_device || !m_device->getController()) { return; } event->setLocalPos(m_videoWidget->mapFrom(this, event->localPos().toPoint())); - m_controller->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); + m_device->getController()->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); } else if (!m_dragPosition.isNull()){ if (event->buttons() & Qt::LeftButton) { move(event->globalPos() - m_dragPosition); @@ -341,7 +342,7 @@ void VideoForm::mouseMoveEvent(QMouseEvent *event) void VideoForm::wheelEvent(QWheelEvent *event) { if (m_videoWidget->geometry().contains(event->pos())) { - if (!m_controller) { + if (!m_device || !m_device->getController()) { return; } QPointF pos = m_videoWidget->mapFrom(this, event->pos()); @@ -352,7 +353,7 @@ void VideoForm::wheelEvent(QWheelEvent *event) */ QWheelEvent wheelEvent(pos, event->globalPosF(), event->delta(), event->buttons(), event->modifiers(), event->orientation()); - m_controller->wheelEvent(&wheelEvent, m_videoWidget->frameSize(), m_videoWidget->size()); + m_device->getController()->wheelEvent(&wheelEvent, m_videoWidget->frameSize(), m_videoWidget->size()); } } @@ -363,30 +364,30 @@ void VideoForm::keyPressEvent(QKeyEvent *event) && isFullScreen()) { switchFullScreen(); } - if (!m_controller) { + if (!m_device || !m_device->getController()) { return; } if (event->key() == Qt::Key_C && (event->modifiers() & Qt::ControlModifier)) { - m_controller->requestDeviceClipboard(); + m_device->getController()->requestDeviceClipboard(); } if (event->key() == Qt::Key_V && (event->modifiers() & Qt::ControlModifier)) { if (event->modifiers() & Qt::ShiftModifier) { - m_controller->setDeviceClipboard(); + m_device->getController()->setDeviceClipboard(); } else { - m_controller->clipboardPaste(); + m_device->getController()->clipboardPaste(); } return; } - m_controller->keyEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); + m_device->getController()->keyEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); } void VideoForm::keyReleaseEvent(QKeyEvent *event) { - if (!m_controller) { + if (!m_device || !m_device->getController()) { return; } - m_controller->keyEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); + m_device->getController()->keyEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); } void VideoForm::paintEvent(QPaintEvent *paint) @@ -449,7 +450,7 @@ void VideoForm::dragLeaveEvent(QDragLeaveEvent *event) void VideoForm::dropEvent(QDropEvent *event) { - if (!m_fileHandler) { + if (!m_device || !m_device->getFileHandler()) { return; } const QMimeData* qm = event->mimeData(); @@ -462,8 +463,8 @@ void VideoForm::dropEvent(QDropEvent *event) } if (fileInfo.isFile() && fileInfo.suffix() == "apk") { - m_fileHandler->installApkRequest(m_serial, file); + m_device->getFileHandler()->installApkRequest(m_device->getSerial(), file); return; } - m_fileHandler->pushFileRequest(m_serial, file, Config::getInstance().getPushFilePath() + fileInfo.fileName()); + m_device->getFileHandler()->pushFileRequest(m_device->getSerial(), file, Config::getInstance().getPushFilePath() + fileInfo.fileName()); } diff --git a/QtScrcpy/device/ui/videoform.h b/QtScrcpy/device/ui/videoform.h index aaf598a..6557c64 100644 --- a/QtScrcpy/device/ui/videoform.h +++ b/QtScrcpy/device/ui/videoform.h @@ -10,7 +10,7 @@ class videoForm; struct AVFrame; class ToolForm; -class Controller; +class Device; class FileHandler; class QYUVOpenGLWidget; class VideoForm : public QWidget @@ -24,14 +24,14 @@ public: void staysOnTop(bool top = true); void updateShowSize(const QSize &newSize); void updateRender(const AVFrame *frame); - void setController(Controller *controller); - Controller* getController(); - void setFileHandler(FileHandler *fileHandler); - void setSerial(const QString &serial); - const QString& getSerial(); + void setDevice(Device *device); + Device* getDevice(); + void setMainControl(bool mainControl); + bool mainControl(); signals: void screenshot(); + void mainControlChange(VideoForm* videoFrom, bool mainControl); public slots: void onGrabCursor(bool grab); @@ -74,11 +74,10 @@ private: float m_widthHeightRatio = 0.5f; bool m_skin = true; QPoint m_fullScreenBeforePos; + bool m_mainControl = false; //outside member - QString m_serial = ""; - QPointer m_controller; - QPointer m_fileHandler; + QPointer m_device; }; #endif // VIDEOFORM_H From 0fbf37afd374c1e0a0a44233f13039db60ddfd60 Mon Sep 17 00:00:00 2001 From: rankun Date: Fri, 6 Mar 2020 17:05:35 +0800 Subject: [PATCH 02/19] refactor: move mainControl from videoFrom to Device --- QtScrcpy/device/device.cpp | 14 ++++++++++++++ QtScrcpy/device/device.h | 5 +++++ QtScrcpy/device/ui/toolform.cpp | 8 ++++---- QtScrcpy/device/ui/videoform.cpp | 19 ------------------- QtScrcpy/device/ui/videoform.h | 5 ----- 5 files changed, 23 insertions(+), 28 deletions(-) diff --git a/QtScrcpy/device/device.cpp b/QtScrcpy/device/device.cpp index c152e06..53304f5 100644 --- a/QtScrcpy/device/device.cpp +++ b/QtScrcpy/device/device.cpp @@ -239,6 +239,20 @@ void Device::startServer() }); } +void Device::setMainControl(bool mainControl) +{ + if (m_mainControl == mainControl) { + return; + } + m_mainControl = mainControl; + emit mainControlChange(this, m_mainControl); +} + +bool Device::mainControl() +{ + return m_mainControl; +} + bool Device::saveFrame(const AVFrame* frame) { if (!frame) { diff --git a/QtScrcpy/device/device.h b/QtScrcpy/device/device.h index 2fb8917..e45aa50 100644 --- a/QtScrcpy/device/device.h +++ b/QtScrcpy/device/device.h @@ -40,9 +40,12 @@ public: const QString &getSerial(); void updateScript(QString script); + void setMainControl(bool mainControl); + bool mainControl(); signals: void deviceDisconnect(QString serial); + void mainControlChange(Device* device, bool mainControl); public slots: void onScreenshot(); @@ -67,6 +70,8 @@ private: QTime m_startTimeCount; DeviceParams m_params; + + bool m_mainControl = false; }; #endif // DEVICE_H diff --git a/QtScrcpy/device/ui/toolform.cpp b/QtScrcpy/device/ui/toolform.cpp index 452d6d6..9cceb5a 100644 --- a/QtScrcpy/device/ui/toolform.cpp +++ b/QtScrcpy/device/ui/toolform.cpp @@ -52,10 +52,10 @@ void ToolForm::initStyle() void ToolForm::updateGroupControl() { - if (!m_device || !m_device->getVideoForm()) { + if (!m_device) { return; } - if (m_device->getVideoForm()->mainControl()) { + if (m_device->mainControl()) { ui->groupControlBtn->setStyleSheet("color: red"); } else { ui->groupControlBtn->setStyleSheet("color: #DCDCDC"); @@ -204,9 +204,9 @@ void ToolForm::on_touchBtn_clicked() void ToolForm::on_groupControlBtn_clicked() { - if (!m_device || !m_device->getVideoForm()) { + if (!m_device) { return; } - m_device->getVideoForm()->setMainControl(!m_device->getVideoForm()->mainControl()); + m_device->setMainControl(!m_device->mainControl()); updateGroupControl(); } diff --git a/QtScrcpy/device/ui/videoform.cpp b/QtScrcpy/device/ui/videoform.cpp index 4b5997e..63b2f4e 100644 --- a/QtScrcpy/device/ui/videoform.cpp +++ b/QtScrcpy/device/ui/videoform.cpp @@ -255,25 +255,6 @@ void VideoForm::staysOnTop(bool top) } } -Device *VideoForm::getDevice() -{ - return m_device; -} - -void VideoForm::setMainControl(bool mainControl) -{ - if (m_mainControl == mainControl) { - return; - } - m_mainControl = mainControl; - emit mainControlChange(this, m_mainControl); -} - -bool VideoForm::mainControl() -{ - return m_mainControl; -} - void VideoForm::setDevice(Device *device) { m_device = device; diff --git a/QtScrcpy/device/ui/videoform.h b/QtScrcpy/device/ui/videoform.h index 6557c64..26e5631 100644 --- a/QtScrcpy/device/ui/videoform.h +++ b/QtScrcpy/device/ui/videoform.h @@ -25,13 +25,9 @@ public: void updateShowSize(const QSize &newSize); void updateRender(const AVFrame *frame); void setDevice(Device *device); - Device* getDevice(); - void setMainControl(bool mainControl); - bool mainControl(); signals: void screenshot(); - void mainControlChange(VideoForm* videoFrom, bool mainControl); public slots: void onGrabCursor(bool grab); @@ -74,7 +70,6 @@ private: float m_widthHeightRatio = 0.5f; bool m_skin = true; QPoint m_fullScreenBeforePos; - bool m_mainControl = false; //outside member QPointer m_device; From 09331711c88d00836866ca82561169c065cbc5d1 Mon Sep 17 00:00:00 2001 From: rankun Date: Fri, 6 Mar 2020 22:12:37 +0800 Subject: [PATCH 03/19] refactor: for group control --- QtScrcpy/device/controller/controller.cpp | 38 +++++----- QtScrcpy/device/controller/controller.h | 40 ++++++----- QtScrcpy/device/device.cpp | 79 +++++++++++++++------ QtScrcpy/device/device.h | 38 ++++++++-- QtScrcpy/device/filehandler/filehandler.cpp | 4 +- QtScrcpy/device/filehandler/filehandler.h | 6 +- QtScrcpy/device/ui/toolform.cpp | 62 +++++++--------- QtScrcpy/device/ui/toolform.h | 15 ---- QtScrcpy/device/ui/videoform.cpp | 43 ++++++----- QtScrcpy/device/ui/videoform.h | 5 +- 10 files changed, 188 insertions(+), 142 deletions(-) diff --git a/QtScrcpy/device/controller/controller.cpp b/QtScrcpy/device/controller/controller.cpp index 0ec988e..65fd60a 100644 --- a/QtScrcpy/device/controller/controller.cpp +++ b/QtScrcpy/device/controller/controller.cpp @@ -62,7 +62,7 @@ void Controller::updateScript(QString gameScript) connect(m_inputConvert, &InputConvertBase::grabCursor, this, &Controller::grabCursor); } -void Controller::postTurnOn() +void Controller::onPostTurnOn() { ControlMsg* controlMsg = new ControlMsg(ControlMsg::CMT_BACK_OR_SCREEN_ON); if (!controlMsg) { @@ -71,42 +71,42 @@ void Controller::postTurnOn() postControlMsg(controlMsg); } -void Controller::postGoHome() +void Controller::onPostGoHome() { postKeyCodeClick(AKEYCODE_HOME); } -void Controller::postGoMenu() +void Controller::onPostGoMenu() { postKeyCodeClick(AKEYCODE_MENU); } -void Controller::postGoBack() +void Controller::onPostGoBack() { postKeyCodeClick(AKEYCODE_BACK); } -void Controller::postAppSwitch() +void Controller::onPostAppSwitch() { postKeyCodeClick(AKEYCODE_APP_SWITCH); } -void Controller::postPower() +void Controller::onPostPower() { postKeyCodeClick(AKEYCODE_POWER); } -void Controller::postVolumeUp() +void Controller::onPostVolumeUp() { postKeyCodeClick(AKEYCODE_VOLUME_UP); } -void Controller::postVolumeDown() +void Controller::onPostVolumeDown() { postKeyCodeClick(AKEYCODE_VOLUME_DOWN); } -void Controller::expandNotificationPanel() +void Controller::onExpandNotificationPanel() { ControlMsg* controlMsg = new ControlMsg(ControlMsg::CMT_EXPAND_NOTIFICATION_PANEL); if (!controlMsg) { @@ -115,7 +115,7 @@ void Controller::expandNotificationPanel() postControlMsg(controlMsg); } -void Controller::collapseNotificationPanel() +void Controller::onCollapseNotificationPanel() { ControlMsg* controlMsg = new ControlMsg(ControlMsg::CMT_COLLAPSE_NOTIFICATION_PANEL); if (!controlMsg) { @@ -124,7 +124,7 @@ void Controller::collapseNotificationPanel() postControlMsg(controlMsg); } -void Controller::requestDeviceClipboard() +void Controller::onRequestDeviceClipboard() { ControlMsg* controlMsg = new ControlMsg(ControlMsg::CMT_GET_CLIPBOARD); if (!controlMsg) { @@ -133,7 +133,7 @@ void Controller::requestDeviceClipboard() postControlMsg(controlMsg); } -void Controller::setDeviceClipboard() +void Controller::onSetDeviceClipboard() { QClipboard *board = QApplication::clipboard(); QString text = board->text(); @@ -145,14 +145,14 @@ void Controller::setDeviceClipboard() postControlMsg(controlMsg); } -void Controller::clipboardPaste() +void Controller::onClipboardPaste() { QClipboard *board = QApplication::clipboard(); QString text = board->text(); - postTextInput(text); + onPostTextInput(text); } -void Controller::postTextInput(QString& text) +void Controller::onPostTextInput(QString& text) { ControlMsg* controlMsg = new ControlMsg(ControlMsg::CMT_INJECT_TEXT); if (!controlMsg) { @@ -162,7 +162,7 @@ void Controller::postTextInput(QString& text) postControlMsg(controlMsg); } -void Controller::setScreenPowerMode(ControlMsg::ScreenPowerMode mode) +void Controller::onSetScreenPowerMode(ControlMsg::ScreenPowerMode mode) { ControlMsg* controlMsg = new ControlMsg(ControlMsg::CMT_SET_SCREEN_POWER_MODE); if (!controlMsg) { @@ -172,21 +172,21 @@ void Controller::setScreenPowerMode(ControlMsg::ScreenPowerMode mode) postControlMsg(controlMsg); } -void Controller::mouseEvent(const QMouseEvent *from, const QSize &frameSize, const QSize &showSize) +void Controller::onMouseEvent(const QMouseEvent *from, const QSize &frameSize, const QSize &showSize) { if (m_inputConvert) { m_inputConvert->mouseEvent(from, frameSize, showSize); } } -void Controller::wheelEvent(const QWheelEvent *from, const QSize &frameSize, const QSize &showSize) +void Controller::onWheelEvent(const QWheelEvent *from, const QSize &frameSize, const QSize &showSize) { if (m_inputConvert) { m_inputConvert->wheelEvent(from, frameSize, showSize); } } -void Controller::keyEvent(const QKeyEvent *from, const QSize &frameSize, const QSize &showSize) +void Controller::onKeyEvent(const QKeyEvent *from, const QSize &frameSize, const QSize &showSize) { if (m_inputConvert) { m_inputConvert->keyEvent(from, frameSize, showSize); diff --git a/QtScrcpy/device/controller/controller.h b/QtScrcpy/device/controller/controller.h index d6b1730..9bf14d0 100644 --- a/QtScrcpy/device/controller/controller.h +++ b/QtScrcpy/device/controller/controller.h @@ -22,27 +22,29 @@ public: void updateScript(QString gameScript = ""); - // turn the screen on if it was off, press BACK otherwise - void postTurnOn(); - void postGoHome(); - void postGoMenu(); - void postGoBack(); - void postAppSwitch(); - void postPower(); - void postVolumeUp(); - void postVolumeDown(); - void expandNotificationPanel(); - void collapseNotificationPanel(); - void requestDeviceClipboard(); - void setDeviceClipboard(); - void clipboardPaste(); - void postTextInput(QString& text); - void setScreenPowerMode(ControlMsg::ScreenPowerMode mode); +public slots: + void onPostGoBack(); + void onPostGoHome(); + void onPostGoMenu(); + void onPostAppSwitch(); + void onPostPower(); + void onPostVolumeUp(); + void onPostVolumeDown(); + void onExpandNotificationPanel(); + void onCollapseNotificationPanel(); + void onSetScreenPowerMode(ControlMsg::ScreenPowerMode mode); // for input convert - void mouseEvent(const QMouseEvent* from, const QSize& frameSize, const QSize& showSize); - void wheelEvent(const QWheelEvent* from, const QSize& frameSize, const QSize& showSize); - void keyEvent(const QKeyEvent* from, const QSize& frameSize, const QSize& showSize); + void onMouseEvent(const QMouseEvent* from, const QSize& frameSize, const QSize& showSize); + void onWheelEvent(const QWheelEvent* from, const QSize& frameSize, const QSize& showSize); + void onKeyEvent(const QKeyEvent* from, const QSize& frameSize, const QSize& showSize); + + // turn the screen on if it was off, press BACK otherwise + void onPostTurnOn(); + void onRequestDeviceClipboard(); + void onSetDeviceClipboard(); + void onClipboardPaste(); + void onPostTextInput(QString& text); signals: void grabCursor(bool grab); diff --git a/QtScrcpy/device/device.cpp b/QtScrcpy/device/device.cpp index 53304f5..bde630e 100644 --- a/QtScrcpy/device/device.cpp +++ b/QtScrcpy/device/device.cpp @@ -80,16 +80,6 @@ VideoForm *Device::getVideoForm() return m_videoForm; } -Controller *Device::getController() -{ - return m_controller; -} - -FileHandler *Device::getFileHandler() -{ - return m_fileHandler; -} - Server *Device::getServer() { return m_server; @@ -119,35 +109,84 @@ void Device::onScreenshot() m_vb->unLock(); } +void Device::onShowTouch(bool show) +{ + AdbProcess* adb = new AdbProcess(); + if (!adb) { + return; + } + connect(adb, &AdbProcess::adbProcessResult, this, [this](AdbProcess::ADB_EXEC_RESULT processResult){ + if (AdbProcess::AER_SUCCESS_START != processResult) { + sender()->deleteLater(); + } + }); + adb->setShowTouchesEnabled(getSerial(), show); + + qInfo() << getSerial() << " show touch " << (show ? "enable" : "disable"); +} + void Device::initSignals() { + connect(this, &Device::screenshot, this, &Device::onScreenshot); + connect(this, &Device::showTouch, this, &Device::onShowTouch); + connect(this, &Device::setMainControl, this, &Device::onSetMainControl); + if (m_controller && m_videoForm) { connect(m_controller, &Controller::grabCursor, m_videoForm, &VideoForm::onGrabCursor); - connect(m_videoForm, &VideoForm::screenshot, this, &Device::onScreenshot); + } + if (m_controller) { + connect(this, &Device::postGoBack, m_controller, &Controller::onPostGoBack); + connect(this, &Device::postGoHome, m_controller, &Controller::onPostGoHome); + connect(this, &Device::postGoMenu, m_controller, &Controller::onPostGoMenu); + connect(this, &Device::postAppSwitch, m_controller, &Controller::onPostAppSwitch); + connect(this, &Device::postPower, m_controller, &Controller::onPostPower); + connect(this, &Device::postVolumeUp, m_controller, &Controller::onPostVolumeUp); + connect(this, &Device::postVolumeDown, m_controller, &Controller::onPostVolumeDown); + connect(this, &Device::setScreenPowerMode, m_controller, &Controller::onSetScreenPowerMode); + connect(this, &Device::expandNotificationPanel, m_controller, &Controller::onExpandNotificationPanel); + connect(this, &Device::mouseEvent, m_controller, &Controller::onMouseEvent); + connect(this, &Device::wheelEvent, m_controller, &Controller::onWheelEvent); + connect(this, &Device::keyEvent, m_controller, &Controller::onKeyEvent); + + connect(this, &Device::postTurnOn, m_controller, &Controller::onPostTurnOn); + connect(this, &Device::requestDeviceClipboard, m_controller, &Controller::onRequestDeviceClipboard); + connect(this, &Device::setDeviceClipboard, m_controller, &Controller::onSetDeviceClipboard); + connect(this, &Device::clipboardPaste, m_controller, &Controller::onClipboardPaste); + connect(this, &Device::postTextInput, m_controller, &Controller::onPostTextInput); } if (m_videoForm) { connect(m_videoForm, &VideoForm::destroyed, this, [this](QObject *obj){ Q_UNUSED(obj) deleteLater(); }); + + connect(this, &Device::switchFullScreen, m_videoForm, &VideoForm::onSwitchFullScreen); } if (m_fileHandler) { + connect(this, &Device::pushFileRequest, m_fileHandler, &FileHandler::onPushFileRequest); + connect(this, &Device::installApkRequest, m_fileHandler, &FileHandler::onInstallApkRequest); connect(m_fileHandler, &FileHandler::fileHandlerResult, this, [this](FileHandler::FILE_HANDLER_RESULT processResult, bool isApk){ - QString tips = ""; + QString tipsType = ""; if (isApk) { - tips = tr("install apk"); + tipsType = tr("install apk"); } else { - tips = tr("file transfer"); + tipsType = tr("file transfer"); } + QString tips; if (FileHandler::FAR_IS_RUNNING == processResult && m_videoForm) { - QMessageBox::warning(m_videoForm, "QtScrcpy", tr("wait current %1 to complete").arg(tips), QMessageBox::Ok); + tips = tr("wait current %1 to complete").arg(tipsType); } if (FileHandler::FAR_SUCCESS_EXEC == processResult && m_videoForm) { - QMessageBox::information(m_videoForm, "QtScrcpy", tr("%1 complete, save in %2").arg(tips).arg(Config::getInstance().getPushFilePath()), QMessageBox::Ok); + tips = tr("%1 complete, save in %2").arg(tipsType).arg(Config::getInstance().getPushFilePath()); } if (FileHandler::FAR_ERROR_EXEC == processResult && m_videoForm) { - QMessageBox::information(m_videoForm, "QtScrcpy", tr("%1 failed").arg(tips), QMessageBox::Ok); + tips = tr("%1 failed").arg(tipsType); } + qInfo() << tips; + if (!m_mainControl) { + return; + } + QMessageBox::information(m_videoForm, "QtScrcpy", tips, QMessageBox::Ok); }); } @@ -186,7 +225,7 @@ void Device::initSignals() // 显示界面时才自动息屏(m_params.display) if (m_params.closeScreen && m_params.display && m_controller) { - m_controller->setScreenPowerMode(ControlMsg::SPM_OFF); + emit m_controller->onSetScreenPowerMode(ControlMsg::SPM_OFF); } } }); @@ -239,13 +278,13 @@ void Device::startServer() }); } -void Device::setMainControl(bool mainControl) +void Device::onSetMainControl(Device* device, bool mainControl) { + Q_UNUSED(device) if (m_mainControl == mainControl) { return; } m_mainControl = mainControl; - emit mainControlChange(this, m_mainControl); } bool Device::mainControl() diff --git a/QtScrcpy/device/device.h b/QtScrcpy/device/device.h index e45aa50..c5363b8 100644 --- a/QtScrcpy/device/device.h +++ b/QtScrcpy/device/device.h @@ -4,6 +4,11 @@ #include #include +#include "controlmsg.h" + +class QMouseEvent; +class QWheelEvent; +class QKeyEvent; class Recorder; class Server; class VideoBuffer; @@ -34,21 +39,46 @@ public: virtual ~Device(); VideoForm *getVideoForm(); - Controller *getController(); - FileHandler *getFileHandler(); Server *getServer(); const QString &getSerial(); void updateScript(QString script); - void setMainControl(bool mainControl); bool mainControl(); signals: void deviceDisconnect(QString serial); - void mainControlChange(Device* device, bool mainControl); + + void switchFullScreen(); + void postGoBack(); + void postGoHome(); + void postGoMenu(); + void postAppSwitch(); + void postPower(); + void postVolumeUp(); + void postVolumeDown(); + void setScreenPowerMode(ControlMsg::ScreenPowerMode mode); + void expandNotificationPanel(); + void screenshot(); + void showTouch(bool show); + void setMainControl(Device* device, bool mainControl); + + void mouseEvent(const QMouseEvent* from, const QSize& frameSize, const QSize& showSize); + void wheelEvent(const QWheelEvent* from, const QSize& frameSize, const QSize& showSize); + void keyEvent(const QKeyEvent* from, const QSize& frameSize, const QSize& showSize); + + void postTurnOn(); + void postTextInput(QString& text); + void requestDeviceClipboard(); + void setDeviceClipboard(); + void clipboardPaste(); + + void pushFileRequest(const QString& serial, const QString& file, const QString& devicePath = ""); + void installApkRequest(const QString& serial, const QString& apkFile); public slots: void onScreenshot(); + void onShowTouch(bool show); + void onSetMainControl(Device* device, bool mainControl); private: void initSignals(); diff --git a/QtScrcpy/device/filehandler/filehandler.cpp b/QtScrcpy/device/filehandler/filehandler.cpp index c92425d..4730f6c 100644 --- a/QtScrcpy/device/filehandler/filehandler.cpp +++ b/QtScrcpy/device/filehandler/filehandler.cpp @@ -24,7 +24,7 @@ FileHandler::~FileHandler() } -void FileHandler::pushFileRequest(const QString &serial, const QString &file, const QString& devicePath) +void FileHandler::onPushFileRequest(const QString &serial, const QString &file, const QString& devicePath) { if (m_adb.isRuning()) { emit fileHandlerResult(FAR_IS_RUNNING, false); @@ -35,7 +35,7 @@ void FileHandler::pushFileRequest(const QString &serial, const QString &file, co m_adb.push(serial, file, devicePath); } -void FileHandler::installApkRequest(const QString &serial, const QString &apkFile) +void FileHandler::onInstallApkRequest(const QString &serial, const QString &apkFile) { if (m_adb.isRuning()) { emit fileHandlerResult(FAR_IS_RUNNING, true); diff --git a/QtScrcpy/device/filehandler/filehandler.h b/QtScrcpy/device/filehandler/filehandler.h index 3a2023c..8c48db1 100644 --- a/QtScrcpy/device/filehandler/filehandler.h +++ b/QtScrcpy/device/filehandler/filehandler.h @@ -17,10 +17,12 @@ public: FileHandler(QObject *parent = nullptr); virtual ~FileHandler(); - void pushFileRequest(const QString& serial, const QString& file, const QString& devicePath = ""); - void installApkRequest(const QString& serial, const QString& apkFile); const QString &getDevicePath(); +public slots: + void onPushFileRequest(const QString& serial, const QString& file, const QString& devicePath = ""); + void onInstallApkRequest(const QString& serial, const QString& apkFile); + signals: void fileHandlerResult(FILE_HANDLER_RESULT processResult, bool isApk = false); diff --git a/QtScrcpy/device/ui/toolform.cpp b/QtScrcpy/device/ui/toolform.cpp index 9cceb5a..6fe898a 100644 --- a/QtScrcpy/device/ui/toolform.cpp +++ b/QtScrcpy/device/ui/toolform.cpp @@ -97,87 +97,91 @@ void ToolForm::hideEvent(QHideEvent *event) void ToolForm::on_fullScreenBtn_clicked() { - if (!m_device || !m_device->getVideoForm()) { + if (!m_device) { return; } - m_device->getVideoForm()->switchFullScreen(); + + emit m_device->switchFullScreen(); } void ToolForm::on_returnBtn_clicked() { - if (!m_device || !m_device->getController()) { + if (!m_device) { return; } - m_device->getController()->postGoBack(); + emit m_device->postGoBack(); } void ToolForm::on_homeBtn_clicked() { - if (!m_device || !m_device->getController()) { + if (!m_device) { return; } - m_device->getController()->postGoHome(); + emit m_device->postGoHome(); } void ToolForm::on_menuBtn_clicked() { - if (!m_device || !m_device->getController()) { + if (!m_device) { return; } - m_device->getController()->postGoMenu(); + emit m_device->postGoMenu(); } void ToolForm::on_appSwitchBtn_clicked() { - if (!m_device || !m_device->getController()) { + if (!m_device) { return; } - m_device->getController()->postAppSwitch(); + emit m_device->postAppSwitch(); } void ToolForm::on_powerBtn_clicked() { - if (!m_device || !m_device->getController()) { + if (!m_device) { return; } - m_device->getController()->postPower(); + emit m_device->postPower(); } void ToolForm::on_screenShotBtn_clicked() { - emit screenshot(); + if (!m_device) { + return; + } + emit m_device->screenshot(); } void ToolForm::on_volumeUpBtn_clicked() { - if (!m_device || !m_device->getController()) { + if (!m_device) { return; } - m_device->getController()->postVolumeUp(); + emit m_device->postVolumeUp(); } void ToolForm::on_volumeDownBtn_clicked() { - if (!m_device || !m_device->getController()) { + if (!m_device) { return; } - m_device->getController()->postVolumeDown(); + emit m_device->postVolumeDown(); } void ToolForm::on_closeScreenBtn_clicked() { - if (!m_device || !m_device->getController()) { + if (!m_device) { return; } - m_device->getController()->setScreenPowerMode(ControlMsg::SPM_OFF); + emit m_device->setScreenPowerMode(ControlMsg::SPM_OFF); } void ToolForm::on_expandNotifyBtn_clicked() { - if (!m_device || !m_device->getController()) { + if (!m_device) { return; } - m_device->getController()->expandNotificationPanel(); + emit m_device->expandNotificationPanel(); } void ToolForm::on_touchBtn_clicked() @@ -187,19 +191,7 @@ void ToolForm::on_touchBtn_clicked() } m_showTouch = !m_showTouch; - - AdbProcess* adb = new AdbProcess(); - if (!adb) { - return; - } - connect(adb, &AdbProcess::adbProcessResult, this, [this](AdbProcess::ADB_EXEC_RESULT processResult){ - if (AdbProcess::AER_SUCCESS_START != processResult) { - sender()->deleteLater(); - } - }); - adb->setShowTouchesEnabled(m_device->getSerial(), m_showTouch); - - qInfo() << "show touch " << (m_showTouch ? "enable" : "disable"); + emit m_device->showTouch(m_showTouch); } void ToolForm::on_groupControlBtn_clicked() @@ -207,6 +199,6 @@ void ToolForm::on_groupControlBtn_clicked() if (!m_device) { return; } - m_device->setMainControl(!m_device->mainControl()); + emit m_device->setMainControl(m_device, !m_device->mainControl()); updateGroupControl(); } diff --git a/QtScrcpy/device/ui/toolform.h b/QtScrcpy/device/ui/toolform.h index 678a984..28f38c5 100644 --- a/QtScrcpy/device/ui/toolform.h +++ b/QtScrcpy/device/ui/toolform.h @@ -29,34 +29,19 @@ protected: void showEvent(QShowEvent *event); void hideEvent(QHideEvent *event); -signals: - void screenshot(); - private slots: void on_fullScreenBtn_clicked(); - void on_returnBtn_clicked(); - void on_homeBtn_clicked(); - void on_menuBtn_clicked(); - void on_appSwitchBtn_clicked(); - void on_powerBtn_clicked(); - void on_screenShotBtn_clicked(); - void on_volumeUpBtn_clicked(); - void on_volumeDownBtn_clicked(); - void on_closeScreenBtn_clicked(); - void on_expandNotifyBtn_clicked(); - void on_touchBtn_clicked(); - void on_groupControlBtn_clicked(); private: diff --git a/QtScrcpy/device/ui/videoform.cpp b/QtScrcpy/device/ui/videoform.cpp index 63b2f4e..5972608 100644 --- a/QtScrcpy/device/ui/videoform.cpp +++ b/QtScrcpy/device/ui/videoform.cpp @@ -100,7 +100,6 @@ void VideoForm::showToolForm(bool show) if (!m_toolForm) { m_toolForm = new ToolForm(this, ToolForm::AP_OUTSIDE_RIGHT); m_toolForm->setDevice(m_device); - connect(m_toolForm, &ToolForm::screenshot, this, &VideoForm::screenshot); } m_toolForm->move(pos().x() + geometry().width(), pos().y() + 30); m_toolForm->setVisible(show); @@ -174,7 +173,7 @@ void VideoForm::updateShowSize(const QSize &newSize) } if (isFullScreen()) { - switchFullScreen(); + onSwitchFullScreen(); } if (m_skin) { QMargins m = getMargins(vertical); @@ -192,7 +191,7 @@ void VideoForm::updateShowSize(const QSize &newSize) } } -void VideoForm::switchFullScreen() +void VideoForm::onSwitchFullScreen() { if (isFullScreen()) { // 横屏全屏铺满全屏,恢复时,恢复保持宽高比 @@ -263,11 +262,11 @@ void VideoForm::setDevice(Device *device) void VideoForm::mousePressEvent(QMouseEvent *event) { if (m_videoWidget->geometry().contains(event->pos())) { - if (!m_device || !m_device->getController()) { + if (!m_device) { return; } event->setLocalPos(m_videoWidget->mapFrom(this, event->localPos().toPoint())); - m_device->getController()->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); + emit m_device->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); } else { if (event->button() == Qt::LeftButton) { m_dragPosition = event->globalPos() - frameGeometry().topLeft(); @@ -279,7 +278,7 @@ void VideoForm::mousePressEvent(QMouseEvent *event) void VideoForm::mouseReleaseEvent(QMouseEvent *event) { if (m_dragPosition.isNull()) { - if (!m_device || !m_device->getController()) { + if (!m_device) { return; } event->setLocalPos(m_videoWidget->mapFrom(this, event->localPos().toPoint())); @@ -298,7 +297,7 @@ void VideoForm::mouseReleaseEvent(QMouseEvent *event) local.setY(m_videoWidget->height()); } event->setLocalPos(local); - m_device->getController()->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); + emit m_device->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); } else { m_dragPosition = QPoint(0, 0); } @@ -307,11 +306,11 @@ void VideoForm::mouseReleaseEvent(QMouseEvent *event) void VideoForm::mouseMoveEvent(QMouseEvent *event) { if (m_videoWidget->geometry().contains(event->pos())) { - if (!m_device || !m_device->getController()) { + if (!m_device) { return; } event->setLocalPos(m_videoWidget->mapFrom(this, event->localPos().toPoint())); - m_device->getController()->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); + emit m_device->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); } else if (!m_dragPosition.isNull()){ if (event->buttons() & Qt::LeftButton) { move(event->globalPos() - m_dragPosition); @@ -323,7 +322,7 @@ void VideoForm::mouseMoveEvent(QMouseEvent *event) void VideoForm::wheelEvent(QWheelEvent *event) { if (m_videoWidget->geometry().contains(event->pos())) { - if (!m_device || !m_device->getController()) { + if (!m_device) { return; } QPointF pos = m_videoWidget->mapFrom(this, event->pos()); @@ -334,7 +333,7 @@ void VideoForm::wheelEvent(QWheelEvent *event) */ QWheelEvent wheelEvent(pos, event->globalPosF(), event->delta(), event->buttons(), event->modifiers(), event->orientation()); - m_device->getController()->wheelEvent(&wheelEvent, m_videoWidget->frameSize(), m_videoWidget->size()); + emit m_device->wheelEvent(&wheelEvent, m_videoWidget->frameSize(), m_videoWidget->size()); } } @@ -343,32 +342,32 @@ void VideoForm::keyPressEvent(QKeyEvent *event) if (Qt::Key_Escape == event->key() && !event->isAutoRepeat() && isFullScreen()) { - switchFullScreen(); + onSwitchFullScreen(); } - if (!m_device || !m_device->getController()) { + if (!m_device) { return; } if (event->key() == Qt::Key_C && (event->modifiers() & Qt::ControlModifier)) { - m_device->getController()->requestDeviceClipboard(); + emit m_device->requestDeviceClipboard(); } if (event->key() == Qt::Key_V && (event->modifiers() & Qt::ControlModifier)) { if (event->modifiers() & Qt::ShiftModifier) { - m_device->getController()->setDeviceClipboard(); + emit m_device->setDeviceClipboard(); } else { - m_device->getController()->clipboardPaste(); + emit m_device->clipboardPaste(); } return; } - m_device->getController()->keyEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); + emit m_device->keyEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); } void VideoForm::keyReleaseEvent(QKeyEvent *event) { - if (!m_device || !m_device->getController()) { + if (!m_device) { return; } - m_device->getController()->keyEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); + emit m_device->keyEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); } void VideoForm::paintEvent(QPaintEvent *paint) @@ -431,7 +430,7 @@ void VideoForm::dragLeaveEvent(QDragLeaveEvent *event) void VideoForm::dropEvent(QDropEvent *event) { - if (!m_device || !m_device->getFileHandler()) { + if (!m_device) { return; } const QMimeData* qm = event->mimeData(); @@ -444,8 +443,8 @@ void VideoForm::dropEvent(QDropEvent *event) } if (fileInfo.isFile() && fileInfo.suffix() == "apk") { - m_device->getFileHandler()->installApkRequest(m_device->getSerial(), file); + emit m_device->installApkRequest(m_device->getSerial(), file); return; } - m_device->getFileHandler()->pushFileRequest(m_device->getSerial(), file, Config::getInstance().getPushFilePath() + fileInfo.fileName()); + emit m_device->pushFileRequest(m_device->getSerial(), file, Config::getInstance().getPushFilePath() + fileInfo.fileName()); } diff --git a/QtScrcpy/device/ui/videoform.h b/QtScrcpy/device/ui/videoform.h index 26e5631..7df3248 100644 --- a/QtScrcpy/device/ui/videoform.h +++ b/QtScrcpy/device/ui/videoform.h @@ -20,17 +20,14 @@ public: explicit VideoForm(bool skin = true, QWidget *parent = 0); ~VideoForm(); - void switchFullScreen(); void staysOnTop(bool top = true); void updateShowSize(const QSize &newSize); void updateRender(const AVFrame *frame); void setDevice(Device *device); -signals: - void screenshot(); - public slots: void onGrabCursor(bool grab); + void onSwitchFullScreen(); private: void updateStyleSheet(bool vertical); From 81a0e9544ccc574e3cf962a43bd82995c063515d Mon Sep 17 00:00:00 2001 From: rankun Date: Sat, 7 Mar 2020 15:25:16 +0800 Subject: [PATCH 04/19] refactor: GrabCursor --- QtScrcpy/device/ui/toolform.cpp | 3 -- QtScrcpy/device/ui/videoform.cpp | 29 ++++++++++++++++-- QtScrcpy/util/mousetap/cocoamousetap.h | 2 +- QtScrcpy/util/mousetap/cocoamousetap.mm | 39 +++++++------------------ QtScrcpy/util/mousetap/mousetap.h | 4 ++- QtScrcpy/util/mousetap/winmousetap.cpp | 9 ++---- QtScrcpy/util/mousetap/winmousetap.h | 2 +- 7 files changed, 43 insertions(+), 45 deletions(-) diff --git a/QtScrcpy/device/ui/toolform.cpp b/QtScrcpy/device/ui/toolform.cpp index 6fe898a..c21d142 100644 --- a/QtScrcpy/device/ui/toolform.cpp +++ b/QtScrcpy/device/ui/toolform.cpp @@ -7,9 +7,6 @@ #include "ui_toolform.h" #include "iconhelper.h" #include "device.h" -#include "videoform.h" -#include "controller.h" -#include "adbprocess.h" ToolForm::ToolForm(QWidget* adsorbWidget, AdsorbPositions adsorbPos) : MagneticWidget(adsorbWidget, adsorbPos) diff --git a/QtScrcpy/device/ui/videoform.cpp b/QtScrcpy/device/ui/videoform.cpp index 5972608..69b0cad 100644 --- a/QtScrcpy/device/ui/videoform.cpp +++ b/QtScrcpy/device/ui/videoform.cpp @@ -17,7 +17,6 @@ #include "toolform.h" #include "device.h" #include "controller.h" -#include "filehandler.h" #include "config.h" extern "C" { @@ -73,8 +72,32 @@ void VideoForm::initUI() void VideoForm::onGrabCursor(bool grab) { -#if defined(Q_OS_WIN32) || defined(Q_OS_OSX) - MouseTap::getInstance()->enableMouseEventTap(m_videoWidget, grab); +#if defined(Q_OS_WIN32) + QRect rc; + rc = QRect(m_videoWidget->parentWidget()->mapToGlobal(m_videoWidget->pos()) + , m_videoWidget->size()); + // high dpi support + rc.setTopLeft(rc.topLeft() * m_videoWidget->devicePixelRatio()); + rc.setBottomRight(rc.bottomRight() * m_videoWidget->devicePixelRatio()); + MouseTap::getInstance()->enableMouseEventTap(rc, grab); +#elif defined(Q_OS_OSX) + // get nswindow from qt widget + NSView *nsview = (NSView *)m_videoWidget->window()->winId(); + if (!nsview) { + return; + } + NSWindow *nswindow = [nsview window]; + + NSRect windowRect = [nswindow contentRectForFrameRect:[nswindow frame]]; + QRect rc(windowRect.origin.x, windowRect.origin.y, + windowRect.size.width, windowRect.size.height); + + rc.setX(rc.x() + 100); + rc.setY(rc.y() + 30); + rc.setWidth(rc.width() - 180); + rc.setHeight(rc.height() - 60); + + MouseTap::getInstance()->enableMouseEventTap(rc, grab); #else Q_UNUSED(grab) #endif diff --git a/QtScrcpy/util/mousetap/cocoamousetap.h b/QtScrcpy/util/mousetap/cocoamousetap.h index 419887b..e1757f8 100644 --- a/QtScrcpy/util/mousetap/cocoamousetap.h +++ b/QtScrcpy/util/mousetap/cocoamousetap.h @@ -15,7 +15,7 @@ public: void initMouseEventTap() override; void quitMouseEventTap() override; - void enableMouseEventTap(QWidget* widget, bool enabled) override; + void enableMouseEventTap(QRect rc, bool enabled) override; protected: void run() override; diff --git a/QtScrcpy/util/mousetap/cocoamousetap.mm b/QtScrcpy/util/mousetap/cocoamousetap.mm index c03827c..78afca8 100644 --- a/QtScrcpy/util/mousetap/cocoamousetap.mm +++ b/QtScrcpy/util/mousetap/cocoamousetap.mm @@ -1,6 +1,5 @@ #import #include -#include #include "cocoamousetap.h" @@ -20,55 +19,37 @@ typedef struct MouseEventTapData{ CFMachPortRef tap = Q_NULLPTR; CFRunLoopRef runloop = Q_NULLPTR; CFRunLoopSourceRef runloopSource = Q_NULLPTR; - QWidget* widget = Q_NULLPTR; + QRect rc; } MouseEventTapData; static CGEventRef Cocoa_MouseTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) { Q_UNUSED(proxy); MouseEventTapData *tapdata = (MouseEventTapData*)refcon; - - NSView *nsview; - NSWindow *nswindow; - NSRect windowRect; - NSRect newWindowRect; - CGPoint eventLocation; - switch (type) { case kCGEventTapDisabledByTimeout: { CGEventTapEnable(tapdata->tap, true); - return NULL; + return nullptr; } case kCGEventTapDisabledByUserInput: { - return NULL; + return nullptr; } default: break; } - if (!tapdata->widget) { + if (tapdata->rc.isEmpty()) { return event; } - // get nswindow from qt widget - nsview = (NSView *)tapdata->widget->window()->winId(); - if (!nsview) { - return event; - } - nswindow = [nsview window]; - eventLocation = CGEventGetUnflippedLocation(event); - windowRect = [nswindow contentRectForFrameRect:[nswindow frame]]; - - windowRect.origin.x += 100; - windowRect.origin.y += 30; - windowRect.size.width -= 180; - windowRect.size.height -= 60; - - newWindowRect = NSMakeRect(windowRect.origin.x, windowRect.origin.y, + NSRect windowRect = NSMakeRect(tapdata->rc.left(), tapdata->rc.top(), + tapdata->rc.width(), tapdata->rc.height()); + NSRect newWindowRect = NSMakeRect(windowRect.origin.x, windowRect.origin.y, windowRect.size.width - 10, windowRect.size.height - 10); + CGPoint eventLocation = CGEventGetUnflippedLocation(event); //qDebug() << newWindowRect.origin.x << newWindowRect.origin.y << newWindowRect.size.width << newWindowRect.size.height; if (!NSMouseInRect(NSPointFromCGPoint(eventLocation), newWindowRect, NO)) { @@ -171,11 +152,11 @@ void CocoaMouseTap::quitMouseEventTap() } } -void CocoaMouseTap::enableMouseEventTap(QWidget* widget, bool enabled) +void CocoaMouseTap::enableMouseEventTap(QRect rc, bool enabled) { if (m_tapData && m_tapData->tap) { - enabled ? m_tapData->widget = widget : m_tapData->widget = Q_NULLPTR; + enabled ? m_tapData->rc = rc : m_tapData->rc = QRect(); CGEventTapEnable(m_tapData->tap, enabled); } } diff --git a/QtScrcpy/util/mousetap/mousetap.h b/QtScrcpy/util/mousetap/mousetap.h index eab1389..ce1e323 100644 --- a/QtScrcpy/util/mousetap/mousetap.h +++ b/QtScrcpy/util/mousetap/mousetap.h @@ -1,12 +1,14 @@ #ifndef MOUSETAP_H #define MOUSETAP_H +#include + class QWidget; class MouseTap { public: static MouseTap* getInstance(); virtual void initMouseEventTap() = 0; virtual void quitMouseEventTap() = 0; - virtual void enableMouseEventTap(QWidget* widget, bool enabled) = 0; + virtual void enableMouseEventTap(QRect rc, bool enabled) = 0; private: static MouseTap *s_instance; diff --git a/QtScrcpy/util/mousetap/winmousetap.cpp b/QtScrcpy/util/mousetap/winmousetap.cpp index 7303998..ecbf369 100644 --- a/QtScrcpy/util/mousetap/winmousetap.cpp +++ b/QtScrcpy/util/mousetap/winmousetap.cpp @@ -24,17 +24,12 @@ void WinMouseTap::quitMouseEventTap() } -void WinMouseTap::enableMouseEventTap(QWidget *widget, bool enabled) +void WinMouseTap::enableMouseEventTap(QRect rc, bool enabled) { - if (!widget) { + if (enabled && rc.isEmpty()) { return; } if(enabled) { - QRect rc(widget->parentWidget()->mapToGlobal(widget->pos()) - , widget->size()); - // high dpi support - rc.setTopLeft(rc.topLeft() * widget->devicePixelRatio()); - rc.setBottomRight(rc.bottomRight() * widget->devicePixelRatio()); RECT mainRect; mainRect.left = (LONG)rc.left(); mainRect.right = (LONG)rc.right(); diff --git a/QtScrcpy/util/mousetap/winmousetap.h b/QtScrcpy/util/mousetap/winmousetap.h index 9b23f68..a5d4de1 100644 --- a/QtScrcpy/util/mousetap/winmousetap.h +++ b/QtScrcpy/util/mousetap/winmousetap.h @@ -11,7 +11,7 @@ public: void initMouseEventTap() override; void quitMouseEventTap() override; - void enableMouseEventTap(QWidget* widget, bool enabled) override; + void enableMouseEventTap(QRect rc, bool enabled) override; }; #endif // WINMOUSETAP_H From ad2243b0351d3d262ba09b97aa5f2ad0c569e578 Mon Sep 17 00:00:00 2001 From: rankun Date: Sat, 7 Mar 2020 21:49:39 +0800 Subject: [PATCH 05/19] fix: mousetap on mac --- QtScrcpy/device/ui/videoform.cpp | 14 ++------- QtScrcpy/util/mousetap/cocoamousetap.mm | 42 +++++++++++-------------- QtScrcpy/util/mousetap/mousetap.h | 1 + 3 files changed, 23 insertions(+), 34 deletions(-) diff --git a/QtScrcpy/device/ui/videoform.cpp b/QtScrcpy/device/ui/videoform.cpp index 69b0cad..f3e2aa3 100644 --- a/QtScrcpy/device/ui/videoform.cpp +++ b/QtScrcpy/device/ui/videoform.cpp @@ -81,17 +81,9 @@ void VideoForm::onGrabCursor(bool grab) rc.setBottomRight(rc.bottomRight() * m_videoWidget->devicePixelRatio()); MouseTap::getInstance()->enableMouseEventTap(rc, grab); #elif defined(Q_OS_OSX) - // get nswindow from qt widget - NSView *nsview = (NSView *)m_videoWidget->window()->winId(); - if (!nsview) { - return; - } - NSWindow *nswindow = [nsview window]; - - NSRect windowRect = [nswindow contentRectForFrameRect:[nswindow frame]]; - QRect rc(windowRect.origin.x, windowRect.origin.y, - windowRect.size.width, windowRect.size.height); - + QRect rc = m_videoWidget->geometry(); + rc.setTopLeft(m_videoWidget->mapToGlobal(rc.topLeft())); + rc.setBottomRight(m_videoWidget->mapToGlobal(rc.bottomRight())); rc.setX(rc.x() + 100); rc.setY(rc.y() + 30); rc.setWidth(rc.width() - 180); diff --git a/QtScrcpy/util/mousetap/cocoamousetap.mm b/QtScrcpy/util/mousetap/cocoamousetap.mm index 78afca8..67c009f 100644 --- a/QtScrcpy/util/mousetap/cocoamousetap.mm +++ b/QtScrcpy/util/mousetap/cocoamousetap.mm @@ -45,33 +45,29 @@ static CGEventRef Cocoa_MouseTapCallback(CGEventTapProxy proxy, CGEventType type return event; } - NSRect windowRect = NSMakeRect(tapdata->rc.left(), tapdata->rc.top(), + NSRect limitWindowRect = NSMakeRect(tapdata->rc.left(), tapdata->rc.top(), tapdata->rc.width(), tapdata->rc.height()); - NSRect newWindowRect = NSMakeRect(windowRect.origin.x, windowRect.origin.y, - windowRect.size.width - 10, windowRect.size.height - 10); - CGPoint eventLocation = CGEventGetUnflippedLocation(event); - //qDebug() << newWindowRect.origin.x << newWindowRect.origin.y << newWindowRect.size.width << newWindowRect.size.height; - - if (!NSMouseInRect(NSPointFromCGPoint(eventLocation), newWindowRect, NO)) { - - /* This is in CGs global screenspace coordinate system, which has a - * flipped Y. - */ - CGPoint newLocation = CGEventGetLocation(event); - - if (eventLocation.x < NSMinX(windowRect)) { - newLocation.x = NSMinX(windowRect); - } else if (eventLocation.x >= NSMaxX(windowRect)) { - newLocation.x = NSMaxX(windowRect) - 1.0; + // check rect samll than limit rect + NSRect checkWindowRect = NSMakeRect(limitWindowRect.origin.x + 10, limitWindowRect.origin.y + 10, + limitWindowRect.size.width - 10, limitWindowRect.size.height - 10); + /* This is in CGs global screenspace coordinate system, which has a + * flipped Y. + */ + CGPoint eventLocation = CGEventGetLocation(event); + if (!NSMouseInRect(NSPointFromCGPoint(eventLocation), checkWindowRect, NO)) { + if (eventLocation.x <= NSMinX(limitWindowRect)) { + eventLocation.x = NSMinX(limitWindowRect) + 1.0; + } else if (eventLocation.x >= NSMaxX(limitWindowRect)) { + eventLocation.x = NSMaxX(limitWindowRect) - 1.0; } - if (eventLocation.y <= NSMinY(windowRect)) { - newLocation.y -= (NSMinY(windowRect) - eventLocation.y + 1); - } else if (eventLocation.y > NSMaxY(windowRect)) { - newLocation.y += (eventLocation.y - NSMaxY(windowRect)); + if (eventLocation.y <= NSMinY(limitWindowRect)) { + eventLocation.y = NSMinY(limitWindowRect) + 1.0; + } else if (eventLocation.y >= NSMaxY(limitWindowRect)) { + eventLocation.y = NSMaxY(limitWindowRect) - 1.0; } - CGWarpMouseCursorPosition(newLocation); + CGWarpMouseCursorPosition(eventLocation); CGAssociateMouseAndMouseCursorPosition(YES); if ((CGEventMaskBit(type) & movementEventsMask) == 0) { @@ -80,7 +76,7 @@ static CGEventRef Cocoa_MouseTapCallback(CGEventTapProxy proxy, CGEventType type * movement events, since they mean that our warp cursor above * behaves strangely. */ - CGEventSetLocation(event, newLocation); + CGEventSetLocation(event, eventLocation); } } diff --git a/QtScrcpy/util/mousetap/mousetap.h b/QtScrcpy/util/mousetap/mousetap.h index ce1e323..8cad8f9 100644 --- a/QtScrcpy/util/mousetap/mousetap.h +++ b/QtScrcpy/util/mousetap/mousetap.h @@ -8,6 +8,7 @@ public: static MouseTap* getInstance(); virtual void initMouseEventTap() = 0; virtual void quitMouseEventTap() = 0; + // rc base global screenspace coordinate system, which has a flipped Y. virtual void enableMouseEventTap(QRect rc, bool enabled) = 0; private: From 8f8edd4bae7ef3c8b652dcbd2f337ef96847f366 Mon Sep 17 00:00:00 2001 From: rankun Date: Sun, 8 Mar 2020 09:10:09 +0800 Subject: [PATCH 06/19] refactor: onGrabCursor --- QtScrcpy/device/device.cpp | 15 +++++++++++++-- QtScrcpy/device/device.h | 22 +++++++++++++--------- QtScrcpy/device/ui/videoform.cpp | 13 +++++-------- QtScrcpy/device/ui/videoform.h | 2 +- 4 files changed, 32 insertions(+), 20 deletions(-) diff --git a/QtScrcpy/device/device.cpp b/QtScrcpy/device/device.cpp index bde630e..15d6414 100644 --- a/QtScrcpy/device/device.cpp +++ b/QtScrcpy/device/device.cpp @@ -13,6 +13,7 @@ #include "controller.h" #include "config.h" #include "avframeconvert.h" +#include "mousetap/mousetap.h" extern "C" { #include "libavutil/imgutils.h" @@ -130,9 +131,10 @@ void Device::initSignals() connect(this, &Device::screenshot, this, &Device::onScreenshot); connect(this, &Device::showTouch, this, &Device::onShowTouch); connect(this, &Device::setMainControl, this, &Device::onSetMainControl); + connect(this, &Device::grabCursor, this, &Device::onGrabCursor); - if (m_controller && m_videoForm) { - connect(m_controller, &Controller::grabCursor, m_videoForm, &VideoForm::onGrabCursor); + if (m_controller) { + connect(m_controller, &Controller::grabCursor, this, &Device::grabCursor); } if (m_controller) { connect(this, &Device::postGoBack, m_controller, &Controller::onPostGoBack); @@ -287,6 +289,15 @@ void Device::onSetMainControl(Device* device, bool mainControl) m_mainControl = mainControl; } +void Device::onGrabCursor(bool grab) +{ + if (!m_videoForm) { + return; + } + QRect rc = m_videoForm->getGrabCursorRect(); + MouseTap::getInstance()->enableMouseEventTap(rc, grab); +} + bool Device::mainControl() { return m_mainControl; diff --git a/QtScrcpy/device/device.h b/QtScrcpy/device/device.h index c5363b8..3a28f37 100644 --- a/QtScrcpy/device/device.h +++ b/QtScrcpy/device/device.h @@ -48,6 +48,7 @@ public: signals: void deviceDisconnect(QString serial); + // tool bar void switchFullScreen(); void postGoBack(); void postGoHome(); @@ -58,27 +59,30 @@ signals: void postVolumeDown(); void setScreenPowerMode(ControlMsg::ScreenPowerMode mode); void expandNotificationPanel(); - void screenshot(); - void showTouch(bool show); - void setMainControl(Device* device, bool mainControl); - - void mouseEvent(const QMouseEvent* from, const QSize& frameSize, const QSize& showSize); - void wheelEvent(const QWheelEvent* from, const QSize& frameSize, const QSize& showSize); - void keyEvent(const QKeyEvent* from, const QSize& frameSize, const QSize& showSize); - void postTurnOn(); void postTextInput(QString& text); void requestDeviceClipboard(); void setDeviceClipboard(); void clipboardPaste(); - void pushFileRequest(const QString& serial, const QString& file, const QString& devicePath = ""); void installApkRequest(const QString& serial, const QString& apkFile); + // key map + void mouseEvent(const QMouseEvent* from, const QSize& frameSize, const QSize& showSize); + void wheelEvent(const QWheelEvent* from, const QSize& frameSize, const QSize& showSize); + void keyEvent(const QKeyEvent* from, const QSize& frameSize, const QSize& showSize); + + // self connect signal and slots + void screenshot(); + void showTouch(bool show); + void setMainControl(Device* device, bool mainControl); + void grabCursor(bool grab); + public slots: void onScreenshot(); void onShowTouch(bool show); void onSetMainControl(Device* device, bool mainControl); + void onGrabCursor(bool grab); private: void initSignals(); diff --git a/QtScrcpy/device/ui/videoform.cpp b/QtScrcpy/device/ui/videoform.cpp index f3e2aa3..c9d9c40 100644 --- a/QtScrcpy/device/ui/videoform.cpp +++ b/QtScrcpy/device/ui/videoform.cpp @@ -11,7 +11,6 @@ #include "videoform.h" #include "qyuvopenglwidget.h" -#include "mousetap/mousetap.h" #include "ui_videoform.h" #include "iconhelper.h" #include "toolform.h" @@ -70,29 +69,27 @@ void VideoForm::initUI() ui->keepRadioWidget->setMouseTracking(true); } -void VideoForm::onGrabCursor(bool grab) +QRect VideoForm::getGrabCursorRect() { -#if defined(Q_OS_WIN32) QRect rc; - rc = QRect(m_videoWidget->parentWidget()->mapToGlobal(m_videoWidget->pos()) +#if defined(Q_OS_WIN32) + rc = QRect(m_videoWidget->mapToGlobal(m_videoWidget->pos()) , m_videoWidget->size()); // high dpi support rc.setTopLeft(rc.topLeft() * m_videoWidget->devicePixelRatio()); rc.setBottomRight(rc.bottomRight() * m_videoWidget->devicePixelRatio()); - MouseTap::getInstance()->enableMouseEventTap(rc, grab); #elif defined(Q_OS_OSX) - QRect rc = m_videoWidget->geometry(); + rc = m_videoWidget->geometry(); rc.setTopLeft(m_videoWidget->mapToGlobal(rc.topLeft())); rc.setBottomRight(m_videoWidget->mapToGlobal(rc.bottomRight())); rc.setX(rc.x() + 100); rc.setY(rc.y() + 30); rc.setWidth(rc.width() - 180); rc.setHeight(rc.height() - 60); - - MouseTap::getInstance()->enableMouseEventTap(rc, grab); #else Q_UNUSED(grab) #endif + return rc; } void VideoForm::updateRender(const AVFrame *frame) diff --git a/QtScrcpy/device/ui/videoform.h b/QtScrcpy/device/ui/videoform.h index 7df3248..6f573a0 100644 --- a/QtScrcpy/device/ui/videoform.h +++ b/QtScrcpy/device/ui/videoform.h @@ -24,9 +24,9 @@ public: void updateShowSize(const QSize &newSize); void updateRender(const AVFrame *frame); void setDevice(Device *device); + QRect getGrabCursorRect(); public slots: - void onGrabCursor(bool grab); void onSwitchFullScreen(); private: From 54df9468c96aa1651c168e04a705d17ad8b95217 Mon Sep 17 00:00:00 2001 From: rankun Date: Sun, 8 Mar 2020 09:52:30 +0800 Subject: [PATCH 07/19] feat: ready for group control --- QtScrcpy/device/device.cpp | 18 +++++++++++------- QtScrcpy/device/device.h | 16 ++++++++++++---- QtScrcpy/device/ui/toolform.cpp | 30 ++++++++++++++++++++++++++---- QtScrcpy/device/ui/toolform.h | 3 +++ QtScrcpy/device/ui/videoform.cpp | 12 ++++++------ 5 files changed, 58 insertions(+), 21 deletions(-) diff --git a/QtScrcpy/device/device.cpp b/QtScrcpy/device/device.cpp index 15d6414..0f027ea 100644 --- a/QtScrcpy/device/device.cpp +++ b/QtScrcpy/device/device.cpp @@ -130,7 +130,7 @@ void Device::initSignals() { connect(this, &Device::screenshot, this, &Device::onScreenshot); connect(this, &Device::showTouch, this, &Device::onShowTouch); - connect(this, &Device::setMainControl, this, &Device::onSetMainControl); + connect(this, &Device::setControlState, this, &Device::onSetControlState); connect(this, &Device::grabCursor, this, &Device::onGrabCursor); if (m_controller) { @@ -185,7 +185,7 @@ void Device::initSignals() tips = tr("%1 failed").arg(tipsType); } qInfo() << tips; - if (!m_mainControl) { + if (m_controlState == GCS_CLIENT) { return; } QMessageBox::information(m_videoForm, "QtScrcpy", tips, QMessageBox::Ok); @@ -280,13 +280,14 @@ void Device::startServer() }); } -void Device::onSetMainControl(Device* device, bool mainControl) +void Device::onSetControlState(Device* device, Device::GroupControlState state) { Q_UNUSED(device) - if (m_mainControl == mainControl) { + if (m_controlState == state) { return; } - m_mainControl = mainControl; + m_controlState = state; + emit controlStateChange(this, m_controlState); } void Device::onGrabCursor(bool grab) @@ -294,13 +295,16 @@ void Device::onGrabCursor(bool grab) if (!m_videoForm) { return; } + if (m_controlState == GCS_CLIENT) { + return; + } QRect rc = m_videoForm->getGrabCursorRect(); MouseTap::getInstance()->enableMouseEventTap(rc, grab); } -bool Device::mainControl() +Device::GroupControlState Device::controlState() { - return m_mainControl; + return m_controlState; } bool Device::saveFrame(const AVFrame* frame) diff --git a/QtScrcpy/device/device.h b/QtScrcpy/device/device.h index 3a28f37..9a43b91 100644 --- a/QtScrcpy/device/device.h +++ b/QtScrcpy/device/device.h @@ -35,6 +35,11 @@ public: QString gameScript = ""; // 游戏映射脚本 bool renderExpiredFrames = false; // 是否渲染延迟视频帧 }; + enum GroupControlState { + GCS_FREE = 0, + GCS_HOST, + GCS_CLIENT, + }; explicit Device(DeviceParams params, QObject *parent = nullptr); virtual ~Device(); @@ -43,7 +48,7 @@ public: const QString &getSerial(); void updateScript(QString script); - bool mainControl(); + Device::GroupControlState controlState(); signals: void deviceDisconnect(QString serial); @@ -75,13 +80,16 @@ signals: // self connect signal and slots void screenshot(); void showTouch(bool show); - void setMainControl(Device* device, bool mainControl); + void setControlState(Device* device, Device::GroupControlState state); void grabCursor(bool grab); + // for notify + void controlStateChange(Device* device, Device::GroupControlState state); + public slots: void onScreenshot(); void onShowTouch(bool show); - void onSetMainControl(Device* device, bool mainControl); + void onSetControlState(Device* device, Device::GroupControlState state); void onGrabCursor(bool grab); private: @@ -105,7 +113,7 @@ private: QTime m_startTimeCount; DeviceParams m_params; - bool m_mainControl = false; + GroupControlState m_controlState = GCS_FREE; }; #endif // DEVICE_H diff --git a/QtScrcpy/device/ui/toolform.cpp b/QtScrcpy/device/ui/toolform.cpp index c21d142..dc7172b 100644 --- a/QtScrcpy/device/ui/toolform.cpp +++ b/QtScrcpy/device/ui/toolform.cpp @@ -26,7 +26,11 @@ ToolForm::~ToolForm() void ToolForm::setDevice(Device *device) { + if (!device) { + return; + } m_device = device; + connect(m_device, &Device::controlStateChange, this, &ToolForm::onControlStateChange); } void ToolForm::initStyle() @@ -52,10 +56,16 @@ void ToolForm::updateGroupControl() if (!m_device) { return; } - if (m_device->mainControl()) { - ui->groupControlBtn->setStyleSheet("color: red"); - } else { + switch (m_device->controlState()) { + case Device::GroupControlState::GCS_FREE: ui->groupControlBtn->setStyleSheet("color: #DCDCDC"); + break; + case Device::GroupControlState::GCS_HOST: + ui->groupControlBtn->setStyleSheet("color: red"); + break; + case Device::GroupControlState::GCS_CLIENT: + ui->groupControlBtn->setStyleSheet("color: green"); + break; } } @@ -196,6 +206,18 @@ void ToolForm::on_groupControlBtn_clicked() if (!m_device) { return; } - emit m_device->setMainControl(m_device, !m_device->mainControl()); + Device::GroupControlState state = m_device->controlState(); + if (state == Device::GroupControlState::GCS_FREE) { + emit m_device->setControlState(m_device, Device::GroupControlState::GCS_HOST); + } + if (state == Device::GroupControlState::GCS_HOST) { + emit m_device->setControlState(m_device, Device::GroupControlState::GCS_FREE); + } +} + +void ToolForm::onControlStateChange(Device *device, Device::GroupControlState state) +{ + Q_UNUSED(device) + Q_UNUSED(state) updateGroupControl(); } diff --git a/QtScrcpy/device/ui/toolform.h b/QtScrcpy/device/ui/toolform.h index 28f38c5..1c4d10b 100644 --- a/QtScrcpy/device/ui/toolform.h +++ b/QtScrcpy/device/ui/toolform.h @@ -5,6 +5,7 @@ #include #include "magneticwidget.h" +#include "device.h" namespace Ui { class ToolForm; @@ -44,6 +45,8 @@ private slots: void on_touchBtn_clicked(); void on_groupControlBtn_clicked(); + void onControlStateChange(Device* device, Device::GroupControlState state); + private: void initStyle(); void updateGroupControl(); diff --git a/QtScrcpy/device/ui/videoform.cpp b/QtScrcpy/device/ui/videoform.cpp index c9d9c40..ea4a2c9 100644 --- a/QtScrcpy/device/ui/videoform.cpp +++ b/QtScrcpy/device/ui/videoform.cpp @@ -184,8 +184,8 @@ void VideoForm::updateShowSize(const QSize &newSize) showSize.setHeight(showSize.width() / m_widthHeightRatio); } - if (isFullScreen()) { - onSwitchFullScreen(); + if (isFullScreen() && m_device) { + emit m_device->switchFullScreen(); } if (m_skin) { QMargins m = getMargins(vertical); @@ -351,13 +351,13 @@ void VideoForm::wheelEvent(QWheelEvent *event) void VideoForm::keyPressEvent(QKeyEvent *event) { + if (!m_device) { + return; + } if (Qt::Key_Escape == event->key() && !event->isAutoRepeat() && isFullScreen()) { - onSwitchFullScreen(); - } - if (!m_device) { - return; + emit m_device->switchFullScreen(); } if (event->key() == Qt::Key_C && (event->modifiers() & Qt::ControlModifier)) { emit m_device->requestDeviceClipboard(); From 3487fb7d2b582b11151a8a542e951485887b516c Mon Sep 17 00:00:00 2001 From: rankun Date: Sun, 8 Mar 2020 11:29:33 +0800 Subject: [PATCH 08/19] feat: group control --- QtScrcpy/device/device.cpp | 3 +- QtScrcpy/device/device.h | 2 +- QtScrcpy/device/ui/toolform.cpp | 5 +- QtScrcpy/device/ui/toolform.h | 2 +- QtScrcpy/devicemanage/devicemanage.cpp | 105 +++++++++++++++++++++++++ QtScrcpy/devicemanage/devicemanage.h | 5 ++ 6 files changed, 117 insertions(+), 5 deletions(-) diff --git a/QtScrcpy/device/device.cpp b/QtScrcpy/device/device.cpp index 0f027ea..9dc12eb 100644 --- a/QtScrcpy/device/device.cpp +++ b/QtScrcpy/device/device.cpp @@ -286,8 +286,9 @@ void Device::onSetControlState(Device* device, Device::GroupControlState state) if (m_controlState == state) { return; } + GroupControlState oldState = m_controlState; m_controlState = state; - emit controlStateChange(this, m_controlState); + emit controlStateChange(this, oldState, m_controlState); } void Device::onGrabCursor(bool grab) diff --git a/QtScrcpy/device/device.h b/QtScrcpy/device/device.h index 9a43b91..cc7dd6d 100644 --- a/QtScrcpy/device/device.h +++ b/QtScrcpy/device/device.h @@ -84,7 +84,7 @@ signals: void grabCursor(bool grab); // for notify - void controlStateChange(Device* device, Device::GroupControlState state); + void controlStateChange(Device* device, Device::GroupControlState oldState, Device::GroupControlState newState); public slots: void onScreenshot(); diff --git a/QtScrcpy/device/ui/toolform.cpp b/QtScrcpy/device/ui/toolform.cpp index dc7172b..e640bd4 100644 --- a/QtScrcpy/device/ui/toolform.cpp +++ b/QtScrcpy/device/ui/toolform.cpp @@ -215,9 +215,10 @@ void ToolForm::on_groupControlBtn_clicked() } } -void ToolForm::onControlStateChange(Device *device, Device::GroupControlState state) +void ToolForm::onControlStateChange(Device *device, Device::GroupControlState oldState, Device::GroupControlState newState) { Q_UNUSED(device) - Q_UNUSED(state) + Q_UNUSED(oldState) + Q_UNUSED(newState) updateGroupControl(); } diff --git a/QtScrcpy/device/ui/toolform.h b/QtScrcpy/device/ui/toolform.h index 1c4d10b..037e55c 100644 --- a/QtScrcpy/device/ui/toolform.h +++ b/QtScrcpy/device/ui/toolform.h @@ -45,7 +45,7 @@ private slots: void on_touchBtn_clicked(); void on_groupControlBtn_clicked(); - void onControlStateChange(Device* device, Device::GroupControlState state); + void onControlStateChange(Device* device, Device::GroupControlState oldState, Device::GroupControlState newState); private: void initStyle(); diff --git a/QtScrcpy/devicemanage/devicemanage.cpp b/QtScrcpy/devicemanage/devicemanage.cpp index 8379911..f0d0e44 100644 --- a/QtScrcpy/devicemanage/devicemanage.cpp +++ b/QtScrcpy/devicemanage/devicemanage.cpp @@ -1,4 +1,7 @@ #include +#include +#include +#include #include "devicemanage.h" #include "server.h" @@ -44,6 +47,7 @@ bool DeviceManage::connectDevice(Device::DeviceParams params) */ Device *device = new Device(params); connect(device, &Device::deviceDisconnect, this, &DeviceManage::onDeviceDisconnect); + connect(device, &Device::controlStateChange, this, &DeviceManage::onControlStateChange); m_devices[params.serial] = device; return true; } @@ -102,13 +106,114 @@ void DeviceManage::disconnectAllDevice() } } +void DeviceManage::setGroupControlSignals(Device *host, Device *client, bool install) +{ + if (!host || !client) { + return; + } + if (install) { + connect(host, &Device::postGoBack, client, &Device::postGoBack); + connect(host, &Device::postGoHome, client, &Device::postGoHome); + connect(host, &Device::postGoMenu, client, &Device::postGoMenu); + connect(host, &Device::postAppSwitch, client, &Device::postAppSwitch); + connect(host, &Device::postPower, client, &Device::postPower); + connect(host, &Device::postVolumeUp, client, &Device::postVolumeUp); + connect(host, &Device::postVolumeDown, client, &Device::postVolumeDown); + connect(host, &Device::setScreenPowerMode, client, &Device::setScreenPowerMode); + connect(host, &Device::expandNotificationPanel, client, &Device::expandNotificationPanel); + connect(host, &Device::postTurnOn, client, &Device::postTurnOn); + connect(host, &Device::postTextInput, client, &Device::postTextInput); + connect(host, &Device::setDeviceClipboard, client, &Device::setDeviceClipboard); + connect(host, &Device::clipboardPaste, client, &Device::clipboardPaste); + connect(host, &Device::pushFileRequest, client, &Device::pushFileRequest); + connect(host, &Device::installApkRequest, client, &Device::installApkRequest); + connect(host, &Device::mouseEvent, client, &Device::mouseEvent); + connect(host, &Device::wheelEvent, client, &Device::wheelEvent); + connect(host, &Device::keyEvent, client, &Device::keyEvent); + connect(host, &Device::screenshot, client, &Device::screenshot); + connect(host, &Device::showTouch, client, &Device::showTouch); + // dont connect requestDeviceClipboard + //connect(host, &Device::requestDeviceClipboard, client, &Device::requestDeviceClipboard); + } else { + disconnect(host, &Device::postGoBack, client, &Device::postGoBack); + disconnect(host, &Device::postGoHome, client, &Device::postGoHome); + disconnect(host, &Device::postGoMenu, client, &Device::postGoMenu); + disconnect(host, &Device::postAppSwitch, client, &Device::postAppSwitch); + disconnect(host, &Device::postPower, client, &Device::postPower); + disconnect(host, &Device::postVolumeUp, client, &Device::postVolumeUp); + disconnect(host, &Device::postVolumeDown, client, &Device::postVolumeDown); + disconnect(host, &Device::setScreenPowerMode, client, &Device::setScreenPowerMode); + disconnect(host, &Device::expandNotificationPanel, client, &Device::expandNotificationPanel); + disconnect(host, &Device::postTurnOn, client, &Device::postTurnOn); + disconnect(host, &Device::postTextInput, client, &Device::postTextInput); + disconnect(host, &Device::setDeviceClipboard, client, &Device::setDeviceClipboard); + disconnect(host, &Device::clipboardPaste, client, &Device::clipboardPaste); + disconnect(host, &Device::pushFileRequest, client, &Device::pushFileRequest); + disconnect(host, &Device::installApkRequest, client, &Device::installApkRequest); + disconnect(host, &Device::mouseEvent, client, &Device::mouseEvent); + disconnect(host, &Device::wheelEvent, client, &Device::wheelEvent); + disconnect(host, &Device::keyEvent, client, &Device::keyEvent); + disconnect(host, &Device::screenshot, client, &Device::screenshot); + disconnect(host, &Device::showTouch, client, &Device::showTouch); + } +} + +void DeviceManage::setGroupControlHost(Device *host, bool install) +{ + QMapIterator> i(m_devices); + while (i.hasNext()) { + i.next(); + if (!i.value()) { + continue; + } + if (i.value() == host) { + continue; + } + if (install) { + if (host) { + setGroupControlSignals(host, i.value(), true); + } + emit i.value()->setControlState(i.value(), Device::GroupControlState::GCS_CLIENT); + } else { + if (host) { + setGroupControlSignals(host, i.value(), false); + } + emit i.value()->setControlState(i.value(), Device::GroupControlState::GCS_FREE); + } + } +} + void DeviceManage::onDeviceDisconnect(QString serial) { if (!serial.isEmpty() && m_devices.contains(serial)) { + if (m_devices[serial]->controlState() == Device::GroupControlState::GCS_HOST) { + setGroupControlHost(nullptr, false); + } m_devices.remove(serial); } } +void DeviceManage::onControlStateChange(Device *device, Device::GroupControlState oldState, Device::GroupControlState newState) +{ + if (!device) { + return; + } + // free to host + if (oldState == Device::GroupControlState::GCS_FREE + && newState == Device::GroupControlState::GCS_HOST) { + // install control signals + setGroupControlHost(device, true); + return; + } + // host to free + if (oldState == Device::GroupControlState::GCS_HOST + && newState == Device::GroupControlState::GCS_FREE) { + // uninstall control signals + setGroupControlHost(device, false); + return; + } +} + quint16 DeviceManage::getFreePort() { quint16 port = m_localPortStart; diff --git a/QtScrcpy/devicemanage/devicemanage.h b/QtScrcpy/devicemanage/devicemanage.h index bfe1f7c..cbef25c 100644 --- a/QtScrcpy/devicemanage/devicemanage.h +++ b/QtScrcpy/devicemanage/devicemanage.h @@ -20,8 +20,13 @@ public: bool disconnectDevice(const QString &serial); void disconnectAllDevice(); +protected: + void setGroupControlSignals(Device *host, Device *client, bool install); + void setGroupControlHost(Device *host, bool install); + protected slots: void onDeviceDisconnect(QString serial); + void onControlStateChange(Device* device, Device::GroupControlState oldState, Device::GroupControlState newState); private: quint16 getFreePort(); From 3fd95bf9eedd01f8255c83cca71482e13f8d0310 Mon Sep 17 00:00:00 2001 From: rankun Date: Sun, 8 Mar 2020 14:07:53 +0800 Subject: [PATCH 09/19] fix: group control framesize bug --- QtScrcpy/device/device.cpp | 9 ++++ QtScrcpy/device/device.h | 1 + QtScrcpy/device/ui/videoform.cpp | 5 ++ QtScrcpy/device/ui/videoform.h | 1 + QtScrcpy/devicemanage/devicemanage.cpp | 66 ++++++++++++++++++++++---- QtScrcpy/devicemanage/devicemanage.h | 5 ++ 6 files changed, 79 insertions(+), 8 deletions(-) diff --git a/QtScrcpy/device/device.cpp b/QtScrcpy/device/device.cpp index 9dc12eb..d898920 100644 --- a/QtScrcpy/device/device.cpp +++ b/QtScrcpy/device/device.cpp @@ -91,6 +91,15 @@ const QString &Device::getSerial() return m_params.serial; } +const QSize Device::frameSize() +{ + QSize size; + if (!m_videoForm) { + return size; + } + return m_videoForm->frameSize(); +} + void Device::updateScript(QString script) { if(m_controller){ diff --git a/QtScrcpy/device/device.h b/QtScrcpy/device/device.h index cc7dd6d..5c519fc 100644 --- a/QtScrcpy/device/device.h +++ b/QtScrcpy/device/device.h @@ -46,6 +46,7 @@ public: VideoForm *getVideoForm(); Server *getServer(); const QString &getSerial(); + const QSize frameSize(); void updateScript(QString script); Device::GroupControlState controlState(); diff --git a/QtScrcpy/device/ui/videoform.cpp b/QtScrcpy/device/ui/videoform.cpp index ea4a2c9..5f54fcf 100644 --- a/QtScrcpy/device/ui/videoform.cpp +++ b/QtScrcpy/device/ui/videoform.cpp @@ -92,6 +92,11 @@ QRect VideoForm::getGrabCursorRect() return rc; } +const QSize &VideoForm::frameSize() +{ + return m_frameSize; +} + void VideoForm::updateRender(const AVFrame *frame) { if (m_videoWidget->isHidden()) { diff --git a/QtScrcpy/device/ui/videoform.h b/QtScrcpy/device/ui/videoform.h index 6f573a0..f2a3d09 100644 --- a/QtScrcpy/device/ui/videoform.h +++ b/QtScrcpy/device/ui/videoform.h @@ -25,6 +25,7 @@ public: void updateRender(const AVFrame *frame); void setDevice(Device *device); QRect getGrabCursorRect(); + const QSize &frameSize(); public slots: void onSwitchFullScreen(); diff --git a/QtScrcpy/devicemanage/devicemanage.cpp b/QtScrcpy/devicemanage/devicemanage.cpp index f0d0e44..e456517 100644 --- a/QtScrcpy/devicemanage/devicemanage.cpp +++ b/QtScrcpy/devicemanage/devicemanage.cpp @@ -127,9 +127,6 @@ void DeviceManage::setGroupControlSignals(Device *host, Device *client, bool ins connect(host, &Device::clipboardPaste, client, &Device::clipboardPaste); connect(host, &Device::pushFileRequest, client, &Device::pushFileRequest); connect(host, &Device::installApkRequest, client, &Device::installApkRequest); - connect(host, &Device::mouseEvent, client, &Device::mouseEvent); - connect(host, &Device::wheelEvent, client, &Device::wheelEvent); - connect(host, &Device::keyEvent, client, &Device::keyEvent); connect(host, &Device::screenshot, client, &Device::screenshot); connect(host, &Device::showTouch, client, &Device::showTouch); // dont connect requestDeviceClipboard @@ -150,9 +147,6 @@ void DeviceManage::setGroupControlSignals(Device *host, Device *client, bool ins disconnect(host, &Device::clipboardPaste, client, &Device::clipboardPaste); disconnect(host, &Device::pushFileRequest, client, &Device::pushFileRequest); disconnect(host, &Device::installApkRequest, client, &Device::installApkRequest); - disconnect(host, &Device::mouseEvent, client, &Device::mouseEvent); - disconnect(host, &Device::wheelEvent, client, &Device::wheelEvent); - disconnect(host, &Device::keyEvent, client, &Device::keyEvent); disconnect(host, &Device::screenshot, client, &Device::screenshot); disconnect(host, &Device::showTouch, client, &Device::showTouch); } @@ -201,19 +195,75 @@ void DeviceManage::onControlStateChange(Device *device, Device::GroupControlStat // free to host if (oldState == Device::GroupControlState::GCS_FREE && newState == Device::GroupControlState::GCS_HOST) { - // install control signals + // install direct control signals setGroupControlHost(device, true); + // install convert control signals(frameSize need convert) + connect(device, &Device::mouseEvent, this, &DeviceManage::onMouseEvent, Qt::UniqueConnection); + connect(device, &Device::wheelEvent, this, &DeviceManage::onWheelEvent, Qt::UniqueConnection); + connect(device, &Device::keyEvent, this, &DeviceManage::onKeyEvent, Qt::UniqueConnection); return; } // host to free if (oldState == Device::GroupControlState::GCS_HOST && newState == Device::GroupControlState::GCS_FREE) { - // uninstall control signals + // uninstall direct control signals setGroupControlHost(device, false); + // uninstall convert control signals(frameSize need convert) + disconnect(device, &Device::mouseEvent, this, &DeviceManage::onMouseEvent); + disconnect(device, &Device::wheelEvent, this, &DeviceManage::onWheelEvent); + disconnect(device, &Device::keyEvent, this, &DeviceManage::onKeyEvent); return; } } +void DeviceManage::onMouseEvent(const QMouseEvent *from, const QSize &frameSize, const QSize &showSize) +{ + QMapIterator> i(m_devices); + while (i.hasNext()) { + i.next(); + if (!i.value()) { + continue; + } + if (i.value() == sender()) { + continue; + } + // neend convert frameSize to its frameSize + emit i.value()->mouseEvent(from, i.value()->frameSize(), showSize); + } +} + +void DeviceManage::onWheelEvent(const QWheelEvent *from, const QSize &frameSize, const QSize &showSize) +{ + QMapIterator> i(m_devices); + while (i.hasNext()) { + i.next(); + if (!i.value()) { + continue; + } + if (i.value() == sender()) { + continue; + } + // neend convert frameSize to its frameSize + emit i.value()->wheelEvent(from, i.value()->frameSize(), showSize); + } +} + +void DeviceManage::onKeyEvent(const QKeyEvent *from, const QSize &frameSize, const QSize &showSize) +{ + QMapIterator> i(m_devices); + while (i.hasNext()) { + i.next(); + if (!i.value()) { + continue; + } + if (i.value() == sender()) { + continue; + } + // neend convert frameSize to its frameSize + emit i.value()->keyEvent(from, i.value()->frameSize(), showSize); + } +} + quint16 DeviceManage::getFreePort() { quint16 port = m_localPortStart; diff --git a/QtScrcpy/devicemanage/devicemanage.h b/QtScrcpy/devicemanage/devicemanage.h index cbef25c..fb86089 100644 --- a/QtScrcpy/devicemanage/devicemanage.h +++ b/QtScrcpy/devicemanage/devicemanage.h @@ -28,6 +28,11 @@ protected slots: void onDeviceDisconnect(QString serial); void onControlStateChange(Device* device, Device::GroupControlState oldState, Device::GroupControlState newState); + // neend convert frameSize to its frameSize + void onMouseEvent(const QMouseEvent* from, const QSize& frameSize, const QSize& showSize); + void onWheelEvent(const QWheelEvent* from, const QSize& frameSize, const QSize& showSize); + void onKeyEvent(const QKeyEvent* from, const QSize& frameSize, const QSize& showSize); + private: quint16 getFreePort(); From 7de2e9a8cff6086a46e999604471d248378f2a81 Mon Sep 17 00:00:00 2001 From: rankun Date: Sun, 8 Mar 2020 14:54:53 +0800 Subject: [PATCH 10/19] docs: update faq --- README.md | 1 + README_zh.md | 1 + docs/FAQ.md | 15 +++++++++++---- docs/TODO.md | 1 - 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0f6b2cd..64e4616 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,7 @@ Note: it is not necessary to keep you Android device connected via USB after you - `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard; - `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but breaks non-ASCII characters). +- Group control ## TODO [TODO](docs/TODO.md) diff --git a/README_zh.md b/README_zh.md index 4afff4c..da75149 100644 --- a/README_zh.md +++ b/README_zh.md @@ -176,6 +176,7 @@ Mac OS平台,你可以直接使用我编译好的可执行程序: - `Ctrl` + `c`将设备剪贴板复制到计算机剪贴板; - `Ctrl` + `Shift` + `v`将计算机剪贴板复制到设备剪贴板; - `Ctrl` +`v` 将计算机剪贴板作为一系列文本事件发送到设备(不支持非ASCII字符)。 +- 群控 ## TODO [后期计划](docs/TODO.md) diff --git a/docs/FAQ.md b/docs/FAQ.md index 728270f..10a4c25 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -3,16 +3,23 @@ 如果在此文档没有解决你的问题,描述你的问题,截图软件控制台中打印的日志,一起发到QQ群里提问。 +## 支持声音(软件不做支持) +[关于转发安卓声音到PC的讨论](https://github.com/Genymobile/scrcpy/issues/14#issuecomment-543204526) + ## 无法输入中文 -安装搜狗输入法/QQ输入法就可以支持输入中文了 +手机端安装搜狗输入法/QQ输入法就可以支持输入中文了 ## 可以看到画面,但无法控制 有些手机(小米等手机)需要额外打开控制权限,检查是否USB调试里打开了允许模拟点击 ![image](image/USB调试(安全设置).jpg) -## 错误信息Could not open video stream +## 手机通过数据线连接电脑,刷新设备列表以后,没有任何设备出现 +随便下载一个手机助手,尝试连接成功以后,再用QtScrcpy刷新设备列表连接 + +## 错误信息:AdbProcess::error:adb server version (40) doesnt match this client (41) +任务管理找到adb进程并杀死,重新操作即可 + +## 错误信息:Could not open video stream 导致这个错误的原因有很多,最简单的解决方法是在分辨率设置中,选择一个较低的分辨率 -## 声音 -[关于转发安卓声音到PC的讨论](https://github.com/Genymobile/scrcpy/issues/14#issuecomment-543204526) diff --git a/docs/TODO.md b/docs/TODO.md index cfec2f0..47feaf3 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -7,7 +7,6 @@ ## 中优先级 - 脚本 -- 群控 - 软解 - opengles 3.0 兼容性参考[这里](https://github.com/libretro/glsl-shaders/blob/master/nnedi3/shaders/yuv-to-rgb-2x.glsl) From 8707167bf55445496bf25903d4a241f62bfad0a8 Mon Sep 17 00:00:00 2001 From: rankun Date: Sun, 8 Mar 2020 16:25:34 +0800 Subject: [PATCH 11/19] fix: build error on ubuntu --- QtScrcpy/device/ui/videoform.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QtScrcpy/device/ui/videoform.cpp b/QtScrcpy/device/ui/videoform.cpp index 5f54fcf..d84baa4 100644 --- a/QtScrcpy/device/ui/videoform.cpp +++ b/QtScrcpy/device/ui/videoform.cpp @@ -87,7 +87,7 @@ QRect VideoForm::getGrabCursorRect() rc.setWidth(rc.width() - 180); rc.setHeight(rc.height() - 60); #else - Q_UNUSED(grab) + #endif return rc; } From 78ebca0a6d32bc158d20d3619e231a5e36e90d5b Mon Sep 17 00:00:00 2001 From: rankun Date: Mon, 9 Mar 2020 10:14:24 +0800 Subject: [PATCH 12/19] feat: remember video rect --- QtScrcpy/device/device.cpp | 6 ++++++ QtScrcpy/device/ui/videoform.cpp | 9 +++++++++ QtScrcpy/device/ui/videoform.h | 1 + QtScrcpy/util/config.cpp | 29 +++++++++++++++++++++++++++++ QtScrcpy/util/config.h | 3 +++ 5 files changed, 48 insertions(+) diff --git a/QtScrcpy/device/device.cpp b/QtScrcpy/device/device.cpp index d898920..e6ccb72 100644 --- a/QtScrcpy/device/device.cpp +++ b/QtScrcpy/device/device.cpp @@ -71,6 +71,7 @@ Device::~Device() delete m_vb; } if (m_videoForm) { + m_videoForm->close(); delete m_videoForm; } emit deviceDisconnect(m_params.serial); @@ -218,6 +219,11 @@ void Device::initSignals() if (m_videoForm) { m_videoForm->setWindowTitle(deviceName); m_videoForm->updateShowSize(size); + + QRect rc = Config::getInstance().getRect(getSerial()); + if (rc.isValid()) { + m_videoForm->setGeometry(rc); + } } // init recorder diff --git a/QtScrcpy/device/ui/videoform.cpp b/QtScrcpy/device/ui/videoform.cpp index d84baa4..1f77b7a 100644 --- a/QtScrcpy/device/ui/videoform.cpp +++ b/QtScrcpy/device/ui/videoform.cpp @@ -430,6 +430,15 @@ void VideoForm::resizeEvent(QResizeEvent *event) } } +void VideoForm::closeEvent(QCloseEvent *event) +{ + Q_UNUSED(event) + if (!m_device) { + return; + } + Config::getInstance().setRect(m_device->getSerial(), geometry()); +} + void VideoForm::dragEnterEvent(QDragEnterEvent *event) { event->acceptProposedAction(); diff --git a/QtScrcpy/device/ui/videoform.h b/QtScrcpy/device/ui/videoform.h index f2a3d09..be05b83 100644 --- a/QtScrcpy/device/ui/videoform.h +++ b/QtScrcpy/device/ui/videoform.h @@ -49,6 +49,7 @@ protected: void paintEvent(QPaintEvent *); void showEvent(QShowEvent *event); void resizeEvent(QResizeEvent *event); + void closeEvent(QCloseEvent *event); void dragEnterEvent(QDragEnterEvent *event); void dragMoveEvent(QDragMoveEvent *event); diff --git a/QtScrcpy/util/config.cpp b/QtScrcpy/util/config.cpp index 630faea..2f7b0a7 100644 --- a/QtScrcpy/util/config.cpp +++ b/QtScrcpy/util/config.cpp @@ -44,6 +44,12 @@ #define COMMON_RECORD_FORMAT_INDEX_KEY "RecordFormatIndex" #define COMMON_RECORD_FORMAT_INDEX_DEF 0 +#define SERIAL_WINDOW_RECT_KEY_X "WindowRectX" +#define SERIAL_WINDOW_RECT_KEY_Y "WindowRectY" +#define SERIAL_WINDOW_RECT_KEY_W "WindowRectW" +#define SERIAL_WINDOW_RECT_KEY_H "WindowRectH" +#define SERIAL_WINDOW_RECT_KEY_DEF -1 + // 最大尺寸 录制格式 QString Config::s_configPath = ""; @@ -140,6 +146,29 @@ void Config::setRecordFormatIndex(int recordFormatIndex) m_userData->endGroup(); } +void Config::setRect(const QString &serial, const QRect &rc) +{ + m_userData->beginGroup(serial); + m_userData->setValue(SERIAL_WINDOW_RECT_KEY_X, rc.left()); + m_userData->setValue(SERIAL_WINDOW_RECT_KEY_Y, rc.top()); + m_userData->setValue(SERIAL_WINDOW_RECT_KEY_W, rc.width()); + m_userData->setValue(SERIAL_WINDOW_RECT_KEY_H, rc.height()); + m_userData->endGroup(); + m_userData->sync(); +} + +QRect Config::getRect(const QString &serial) +{ + QRect rc; + m_userData->beginGroup(serial); + rc.setX(m_userData->value(SERIAL_WINDOW_RECT_KEY_X, SERIAL_WINDOW_RECT_KEY_DEF).toInt()); + rc.setY(m_userData->value(SERIAL_WINDOW_RECT_KEY_Y, SERIAL_WINDOW_RECT_KEY_DEF).toInt()); + rc.setWidth(m_userData->value(SERIAL_WINDOW_RECT_KEY_W, SERIAL_WINDOW_RECT_KEY_DEF).toInt()); + rc.setHeight(m_userData->value(SERIAL_WINDOW_RECT_KEY_H, SERIAL_WINDOW_RECT_KEY_DEF).toInt()); + m_userData->endGroup(); + return rc; +} + QString Config::getServerVersion() { QString server; diff --git a/QtScrcpy/util/config.h b/QtScrcpy/util/config.h index 9e91841..e635278 100644 --- a/QtScrcpy/util/config.h +++ b/QtScrcpy/util/config.h @@ -3,6 +3,7 @@ #include #include +#include class QSettings; class Config : public QObject @@ -29,6 +30,8 @@ public: void setMaxSizeIndex(int maxSizeIndex); int getRecordFormatIndex(); void setRecordFormatIndex(int recordFormatIndex); + void setRect(const QString &serial, const QRect &rc); + QRect getRect(const QString &serial); private: explicit Config(QObject *parent = nullptr); From 37b792535b010b78c43688465aa965b8215ad432 Mon Sep 17 00:00:00 2001 From: rankun Date: Mon, 9 Mar 2020 12:37:54 +0800 Subject: [PATCH 13/19] feat: videoForm delay show --- QtScrcpy/device/device.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/QtScrcpy/device/device.cpp b/QtScrcpy/device/device.cpp index e6ccb72..22fa077 100644 --- a/QtScrcpy/device/device.cpp +++ b/QtScrcpy/device/device.cpp @@ -37,7 +37,6 @@ Device::Device(DeviceParams params, QObject *parent) m_controller = new Controller(params.gameScript, this); m_videoForm = new VideoForm(Config::getInstance().getSkin()); m_videoForm->setDevice(this); - m_videoForm->show(); } m_stream = new Stream(this); @@ -222,8 +221,13 @@ void Device::initSignals() QRect rc = Config::getInstance().getRect(getSerial()); if (rc.isValid()) { - m_videoForm->setGeometry(rc); + m_videoForm->move(rc.topLeft()); + m_videoForm->resize(rc.size()); + // TODO: setGeometry magneticwidget bug + //m_videoForm->setGeometry(rc); } + // videoForm delay show + m_videoForm->show(); } // init recorder From aa8f54bb780fd4f953dfd3c485db0079665d7eb1 Mon Sep 17 00:00:00 2001 From: rankun Date: Mon, 9 Mar 2020 15:07:06 +0800 Subject: [PATCH 14/19] docs: update FAQ.md --- docs/FAQ.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/FAQ.md b/docs/FAQ.md index 10a4c25..1ebe090 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -23,3 +23,6 @@ ## 错误信息:Could not open video stream 导致这个错误的原因有很多,最简单的解决方法是在分辨率设置中,选择一个较低的分辨率 +## 错误信息:QOpenGLShaderProgram::attributeLocation(vertexIn): shader program is not linked +config.ini里修改下解码方式,改成1或者2试试 + From cb65a7d4cabcb9a5c8de9f45a38fbf8dd80ba3d5 Mon Sep 17 00:00:00 2001 From: rankun Date: Mon, 9 Mar 2020 15:38:26 +0800 Subject: [PATCH 15/19] feat: change screenshot format to png Close #102 --- QtScrcpy/device/device.cpp | 4 ++-- README.md | 1 + README_zh.md | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/QtScrcpy/device/device.cpp b/QtScrcpy/device/device.cpp index 22fa077..6e4c900 100644 --- a/QtScrcpy/device/device.cpp +++ b/QtScrcpy/device/device.cpp @@ -368,10 +368,10 @@ bool Device::saveFrame(const AVFrame* frame) } QDateTime dateTime = QDateTime::currentDateTime(); QString fileName = dateTime.toString("_yyyyMMdd_hhmmss_zzz"); - fileName = Config::getInstance().getTitle() + fileName + ".jpg"; + fileName = Config::getInstance().getTitle() + fileName + ".png"; QDir dir(fileDir); absFilePath = dir.absoluteFilePath(fileName); - ret = rgbImage.save(absFilePath); + ret = rgbImage.save(absFilePath, "PNG", 100); if (!ret) { return false; } diff --git a/README.md b/README.md index 64e4616..64a7ed3 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,7 @@ Note: it is not necessary to keep you Android device connected via USB after you - Display Android device screens in real time - Real-time mouse and keyboard control of Android devices - Screen recording +- Screenshot to png - Wireless connection - Supports up to 16 device connections (the number can be higher if your PC performance allows. You need to compile it by yourself) - Full-screen display diff --git a/README_zh.md b/README_zh.md index da75149..54e4360 100644 --- a/README_zh.md +++ b/README_zh.md @@ -163,6 +163,7 @@ Mac OS平台,你可以直接使用我编译好的可执行程序: - 实时显示Android设备屏幕 - 实时键鼠控制Android设备 - 屏幕录制 +- 截图为png - 无线连接 - 最多支持16台设备连接(PC性能允许的情况下可以增加,需要自己编译) - 全屏显示 From 905880530fcd7bd45e5dd678e0fd500b5e9ed719 Mon Sep 17 00:00:00 2001 From: rankun Date: Thu, 12 Mar 2020 18:57:16 +0800 Subject: [PATCH 16/19] feat: adjust wheelEvent speed --- .../device/controller/inputconvert/inputconvertnormal.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/QtScrcpy/device/controller/inputconvert/inputconvertnormal.cpp b/QtScrcpy/device/controller/inputconvert/inputconvertnormal.cpp index 12d1d04..73c4633 100644 --- a/QtScrcpy/device/controller/inputconvert/inputconvertnormal.cpp +++ b/QtScrcpy/device/controller/inputconvert/inputconvertnormal.cpp @@ -1,3 +1,5 @@ +#include + #include "inputconvertnormal.h" InputConvertNormal::InputConvertNormal(Controller* controller) @@ -63,10 +65,10 @@ void InputConvertNormal::wheelEvent(const QWheelEvent *from, const QSize& frameS qint32 vScroll = 0; switch (from->orientation()) { case Qt::Horizontal: - hScroll = from->delta(); + hScroll = from->delta() / abs(from->delta()) * 2; break; case Qt::Vertical: - vScroll = from->delta(); + vScroll = from->delta() / abs(from->delta()) * 2; break; } From 4f013e58b52a059e31b7486ff5669ae8994b3890 Mon Sep 17 00:00:00 2001 From: rankun Date: Fri, 13 Mar 2020 16:46:34 +0800 Subject: [PATCH 17/19] feat: mac package dmg --- .github/workflows/macos.yml | 17 +++++++-- .github/workflows/windows.yml | 10 +++++- ci/mac/package/dmg-background.jpg | Bin 0 -> 41945 bytes ci/mac/package/package.py | 55 ++++++++++++++++++++++++++++++ ci/mac/package/requirements.txt | 1 + ci/mac/package_for_mac.sh | 37 ++++++++++++++++++++ 6 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 ci/mac/package/dmg-background.jpg create mode 100644 ci/mac/package/package.py create mode 100644 ci/mac/package/requirements.txt create mode 100755 ci/mac/package_for_mac.sh diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index c5d3e60..84fe4fa 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -46,15 +46,18 @@ jobs: - uses: actions/checkout@v1 with: fetch-depth: 1 + # 编译 - name: Build MacOS run: | export ENV_QT_CLANG=$(pwd)/${{env.Qt5_Dir}} ci/mac/build_for_mac.sh release + # 发布 - name: Publish if: startsWith(github.event.ref, 'refs/tags/') run: | export ENV_QT_CLANG=$(pwd)/${{env.Qt5_Dir}} ci/mac/publish_for_mac.sh ../build + ci/mac/package_for_mac.sh # tag 打包 - name: Package if: startsWith(github.event.ref, 'refs/tags/') @@ -65,7 +68,7 @@ jobs: [string]$tag = ${env:ref}.Substring(${env:ref}.LastIndexOf('/') + 1) [string]$name = 'QtScrcpy-mac-x64-' + ${tag} # 打包zip - Compress-Archive -Path ci\build\QtScrcpy.app ci\build\${name}.zip + Compress-Archive -Path ci\build\QtScrcpy.dmg ci\build\${name}.zip # 查询Release - name: Query Release if: startsWith(github.event.ref, 'refs/tags/') @@ -73,12 +76,20 @@ jobs: env: githubFullName: ${{ github.event.repository.full_name }} ref: ${{ github.event.ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} run: | [string]$tag = ${env:ref}.Substring(${env:ref}.LastIndexOf('/') + 1) [string]$url = 'https://api.github.com/repos/' + ${env:githubFullName} + '/releases/tags/' + ${tag} + + $token = ${env:github_token} + $authInfo = ("{0}" -f $token) + $authInfo = [System.Text.Encoding]::UTF8.GetBytes($authInfo) + $authInfo = [System.Convert]::ToBase64String($authInfo) + $headers = @{Authorization=("barry-ran {0}" -f $authInfo)} + $response={} try { - $response = Invoke-RestMethod -Uri $url -Method Get + $response = Invoke-RestMethod -Uri $url -Headers $headers -Method Get } catch { Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__ Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription @@ -102,12 +113,14 @@ jobs: run: | [string]$tag = ${env:ref}.Substring(${env:ref}.LastIndexOf('/') + 1) [string]$url = 'https://api.github.com/repos/' + ${env:githubFullName} + '/releases/tags/' + ${tag} + # github token防止api rate limite,否则一个小时只能60个api请求 $token = ${env:github_token} $authInfo = ("{0}" -f $token) $authInfo = [System.Text.Encoding]::UTF8.GetBytes($authInfo) $authInfo = [System.Convert]::ToBase64String($authInfo) $headers = @{Authorization=("barry-ran {0}" -f $authInfo)} + $response = Invoke-RestMethod -Uri $url -ContentType 'text/json' -Headers $headers -Method Get [string]$latestUpUrl = $response.upload_url Write-Host 'latestUpUrl:'$latestUpUrl diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 2b40ad2..6b58178 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -103,12 +103,20 @@ jobs: env: githubFullName: ${{ github.event.repository.full_name }} ref: ${{ github.event.ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} run: | [string]$tag = ${env:ref}.Substring(${env:ref}.LastIndexOf('/') + 1) [string]$url = 'https://api.github.com/repos/' + ${env:githubFullName} + '/releases/tags/' + ${tag} + + $token = ${env:github_token} + $authInfo = ("{0}" -f $token) + $authInfo = [System.Text.Encoding]::UTF8.GetBytes($authInfo) + $authInfo = [System.Convert]::ToBase64String($authInfo) + $headers = @{Authorization=("barry-ran {0}" -f $authInfo)} + $response={} try { - $response = Invoke-RestMethod -Uri $url -Method Get + $response = Invoke-RestMethod -Uri $url -Headers $headers -Method Get } catch { Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__ Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription diff --git a/ci/mac/package/dmg-background.jpg b/ci/mac/package/dmg-background.jpg new file mode 100644 index 0000000000000000000000000000000000000000..84c50745d2046724bb729311ce182bb05f0e98db GIT binary patch literal 41945 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 /dev/null # disable output +# 设置当前目录,cd的目录影响接下来执行程序的工作目录 +old_cd=$(pwd) +cd $(dirname "$0") + +echo +echo +echo --------------------------------------------------------------- +echo pip install requirements +echo --------------------------------------------------------------- + +pip install -r $script_path/package/requirements.txt +if [ $? -ne 0 ] ;then + echo "pip install requirements failed" + exit 1 +fi + +echo +echo +echo --------------------------------------------------------------- +echo create package +echo --------------------------------------------------------------- + +python $script_path/package/package.py +if [ $? -ne 0 ] ;then + echo "create package failed" + exit 1 +fi + +# 恢复当前目录 +cd $old_cd +exit 0 From 9bcfed92dba8c912c683735ebed375b95a11ef13 Mon Sep 17 00:00:00 2001 From: rankun Date: Mon, 16 Mar 2020 10:59:58 +0800 Subject: [PATCH 18/19] feat: add shortcut --- QtScrcpy/device/controller/controller.cpp | 2 +- QtScrcpy/device/controller/controller.h | 2 +- QtScrcpy/device/device.cpp | 3 +- QtScrcpy/device/device.h | 3 +- QtScrcpy/device/ui/videoform.cpp | 225 ++++++++++++++++++++-- QtScrcpy/device/ui/videoform.h | 5 + QtScrcpy/devicemanage/devicemanage.cpp | 6 +- README.md | 26 +++ README_zh.md | 26 +++ 9 files changed, 273 insertions(+), 25 deletions(-) diff --git a/QtScrcpy/device/controller/controller.cpp b/QtScrcpy/device/controller/controller.cpp index 65fd60a..8418109 100644 --- a/QtScrcpy/device/controller/controller.cpp +++ b/QtScrcpy/device/controller/controller.cpp @@ -62,7 +62,7 @@ void Controller::updateScript(QString gameScript) connect(m_inputConvert, &InputConvertBase::grabCursor, this, &Controller::grabCursor); } -void Controller::onPostTurnOn() +void Controller::onPostBackOrScreenOn() { ControlMsg* controlMsg = new ControlMsg(ControlMsg::CMT_BACK_OR_SCREEN_ON); if (!controlMsg) { diff --git a/QtScrcpy/device/controller/controller.h b/QtScrcpy/device/controller/controller.h index 9bf14d0..cf11610 100644 --- a/QtScrcpy/device/controller/controller.h +++ b/QtScrcpy/device/controller/controller.h @@ -40,7 +40,7 @@ public slots: void onKeyEvent(const QKeyEvent* from, const QSize& frameSize, const QSize& showSize); // turn the screen on if it was off, press BACK otherwise - void onPostTurnOn(); + void onPostBackOrScreenOn(); void onRequestDeviceClipboard(); void onSetDeviceClipboard(); void onClipboardPaste(); diff --git a/QtScrcpy/device/device.cpp b/QtScrcpy/device/device.cpp index 6e4c900..5cb0396 100644 --- a/QtScrcpy/device/device.cpp +++ b/QtScrcpy/device/device.cpp @@ -155,11 +155,12 @@ void Device::initSignals() connect(this, &Device::postVolumeDown, m_controller, &Controller::onPostVolumeDown); connect(this, &Device::setScreenPowerMode, m_controller, &Controller::onSetScreenPowerMode); connect(this, &Device::expandNotificationPanel, m_controller, &Controller::onExpandNotificationPanel); + connect(this, &Device::collapseNotificationPanel, m_controller, &Controller::onCollapseNotificationPanel); connect(this, &Device::mouseEvent, m_controller, &Controller::onMouseEvent); connect(this, &Device::wheelEvent, m_controller, &Controller::onWheelEvent); connect(this, &Device::keyEvent, m_controller, &Controller::onKeyEvent); - connect(this, &Device::postTurnOn, m_controller, &Controller::onPostTurnOn); + connect(this, &Device::postBackOrScreenOn, m_controller, &Controller::onPostBackOrScreenOn); connect(this, &Device::requestDeviceClipboard, m_controller, &Controller::onRequestDeviceClipboard); connect(this, &Device::setDeviceClipboard, m_controller, &Controller::onSetDeviceClipboard); connect(this, &Device::clipboardPaste, m_controller, &Controller::onClipboardPaste); diff --git a/QtScrcpy/device/device.h b/QtScrcpy/device/device.h index 5c519fc..041ad8e 100644 --- a/QtScrcpy/device/device.h +++ b/QtScrcpy/device/device.h @@ -65,7 +65,8 @@ signals: void postVolumeDown(); void setScreenPowerMode(ControlMsg::ScreenPowerMode mode); void expandNotificationPanel(); - void postTurnOn(); + void collapseNotificationPanel(); + void postBackOrScreenOn(); void postTextInput(QString& text); void requestDeviceClipboard(); void setDeviceClipboard(); diff --git a/QtScrcpy/device/ui/videoform.cpp b/QtScrcpy/device/ui/videoform.cpp index 1f77b7a..223897d 100644 --- a/QtScrcpy/device/ui/videoform.cpp +++ b/QtScrcpy/device/ui/videoform.cpp @@ -8,6 +8,9 @@ #include #include #include +#include +#include +#include #include "videoform.h" #include "qyuvopenglwidget.h" @@ -29,6 +32,7 @@ VideoForm::VideoForm(bool skin, QWidget *parent) { ui->setupUi(this); initUI(); + installShortcut(); updateShowSize(size()); bool vertical = size().height() > size().width(); if (m_skin) { @@ -97,6 +101,21 @@ const QSize &VideoForm::frameSize() return m_frameSize; } +void VideoForm::resizeSquare() +{ + QRect screenRect = getScreenRect(); + if (screenRect.isEmpty()) { + qWarning() << "getScreenRect is empty"; + return; + } + resize(screenRect.height(), screenRect.height()); +} + +void VideoForm::removeBlackRect() +{ + resize(ui->keepRadioWidget->goodSize()); +} + void VideoForm::updateRender(const AVFrame *frame) { if (m_videoWidget->isHidden()) { @@ -124,16 +143,178 @@ void VideoForm::showToolForm(bool show) void VideoForm::moveCenter() { - QDesktopWidget* desktop = QApplication::desktop(); - if (!desktop) { - qWarning() << "QApplication::desktop() is nullptr"; + QRect screenRect = getScreenRect(); + if (screenRect.isEmpty()) { + qWarning() << "getScreenRect is empty"; return; } - QRect screenRect = desktop->availableGeometry(); // 窗口居中 move(screenRect.center() - QRect(0, 0, size().width(), size().height()).center()); } +void VideoForm::installShortcut() +{ + QShortcut *shortcut = nullptr; + + // switchFullScreen + shortcut = new QShortcut(QKeySequence("Ctrl+f"), this); + connect(shortcut, &QShortcut::activated, this, [this](){ + if (!m_device) { + return; + } + emit m_device->switchFullScreen(); + }); + + // resizeSquare + shortcut = new QShortcut(QKeySequence("Ctrl+g"), this); + connect(shortcut, &QShortcut::activated, this, [this](){ + resizeSquare(); + }); + + // removeBlackRect + shortcut = new QShortcut(QKeySequence("Ctrl+x"), this); + connect(shortcut, &QShortcut::activated, this, [this](){ + removeBlackRect(); + }); + + + // postGoHome + shortcut = new QShortcut(QKeySequence("Ctrl+h"), this); + connect(shortcut, &QShortcut::activated, this, [this](){ + if (!m_device) { + return; + } + emit m_device->postGoHome(); + }); + + // postGoBack + shortcut = new QShortcut(QKeySequence("Ctrl+b"), this); + connect(shortcut, &QShortcut::activated, this, [this](){ + if (!m_device) { + return; + } + emit m_device->postGoBack(); + }); + + // postAppSwitch + shortcut = new QShortcut(QKeySequence("Ctrl+s"), this); + connect(shortcut, &QShortcut::activated, this, [this](){ + if (!m_device) { + return; + } + emit m_device->postAppSwitch(); + }); + + // postGoMenu + shortcut = new QShortcut(QKeySequence("Ctrl+m"), this); + connect(shortcut, &QShortcut::activated, this, [this](){ + if (!m_device) { + return; + } + emit m_device->postGoMenu(); + }); + + // postVolumeUp + shortcut = new QShortcut(QKeySequence("Ctrl+up"), this); + connect(shortcut, &QShortcut::activated, this, [this](){ + if (!m_device) { + return; + } + emit m_device->postVolumeUp(); + }); + + // postVolumeDown + shortcut = new QShortcut(QKeySequence("Ctrl+down"), this); + connect(shortcut, &QShortcut::activated, this, [this](){ + if (!m_device) { + return; + } + emit m_device->postVolumeDown(); + }); + + // postPower + shortcut = new QShortcut(QKeySequence("Ctrl+p"), this); + connect(shortcut, &QShortcut::activated, this, [this](){ + if (!m_device) { + return; + } + emit m_device->postPower(); + }); + + // setScreenPowerMode(ControlMsg::SPM_OFF) + shortcut = new QShortcut(QKeySequence("Ctrl+o"), this); + connect(shortcut, &QShortcut::activated, this, [this](){ + if (!m_device) { + return; + } + emit m_device->setScreenPowerMode(ControlMsg::SPM_OFF); + }); + + // expandNotificationPanel + shortcut = new QShortcut(QKeySequence("Ctrl+n"), this); + connect(shortcut, &QShortcut::activated, this, [this](){ + if (!m_device) { + return; + } + emit m_device->expandNotificationPanel(); + }); + + // collapseNotificationPanel + shortcut = new QShortcut(QKeySequence("Ctrl+Shift+n"), this); + connect(shortcut, &QShortcut::activated, this, [this](){ + if (!m_device) { + return; + } + emit m_device->collapseNotificationPanel(); + }); + + // requestDeviceClipboard + shortcut = new QShortcut(QKeySequence("Ctrl+c"), this); + connect(shortcut, &QShortcut::activated, this, [this](){ + if (!m_device) { + return; + } + emit m_device->requestDeviceClipboard(); + }); + + // clipboardPaste + shortcut = new QShortcut(QKeySequence("Ctrl+v"), this); + connect(shortcut, &QShortcut::activated, this, [this](){ + if (!m_device) { + return; + } + emit m_device->clipboardPaste(); + }); + + // setDeviceClipboard + shortcut = new QShortcut(QKeySequence("Ctrl+Shift+v"), this); + connect(shortcut, &QShortcut::activated, this, [this](){ + if (!m_device) { + return; + } + emit m_device->setDeviceClipboard(); + }); +} + +QRect VideoForm::getScreenRect() +{ + QRect screenRect; + QWidget *win = window(); + if (!win) { + return screenRect; + } + QWindow *winHandle = win->windowHandle(); + if (!winHandle) { + return screenRect; + } + QScreen *screen = winHandle->screen(); + if (!screen) { + return screenRect; + } + screenRect = screen->availableGeometry(); + return screenRect; +} + void VideoForm::updateStyleSheet(bool vertical) { if (vertical) { @@ -175,12 +356,11 @@ void VideoForm::updateShowSize(const QSize &newSize) bool vertical = m_widthHeightRatio < 1.0f ? true : false; QSize showSize = newSize; - QDesktopWidget* desktop = QApplication::desktop(); - if (!desktop) { - qWarning() << "QApplication::desktop() is nullptr"; + QRect screenRect = getScreenRect(); + if (screenRect.isEmpty()) { + qWarning() << "getScreenRect is empty"; return; } - QRect screenRect = desktop->availableGeometry(); if (vertical) { showSize.setHeight(qMin(newSize.height(), screenRect.height() - 200)); showSize.setWidth(showSize.height() * m_widthHeightRatio); @@ -278,6 +458,12 @@ void VideoForm::setDevice(Device *device) void VideoForm::mousePressEvent(QMouseEvent *event) { + if (event->button() == Qt::MiddleButton) { + if (m_device) { + emit m_device->postGoHome(); + } + } + if (m_videoWidget->geometry().contains(event->pos())) { if (!m_device) { return; @@ -336,6 +522,18 @@ void VideoForm::mouseMoveEvent(QMouseEvent *event) } } +void VideoForm::mouseDoubleClickEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton + && !m_videoWidget->geometry().contains(event->pos())) { + removeBlackRect(); + } + + if (event->button() == Qt::RightButton && m_device) { + emit m_device->postBackOrScreenOn(); + } +} + void VideoForm::wheelEvent(QWheelEvent *event) { if (m_videoWidget->geometry().contains(event->pos())) { @@ -364,17 +562,6 @@ void VideoForm::keyPressEvent(QKeyEvent *event) && isFullScreen()) { emit m_device->switchFullScreen(); } - if (event->key() == Qt::Key_C && (event->modifiers() & Qt::ControlModifier)) { - emit m_device->requestDeviceClipboard(); - } - if (event->key() == Qt::Key_V && (event->modifiers() & Qt::ControlModifier)) { - if (event->modifiers() & Qt::ShiftModifier) { - emit m_device->setDeviceClipboard(); - } else { - emit m_device->clipboardPaste(); - } - return; - } emit m_device->keyEvent(event, m_videoWidget->frameSize(), m_videoWidget->size()); } diff --git a/QtScrcpy/device/ui/videoform.h b/QtScrcpy/device/ui/videoform.h index be05b83..9de6438 100644 --- a/QtScrcpy/device/ui/videoform.h +++ b/QtScrcpy/device/ui/videoform.h @@ -26,6 +26,8 @@ public: void setDevice(Device *device); QRect getGrabCursorRect(); const QSize &frameSize(); + void resizeSquare(); + void removeBlackRect(); public slots: void onSwitchFullScreen(); @@ -37,11 +39,14 @@ private: void showToolForm(bool show = true); void moveCenter(); + void installShortcut(); + QRect getScreenRect(); protected: void mousePressEvent(QMouseEvent *event); void mouseReleaseEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); + void mouseDoubleClickEvent(QMouseEvent *event); void wheelEvent(QWheelEvent *event); void keyPressEvent(QKeyEvent *event); void keyReleaseEvent(QKeyEvent *event); diff --git a/QtScrcpy/devicemanage/devicemanage.cpp b/QtScrcpy/devicemanage/devicemanage.cpp index e456517..75f5465 100644 --- a/QtScrcpy/devicemanage/devicemanage.cpp +++ b/QtScrcpy/devicemanage/devicemanage.cpp @@ -121,7 +121,8 @@ void DeviceManage::setGroupControlSignals(Device *host, Device *client, bool ins connect(host, &Device::postVolumeDown, client, &Device::postVolumeDown); connect(host, &Device::setScreenPowerMode, client, &Device::setScreenPowerMode); connect(host, &Device::expandNotificationPanel, client, &Device::expandNotificationPanel); - connect(host, &Device::postTurnOn, client, &Device::postTurnOn); + connect(host, &Device::collapseNotificationPanel, client, &Device::collapseNotificationPanel); + connect(host, &Device::postBackOrScreenOn, client, &Device::postBackOrScreenOn); connect(host, &Device::postTextInput, client, &Device::postTextInput); connect(host, &Device::setDeviceClipboard, client, &Device::setDeviceClipboard); connect(host, &Device::clipboardPaste, client, &Device::clipboardPaste); @@ -141,7 +142,8 @@ void DeviceManage::setGroupControlSignals(Device *host, Device *client, bool ins disconnect(host, &Device::postVolumeDown, client, &Device::postVolumeDown); disconnect(host, &Device::setScreenPowerMode, client, &Device::setScreenPowerMode); disconnect(host, &Device::expandNotificationPanel, client, &Device::expandNotificationPanel); - disconnect(host, &Device::postTurnOn, client, &Device::postTurnOn); + disconnect(host, &Device::collapseNotificationPanel, client, &Device::collapseNotificationPanel); + disconnect(host, &Device::postBackOrScreenOn, client, &Device::postBackOrScreenOn); disconnect(host, &Device::postTextInput, client, &Device::postTextInput); disconnect(host, &Device::setDeviceClipboard, client, &Device::setDeviceClipboard); disconnect(host, &Device::clipboardPaste, client, &Device::clipboardPaste); diff --git a/README.md b/README.md index 64a7ed3..742b7df 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,32 @@ Note: it is not necessary to keep you Android device connected via USB after you breaks non-ASCII characters). - Group control +## Shortcuts + + | Action | Shortcut (Windows) | Shortcut (macOS) + | -------------------------------------- |:----------------------------- |:----------------------------- + | Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f` + | Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g` + | Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_ + | Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_ + | Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_ + | Click on `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s` + | Click on `MENU` | `Ctrl`+`m` | `Ctrl`+`m` + | Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_ + | Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_ + | Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p` + | Power on | _Right-click²_ | _Right-click²_ + | Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o` + | Expand notification panel | `Ctrl`+`n` | `Cmd`+`n` + | Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` + | Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c` + | Paste computer clipboard to device | `Ctrl`+`v` | `Cmd`+`v` + | Copy computer clipboard to device | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v` + +_¹Double-click on black borders to remove them._ + +_²Right-click turns the screen on if it was off, presses BACK otherwise._ + ## TODO [TODO](docs/TODO.md) diff --git a/README_zh.md b/README_zh.md index 54e4360..12d63ad 100644 --- a/README_zh.md +++ b/README_zh.md @@ -179,6 +179,32 @@ Mac OS平台,你可以直接使用我编译好的可执行程序: - `Ctrl` +`v` 将计算机剪贴板作为一系列文本事件发送到设备(不支持非ASCII字符)。 - 群控 +## 快捷键 + + | 功能 | 快捷键(Windows) | 快捷键 (macOS) + | -------------------------------------- |:----------------------------- |:----------------------------- + | 切换全屏 | `Ctrl`+`f` | `Cmd`+`f` + | 调整窗口大小为 1:1 | `Ctrl`+`g` | `Cmd`+`g` + | 调整窗口大小去除黑边 | `Ctrl`+`x` \| _左键双击_ | `Cmd`+`x` \| _左键双击_ + | 点击 `主页` | `Ctrl`+`h` \| _点击鼠标中键_ | `Ctrl`+`h` \| _点击鼠标中键_ + | 点击 `BACK` | `Ctrl`+`b` \| _右键双击_ | `Cmd`+`b` \| _右键双击_ + | 点击 `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s` + | 点击 `MENU` | `Ctrl`+`m` | `Ctrl`+`m` + | 点击 `VOLUME_UP` | `Ctrl`+`↑` _(上)_ | `Cmd`+`↑` _(上)_ + | 点击 `VOLUME_DOWN` | `Ctrl`+`↓` _(下)_ | `Cmd`+`↓` _(下)_ + | 点击 `POWER` | `Ctrl`+`p` | `Cmd`+`p` + | 打开电源 | _右键双击_ | _右键双击_ + | 关闭屏幕 (保持投屏) | `Ctrl`+`o` | `Cmd`+`o` + | 打开下拉菜单 | `Ctrl`+`n` | `Cmd`+`n` + | 关闭下拉菜单 | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` + | 复制设备剪切板到电脑 | `Ctrl`+`c` | `Cmd`+`c` + | 粘贴电脑剪切板到设备 | `Ctrl`+`v` | `Cmd`+`v` + | 复制电脑剪切板到设备 | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v` + +鼠标左键双击黑色区域可以去除黑色区域 + +如果电源关闭,鼠标右键双击打开电源;如果电源开启,鼠标右键双击相当于返回 + ## TODO [后期计划](docs/TODO.md) From 748603a587755006f44ead78cbae99661ee4361f Mon Sep 17 00:00:00 2001 From: rankun Date: Mon, 16 Mar 2020 12:47:10 +0800 Subject: [PATCH 19/19] feat: add framess window --- QtScrcpy/device/device.cpp | 2 +- QtScrcpy/device/ui/videoform.cpp | 5 +- QtScrcpy/device/ui/videoform.h | 2 +- QtScrcpy/dialog.cpp | 7 +++ QtScrcpy/dialog.h | 2 + QtScrcpy/dialog.ui | 47 ++++++++++-------- QtScrcpy/res/i18n/QtScrcpy_en.qm | Bin 3739 -> 3801 bytes QtScrcpy/res/i18n/QtScrcpy_en.ts | 81 ++++++++++++++++--------------- QtScrcpy/res/i18n/QtScrcpy_zh.qm | Bin 2868 -> 2918 bytes QtScrcpy/res/i18n/QtScrcpy_zh.ts | 81 ++++++++++++++++--------------- QtScrcpy/util/config.cpp | 19 ++++++++ QtScrcpy/util/config.h | 2 + README.md | 2 +- README_zh.md | 2 +- 14 files changed, 151 insertions(+), 101 deletions(-) diff --git a/QtScrcpy/device/device.cpp b/QtScrcpy/device/device.cpp index 5cb0396..a0bda6b 100644 --- a/QtScrcpy/device/device.cpp +++ b/QtScrcpy/device/device.cpp @@ -35,7 +35,7 @@ Device::Device(DeviceParams params, QObject *parent) m_decoder = new Decoder(m_vb, this); m_fileHandler = new FileHandler(this); m_controller = new Controller(params.gameScript, this); - m_videoForm = new VideoForm(Config::getInstance().getSkin()); + m_videoForm = new VideoForm(Config::getInstance().getFramelessWindow(), Config::getInstance().getSkin()); m_videoForm->setDevice(this); } diff --git a/QtScrcpy/device/ui/videoform.cpp b/QtScrcpy/device/ui/videoform.cpp index 223897d..8fea7db 100644 --- a/QtScrcpy/device/ui/videoform.cpp +++ b/QtScrcpy/device/ui/videoform.cpp @@ -25,7 +25,7 @@ extern "C" #include "libavutil/frame.h" } -VideoForm::VideoForm(bool skin, QWidget *parent) +VideoForm::VideoForm(bool framelessWindow, bool skin, QWidget *parent) : QWidget(parent) , ui(new Ui::videoForm) , m_skin(skin) @@ -38,6 +38,9 @@ VideoForm::VideoForm(bool skin, QWidget *parent) if (m_skin) { updateStyleSheet(vertical); } + if (framelessWindow) { + setWindowFlags(windowFlags() | Qt::FramelessWindowHint); + } } VideoForm::~VideoForm() diff --git a/QtScrcpy/device/ui/videoform.h b/QtScrcpy/device/ui/videoform.h index 9de6438..c3b4fc6 100644 --- a/QtScrcpy/device/ui/videoform.h +++ b/QtScrcpy/device/ui/videoform.h @@ -17,7 +17,7 @@ class VideoForm : public QWidget { Q_OBJECT public: - explicit VideoForm(bool skin = true, QWidget *parent = 0); + explicit VideoForm(bool framelessWindow = false, bool skin = true, QWidget *parent = 0); ~VideoForm(); void staysOnTop(bool top = true); diff --git a/QtScrcpy/dialog.cpp b/QtScrcpy/dialog.cpp index 938b6d5..3aa54bb 100644 --- a/QtScrcpy/dialog.cpp +++ b/QtScrcpy/dialog.cpp @@ -98,6 +98,7 @@ void Dialog::initUI() ui->formatBox->setCurrentIndex(Config::getInstance().getRecordFormatIndex()); ui->recordPathEdt->setText(Config::getInstance().getRecordPath()); + ui->framelessCheck->setChecked(Config::getInstance().getFramelessWindow()); #ifdef Q_OS_OSX // mac need more width @@ -384,3 +385,9 @@ void Dialog::on_formatBox_activated(int index) { Config::getInstance().setRecordFormatIndex(index); } + +void Dialog::on_framelessCheck_stateChanged(int arg1) +{ + Q_UNUSED(arg1) + Config::getInstance().setFramelessWindow(ui->framelessCheck->isChecked()); +} diff --git a/QtScrcpy/dialog.h b/QtScrcpy/dialog.h index a55361f..2b871a3 100644 --- a/QtScrcpy/dialog.h +++ b/QtScrcpy/dialog.h @@ -62,6 +62,8 @@ private slots: void on_formatBox_activated(int index); + void on_framelessCheck_stateChanged(int arg1); + private: bool checkAdbRun(); void initUI(); diff --git a/QtScrcpy/dialog.ui b/QtScrcpy/dialog.ui index f22349b..9060e15 100644 --- a/QtScrcpy/dialog.ui +++ b/QtScrcpy/dialog.ui @@ -215,6 +215,22 @@ + + + + + 0 + 0 + + + + always on top + + + false + + + @@ -228,22 +244,6 @@ - - - - - 0 - 0 - - - - background record - - - false - - - @@ -260,8 +260,8 @@ - - + + 0 @@ -269,13 +269,20 @@ - always on top + background record - + false + + + + frameless + + + diff --git a/QtScrcpy/res/i18n/QtScrcpy_en.qm b/QtScrcpy/res/i18n/QtScrcpy_en.qm index d6717b1ef201fdbedf02cb18dc90591c65e9d1ac..d68f816f5ac8ea34010f9b139fa44daec994c5d3 100644 GIT binary patch delta 417 zcmbO&dsB9Th~@+a)@?Bi3|t%xY~M^77`XKr*lj`?7`P)CWHwtcFz`q*n29c8U|=_& zXe(~n$k3zd%)r3-iP7@WIR*x<97cON2L=WfO~!zu(-{~T8yI)BOk!YQFJtDuD#yUU z6U}^>YXJiT&&-LvYV{ljY{y@E=Fc2)MyDAV7;bPR%FSY6 z;5g6mk6DI+fn_15gHb921N$$|#0xA83~c{6OC!E8Ffd)!sB%e46K>_nJc6i7+9tGYhMR2FfgU?@47sTfq~@;|J}~5 z3=E7SnG6iP8q5p~40;Tzn^!V^U{(`iNMk5sNMy)mNM*=jNM$HyDCPhK1sekc17})M QVs2_qYH{)APF5jK01j7YQ2+n{ delta 378 zcmca9J6m>wh-L=^>$VsM2F^p`*+Vc1_rL(9C1dc85kIDa3spjVqoBy$?=a_hJk@4 zo72H4m4Si%2xsC276t~k6P%?HUl)~=ZwvVBnfsKi)s+o&{fyI>j zfc8xW2Cj1M6Z_p57}$b&oZ>hWyPt8DB7OzRt?T2>?5aU=08O diff --git a/QtScrcpy/res/i18n/QtScrcpy_en.ts b/QtScrcpy/res/i18n/QtScrcpy_en.ts index f158eee..9592331 100644 --- a/QtScrcpy/res/i18n/QtScrcpy_en.ts +++ b/QtScrcpy/res/i18n/QtScrcpy_en.ts @@ -16,22 +16,22 @@ file transfer failed - + install apk install apk - + file transfer file transfer - + wait current %1 to complete wait current %1 to complete - + %1 complete, save in %2 %1 complete, save in %2 @@ -41,7 +41,7 @@ %1 complete\n save in %2 - + %1 failed %1 failed @@ -49,17 +49,17 @@ Dialog - + Wireless Wireless - + wireless connect wireless connect - + wireless disconnect wireless disconnect @@ -75,7 +75,7 @@ - + select path select path @@ -90,27 +90,32 @@ record screen - + + frameless + frameless + + + stop all server stop all server - + adb command: adb command: - + terminate terminate - + execute execute - + clear clear @@ -124,12 +129,12 @@ auto enable - + background record background record - + screen-off screen-off @@ -144,7 +149,7 @@ max size: - + always on top always on top @@ -154,27 +159,27 @@ refresh script - + get device IP get device IP - + USB line USB line - + stop server stop server - + start server start server - + device serial: device serial: @@ -188,12 +193,12 @@ bit rate: - + start adbd start adbd - + refresh devices refresh devices @@ -206,7 +211,7 @@ QObject - + This software is completely open source and free, you can download it at the following address: This software is completely open source and free, you can download it at the following address: @@ -219,12 +224,12 @@ Tool - + full screen full screen - + expand notify expand notify @@ -237,52 +242,52 @@ turn on - + touch switch touch switch - + close screen close screen - + power power - + volume up volume up - + volume down volume down - + app switch app switch - + menu menu - + home home - + return return - + screen shot screen shot @@ -302,7 +307,7 @@ file transfer failed - + file does not exist file does not exist diff --git a/QtScrcpy/res/i18n/QtScrcpy_zh.qm b/QtScrcpy/res/i18n/QtScrcpy_zh.qm index 3552ab7ad6c5dda9a8bbe98353c97ba9b1821a8a..7bcbdadaf83f7736a718185a2cbcb3a6bbbc06c5 100644 GIT binary patch delta 405 zcmdlY_DpPoh~@+a)@?Bi4D4PEY~M^77&vqp*lj`?7&t;0WHwtcFmQZgFcV$Gz`$BR z(N^5DlA%Y_nSp_^gwgWRIR*yyd5rdQ4h#&;`iucbr!z1xN-^$gnZ&@rx{jIqsvH9Y zr#katt_2JXoS_qY)$6U;j=ySQU|?`#d(q&;z`(%3?jSA9z`zpD?ymimfq~JJy=PA! z0|Uo=_BFjb7#P^|*`H}WVqjo>!v5Vej)8%Lk;BGOnSp_sl_SpRGy?;}5{^Wc?3c@r8kbNsUWxl>`GrJ*xtj!?Aq~3@rP& zs+zeN7?_i}4`|JbePX{G0|U!v9;dhv1_qX1o;vd@3=AB{cwVnL%D}*MpHJGd zg@J)Bmv8lD5e5d%Mt+6I>lheV)cG@4NHH)lzvZue9l*fAc#eP90 zV7QXWz`(hOnSp^pgh7>I^D0J0W+}GR2mL!U+&Dn~XJcSs;7lt@%uUTnEiT?Xn`I^& E0CK)#H~;_u delta 378 zcmaDRwnc1$h-L=^>$VsM26hDowr{2k3>?f1>^7kc3>;bvGMgsRX9r{zA!K_sd34zl3-w9`NrjNY##$dJQC)8@M@oooPca9!~L diff --git a/QtScrcpy/res/i18n/QtScrcpy_zh.ts b/QtScrcpy/res/i18n/QtScrcpy_zh.ts index 5001248..f3e0056 100644 --- a/QtScrcpy/res/i18n/QtScrcpy_zh.ts +++ b/QtScrcpy/res/i18n/QtScrcpy_zh.ts @@ -16,22 +16,22 @@ 文件传输失败 - + install apk 安装apk - + file transfer 文件传输 - + wait current %1 to complete 等待当前%1完成 - + %1 complete, save in %2 %1完成,保存在%2 @@ -41,7 +41,7 @@ %1完成\n 保存在 %2 - + %1 failed %1 失败 @@ -49,17 +49,17 @@ Dialog - + Wireless 无线 - + wireless connect 无线连接 - + wireless disconnect 无线断开 @@ -75,7 +75,7 @@ - + select path 选择路径 @@ -90,27 +90,32 @@ 录制屏幕 - + + frameless + 无边框 + + + stop all server 停止所有服务 - + adb command: adb命令: - + terminate 终止 - + execute 执行 - + clear 清理 @@ -124,12 +129,12 @@ 自动启用脚本 - + background record 后台录制 - + screen-off 自动息屏 @@ -144,7 +149,7 @@ 最大尺寸: - + always on top 窗口置顶 @@ -154,27 +159,27 @@ 刷新脚本 - + get device IP 获取设备IP - + USB line USB线 - + stop server 停止服务 - + start server 启动服务 - + device serial: 设备序列号: @@ -188,12 +193,12 @@ 比特率: - + start adbd 启动adbd - + refresh devices 刷新设备列表 @@ -206,7 +211,7 @@ QObject - + This software is completely open source and free, you can download it at the following address: 本软件完全开源免费,你可以在下面的地址下载: @@ -219,12 +224,12 @@ 工具 - + full screen 全屏 - + expand notify 下拉通知 @@ -237,52 +242,52 @@ 解锁 - + touch switch 触摸显示开关 - + close screen 关闭屏幕 - + power 电源 - + volume up 音量加 - + volume down 音量减 - + app switch 切换应用 - + menu 菜单 - + home 主界面 - + return 返回 - + screen shot 截图 @@ -302,7 +307,7 @@ 文件传输失败 - + file does not exist 文件不存在 diff --git a/QtScrcpy/util/config.cpp b/QtScrcpy/util/config.cpp index 2f7b0a7..ea39ea7 100644 --- a/QtScrcpy/util/config.cpp +++ b/QtScrcpy/util/config.cpp @@ -50,6 +50,9 @@ #define SERIAL_WINDOW_RECT_KEY_H "WindowRectH" #define SERIAL_WINDOW_RECT_KEY_DEF -1 +#define COMMON_FRAMELESS_WINDOW_KEY "FramelessWindow" +#define COMMON_FRAMELESS_WINDOW_DEF false + // 最大尺寸 录制格式 QString Config::s_configPath = ""; @@ -169,6 +172,22 @@ QRect Config::getRect(const QString &serial) return rc; } +void Config::setFramelessWindow(bool frameless) +{ + m_userData->beginGroup(GROUP_COMMON); + m_userData->setValue(COMMON_FRAMELESS_WINDOW_KEY, frameless); + m_userData->endGroup(); +} + +bool Config::getFramelessWindow() +{ + bool framelessWindow = false; + m_userData->beginGroup(GROUP_COMMON); + framelessWindow = m_userData->value(COMMON_FRAMELESS_WINDOW_KEY, COMMON_FRAMELESS_WINDOW_DEF).toBool(); + m_userData->endGroup(); + return framelessWindow; +} + QString Config::getServerVersion() { QString server; diff --git a/QtScrcpy/util/config.h b/QtScrcpy/util/config.h index e635278..28cfec2 100644 --- a/QtScrcpy/util/config.h +++ b/QtScrcpy/util/config.h @@ -32,6 +32,8 @@ public: void setRecordFormatIndex(int recordFormatIndex); void setRect(const QString &serial, const QRect &rc); QRect getRect(const QString &serial); + bool getFramelessWindow(); + void setFramelessWindow(bool frameless); private: explicit Config(QObject *parent = nullptr); diff --git a/README.md b/README.md index 742b7df..4003f8e 100644 --- a/README.md +++ b/README.md @@ -226,7 +226,7 @@ There are several reasons listed as below according to importance (high to low). All the dependencies are provided and it is easy to compile. ### PC client -1. Set up the Qt development environment on the target platform (Qt >= 5.9.7, vs >= 2015 (mingw not supported)) +1. Set up the Qt development environment on the target platform (Qt >= 5.12.0, vs >= 2017 (mingw not supported)) 2. Clone the project 3. Open the project root directory all.pro with QtCreator 4. Compile and run diff --git a/README_zh.md b/README_zh.md index 12d63ad..f1c6dcc 100644 --- a/README_zh.md +++ b/README_zh.md @@ -226,7 +226,7 @@ Mac OS平台,你可以直接使用我编译好的可执行程序: 尽量提供了所有依赖资源,方便傻瓜式编译。 ### PC端 -1. 目标平台上搭建Qt开发环境(Qt >= 5.9.7, vs >= 2015 (**不支持mingw**)) +1. 目标平台上搭建Qt开发环境(Qt >= 5.12.0, vs >= 2017 (**不支持mingw**)) 2. 克隆该项目 3. 使用QtCreator打开项目根目录all.pro 4. 编译,运行即可