blob: 4d3df606035c22da78e548c3c3236e2e72a20551 [file] [log] [blame]
/* Copyright (C) 2015 The Android Open Source Project
**
** This software is licensed under the terms of the GNU General Public
** License version 2, as published by the Free Software Foundation, and
** may be copied, distributed, and modified under those terms.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
*/
#include <QCoreApplication>
#include <QDateTime>
#include <QPushButton>
#include <QSettings>
#include <QtWidgets>
#include "android/android.h"
#include "android/base/files/PathUtils.h"
#include "android/base/system/System.h"
#include "android/emulation/ConfigDirs.h"
#include "android/emulation/control/clipboard_agent.h"
#include "android/globals.h"
#include "android/main-common.h"
#include "android/skin/event.h"
#include "android/skin/keycode.h"
#include "android/skin/qt/emulator-qt-window.h"
#include "android/skin/qt/error-dialog.h"
#include "android/skin/qt/extended-pages/common.h"
#include "android/skin/qt/extended-pages/location-page.h"
#include "android/skin/qt/extended-window.h"
#include "android/skin/qt/extended-window-styles.h"
#include "android/skin/qt/extended-window.h"
#include "android/skin/qt/qt-settings.h"
#include "android/skin/qt/qt-ui-commands.h"
#include "android/skin/qt/stylesheet.h"
#include "android/skin/qt/tool-window.h"
#include "android/utils/debug.h"
#include <cassert>
#include <string>
using namespace android::base;
static ToolWindow* twInstance = NULL;
static void onGuestClipboardChanged(
const uint8_t* data,
size_t length) {
QString content = QString::fromUtf8((const char*)data, length);
QApplication::clipboard()->blockSignals(true);
QApplication::clipboard()->setText(content);
QApplication::clipboard()->blockSignals(false);
}
extern "C" void setUiEmuAgent(const UiEmuAgent* agentPtr) {
if (agentPtr->clipboard) {
agentPtr->clipboard->setGuestClipboardCallback(onGuestClipboardChanged);
QObject::connect(
QApplication::clipboard(), &QClipboard::dataChanged,
[agentPtr]() {
QByteArray bytes = QApplication::clipboard()->text().toUtf8();
agentPtr->clipboard->setGuestClipboardContents(
(const uint8_t*)bytes.data(), bytes.size());
});
}
if (twInstance) {
twInstance->setToolEmuAgent(agentPtr);
}
}
ToolWindow::ToolWindow(EmulatorQtWindow* window,
QWidget* parent,
ToolWindow::UIEventRecorderPtr event_recorder,
ToolWindow::UserActionsCounterPtr user_actions_counter)
: QFrame(parent),
mEmulatorWindow(window),
mExtendedWindow(android::base::makeCustomScopedPtr<ExtendedWindow*>(
nullptr,
[](QObject* o) { o->deleteLater(); })),
mUiEmuAgent(nullptr),
mToolsUi(new Ui::ToolControls),
mUIEventRecorder(event_recorder),
mUserActionsCounter(user_actions_counter),
mSizeTweaker(this) {
twInstance = this;
// "Tool" type windows live in another layer on top of everything in OSX, which
// is undesirable
// because it means the extended window must be on top of the emulator window.
// However, on
// Windows and Linux, "Tool" type windows are the only way to make a window that
// does not have
// its own taskbar item.
#ifdef __APPLE__
Qt::WindowFlags flag = Qt::Dialog;
#else
Qt::WindowFlags flag = Qt::Tool;
#endif
setWindowFlags(flag | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint);
mToolsUi->setupUi(this);
// Get the latest user selections from the user-config code.
QSettings settings;
SettingsTheme theme =
(SettingsTheme)settings.value(Ui::Settings::UI_THEME, 0).toInt();
if (theme < 0 || theme >= SETTINGS_THEME_NUM_ENTRIES) {
theme = (SettingsTheme)0;
settings.setValue(Ui::Settings::UI_THEME, 0);
}
adjustAllButtonsForTheme(theme);
this->setStyleSheet(Ui::stylesheetForTheme(theme));
QString default_shortcuts =
"Ctrl+Shift+L SHOW_PANE_LOCATION\n"
"Ctrl+Shift+C SHOW_PANE_CELLULAR\n"
"Ctrl+Shift+B SHOW_PANE_BATTERY\n"
"Ctrl+Shift+P SHOW_PANE_PHONE\n"
"Ctrl+Shift+V SHOW_PANE_VIRTSENSORS\n"
"Ctrl+Shift+F SHOW_PANE_FINGER\n"
"Ctrl+Shift+D SHOW_PANE_DPAD\n"
"Ctrl+Shift+S SHOW_PANE_SETTINGS\n"
#ifdef __APPLE__
"Ctrl+/ SHOW_PANE_HELP\n"
#else
"F1 SHOW_PANE_HELP\n"
#endif
"Ctrl+S TAKE_SCREENSHOT\n"
"Ctrl+Z ENTER_ZOOM\n"
"Ctrl+Up ZOOM_IN\n"
"Ctrl+Down ZOOM_OUT\n"
"Ctrl+Shift+Up PAN_UP\n"
"Ctrl+Shift+Down PAN_DOWN\n"
"Ctrl+Shift+Left PAN_LEFT\n"
"Ctrl+Shift+Right PAN_RIGHT\n"
"Ctrl+= VOLUME_UP\n"
"Ctrl+- VOLUME_DOWN\n"
"Ctrl+P POWER\n"
"Ctrl+M MENU\n"
"Ctrl+T TOGGLE_TRACKBALL\n"
#ifndef __APPLE__
"Ctrl+H HOME\n"
#else
"Ctrl+Shift+H HOME\n"
#endif
"Ctrl+O OVERVIEW\n"
"Ctrl+Backspace BACK\n"
"Ctrl+Left ROTATE_LEFT\n"
"Ctrl+Right ROTATE_RIGHT\n";
QTextStream stream(&default_shortcuts);
mShortcutKeyStore.populateFromTextStream(stream, parseQtUICommand);
// Need to add this one separately because QKeySequence cannot parse
// the string "Ctrl".
mShortcutKeyStore.add(QKeySequence(Qt::Key_Control | Qt::ControlModifier),
QtUICommand::SHOW_MULTITOUCH);
// Update tool tips on all push buttons.
const QList<QPushButton*> childButtons =
findChildren<QPushButton*>(QString(), Qt::FindDirectChildrenOnly);
for (auto button : childButtons) {
QVariant uiCommand = button->property("uiCommand");
if (uiCommand.isValid()) {
QtUICommand cmd;
if (parseQtUICommand(uiCommand.toString(), &cmd)) {
QVector<QKeySequence>* shortcuts =
mShortcutKeyStore.reverseLookup(cmd);
if (shortcuts && shortcuts->length() > 0) {
button->setToolTip(getQtUICommandDescription(cmd) + " (" +
shortcuts->at(0).toString(
QKeySequence::NativeText) +
")");
}
}
} else if (button != mToolsUi->close_button &&
button != mToolsUi->minimize_button &&
button != mToolsUi->more_button) {
// Almost all toolbar buttons are required to have a uiCommand
// property.
// Unfortunately, we have no way of enforcing it at compile time.
assert(0);
}
}
createExtendedWindow(); // But don't show it yet
#ifndef Q_OS_MAC
// Swap minimize and close buttons on non-apple OSes
int tmp_x = mToolsUi->close_button->x();
mToolsUi->close_button->move(mToolsUi->minimize_button->x(),
mToolsUi->close_button->y());
mToolsUi->minimize_button->move(tmp_x, mToolsUi->minimize_button->y());
#endif
}
ToolWindow::~ToolWindow() {}
void ToolWindow::hide() {
QFrame::hide();
assert(mExtendedWindow);
mExtendedWindow->hide();
}
void ToolWindow::closeEvent(QCloseEvent* ce) {
// make sure only parent processes the event - otherwise some
// siblings won't get it, e.g. main window
ce->ignore();
}
void ToolWindow::mousePressEvent(QMouseEvent* event) {
raiseMainWindow();
QFrame::mousePressEvent(event);
}
void ToolWindow::hideEvent(QHideEvent*) {
assert(mExtendedWindow);
mIsExtendedWindowVisibleOnShow = mExtendedWindow->isVisible();
}
void ToolWindow::show() {
QFrame::show();
setFixedSize(size());
if (mIsExtendedWindowVisibleOnShow) {
assert(mExtendedWindow);
mExtendedWindow->show();
}
}
void ToolWindow::handleUICommand(QtUICommand cmd, bool down) {
switch (cmd) {
case QtUICommand::SHOW_PANE_LOCATION:
if (down) {
showOrRaiseExtendedWindow(PANE_IDX_LOCATION);
}
break;
case QtUICommand::SHOW_PANE_CELLULAR:
if (down) {
showOrRaiseExtendedWindow(PANE_IDX_CELLULAR);
}
break;
case QtUICommand::SHOW_PANE_BATTERY:
if (down) {
showOrRaiseExtendedWindow(PANE_IDX_BATTERY);
}
break;
case QtUICommand::SHOW_PANE_PHONE:
if (down) {
showOrRaiseExtendedWindow(PANE_IDX_TELEPHONE);
}
break;
case QtUICommand::SHOW_PANE_VIRTSENSORS:
if (down) {
showOrRaiseExtendedWindow(PANE_IDX_VIRT_SENSORS);
}
break;
case QtUICommand::SHOW_PANE_DPAD:
if (down) {
showOrRaiseExtendedWindow(PANE_IDX_DPAD);
}
break;
case QtUICommand::SHOW_PANE_FINGER:
if (down) {
showOrRaiseExtendedWindow(PANE_IDX_FINGER);
}
break;
case QtUICommand::SHOW_PANE_SETTINGS:
if (down) {
showOrRaiseExtendedWindow(PANE_IDX_SETTINGS);
}
break;
case QtUICommand::SHOW_PANE_HELP:
if (down) {
showOrRaiseExtendedWindow(PANE_IDX_HELP);
}
break;
case QtUICommand::TAKE_SCREENSHOT:
if (down) {
mEmulatorWindow->screenshot();
}
break;
case QtUICommand::ENTER_ZOOM:
if (down) {
mEmulatorWindow->toggleZoomMode();
}
mToolsUi->zoom_button->setChecked(mEmulatorWindow->isInZoomMode());
break;
case QtUICommand::ZOOM_IN:
if (down) {
if (mEmulatorWindow->isInZoomMode()) {
mEmulatorWindow->zoomIn();
} else {
mEmulatorWindow->scaleUp();
}
}
break;
case QtUICommand::ZOOM_OUT:
if (down) {
if (mEmulatorWindow->isInZoomMode()) {
mEmulatorWindow->zoomOut();
} else {
mEmulatorWindow->scaleDown();
}
}
break;
case QtUICommand::PAN_UP:
if (down) {
mEmulatorWindow->panVertical(true);
}
break;
case QtUICommand::PAN_DOWN:
if (down) {
mEmulatorWindow->panVertical(false);
}
break;
case QtUICommand::PAN_LEFT:
if (down) {
mEmulatorWindow->panHorizontal(true);
}
break;
case QtUICommand::PAN_RIGHT:
if (down) {
mEmulatorWindow->panHorizontal(false);
}
break;
case QtUICommand::VOLUME_UP:
forwardKeyToEmulator(KEY_VOLUMEUP, down);
break;
case QtUICommand::VOLUME_DOWN:
forwardKeyToEmulator(KEY_VOLUMEDOWN, down);
break;
case QtUICommand::POWER:
forwardKeyToEmulator(KEY_POWER, down);
break;
case QtUICommand::MENU:
forwardKeyToEmulator(KEY_SOFT1, down);
break;
case QtUICommand::HOME:
forwardKeyToEmulator(KEY_HOME, down);
break;
case QtUICommand::BACK:
forwardKeyToEmulator(KEY_BACK, down);
break;
case QtUICommand::OVERVIEW:
forwardKeyToEmulator(KEY_APPSWITCH, down);
break;
case QtUICommand::ROTATE_RIGHT:
case QtUICommand::ROTATE_LEFT:
if (down) {
// TODO: remove this after we preserve zoom after rotate
if (mEmulatorWindow->isInZoomMode()) {
mToolsUi->zoom_button->click();
}
// Rotating the emulator preserves size, but this can be a
// problem
// if, for example, a very-wide emulator in landscape is rotated
// to
// portrait. To avoid this situation (which makes the scroll
// bars
// appear), force a resize to the new size.
QSize containerSize = mEmulatorWindow->containerSize();
mEmulatorWindow->doResize(
QSize(containerSize.height(), containerSize.width()),
true, true);
SkinEvent* skin_event = new SkinEvent();
skin_event->type = cmd == QtUICommand::ROTATE_RIGHT
? kEventLayoutNext
: kEventLayoutPrev;
// TODO(grigoryj): debug output needed for investigating the
// rotation
// bug.
if (VERBOSE_CHECK(rotation)) {
qWarning("Queuing skin event for %s",
cmd == QtUICommand::ROTATE_RIGHT ? "ROTATE_RIGHT"
: "ROTATE_LEFT");
}
mEmulatorWindow->queueSkinEvent(skin_event);
}
break;
case QtUICommand::TOGGLE_TRACKBALL:
if (down) {
SkinEvent* skin_event = new SkinEvent();
skin_event->type = kEventToggleTrackball;
mEmulatorWindow->queueSkinEvent(skin_event);
}
break;
case QtUICommand::SHOW_MULTITOUCH:
// Multitouch is handled in EmulatorQtWindow, and doesn't
// really need an element in the QtUICommand enum. This
// enum element exists solely for the purpose of displaying
// it in the list of keyboard shortcuts in the Help page.
default:;
}
}
void ToolWindow::forwardKeyToEmulator(uint32_t keycode, bool down) {
SkinEvent* skin_event = new SkinEvent();
skin_event->type = down ? kEventKeyDown : kEventKeyUp;
skin_event->u.key.keycode = keycode;
skin_event->u.key.mod = 0;
mEmulatorWindow->queueSkinEvent(skin_event);
}
bool ToolWindow::handleQtKeyEvent(QKeyEvent* event) {
// We don't care about the keypad modifier for anything, and it gets added
// to the arrow keys of OSX by default, so remove it.
QKeySequence event_key_sequence(event->key() +
(event->modifiers() & ~Qt::KeypadModifier));
bool down = event->type() == QEvent::KeyPress;
bool h = mShortcutKeyStore.handle(event_key_sequence,
[this, down](QtUICommand cmd) {
if (down) {
handleUICommand(cmd, true);
handleUICommand(cmd, false);
}
});
return h;
}
void ToolWindow::closeExtendedWindow() {
// If user is clicking the 'x' button like crazy, we may get multiple
// close events here, so make sure the function doesn't screw the state for
// a next call.
if (mExtendedWindow) {
mExtendedWindow->close();
mExtendedWindow.reset();
}
}
void ToolWindow::dockMainWindow() {
#ifdef __linux__
// On Linux, the gap between the main window and
// the tool bar is 8 pixels bigger than is expected.
// Kludge a correction.
const static int gapAdjust = -8;
#else
// Windows and OSX are OK
const static int gapAdjust = 0;
#endif
// Align horizontally relative to the main window's frame.
// Align vertically to its contents.
move(parentWidget()->frameGeometry().right() + toolGap + gapAdjust,
parentWidget()->geometry().top());
}
void ToolWindow::raiseMainWindow() {
mEmulatorWindow->raise();
mEmulatorWindow->activateWindow();
}
void ToolWindow::setToolEmuAgent(const UiEmuAgent* agPtr) {
mUiEmuAgent = agPtr;
assert(mExtendedWindow);
mExtendedWindow->setAgent(agPtr);
}
void ToolWindow::on_back_button_pressed() {
mEmulatorWindow->raise();
handleUICommand(QtUICommand::BACK, true);
}
void ToolWindow::on_back_button_released() {
mEmulatorWindow->activateWindow();
handleUICommand(QtUICommand::BACK, false);
}
void ToolWindow::on_close_button_clicked() {
parentWidget()->close();
}
void ToolWindow::on_home_button_pressed() {
mEmulatorWindow->raise();
handleUICommand(QtUICommand::HOME, true);
}
void ToolWindow::on_home_button_released() {
mEmulatorWindow->activateWindow();
handleUICommand(QtUICommand::HOME, false);
}
void ToolWindow::on_minimize_button_clicked() {
// showMinimized() on OSX will put the toolbar in the minimized state,
// which is undesired. We only want the main window to minimize, so
// hide it instead.
this->hide();
mEmulatorWindow->showMinimized();
}
void ToolWindow::on_power_button_pressed() {
mEmulatorWindow->raise();
handleUICommand(QtUICommand::POWER, true);
}
void ToolWindow::on_power_button_released() {
mEmulatorWindow->activateWindow();
handleUICommand(QtUICommand::POWER, false);
}
void ToolWindow::on_volume_up_button_pressed() {
mEmulatorWindow->raise();
handleUICommand(QtUICommand::VOLUME_UP, true);
}
void ToolWindow::on_volume_up_button_released() {
mEmulatorWindow->activateWindow();
handleUICommand(QtUICommand::VOLUME_UP, false);
}
void ToolWindow::on_volume_down_button_pressed() {
mEmulatorWindow->raise();
handleUICommand(QtUICommand::VOLUME_DOWN, true);
}
void ToolWindow::on_volume_down_button_released() {
mEmulatorWindow->activateWindow();
handleUICommand(QtUICommand::VOLUME_DOWN, false);
}
void ToolWindow::on_overview_button_pressed() {
mEmulatorWindow->raise();
handleUICommand(QtUICommand::OVERVIEW, true);
}
void ToolWindow::on_overview_button_released() {
mEmulatorWindow->activateWindow();
handleUICommand(QtUICommand::OVERVIEW, false);
}
void ToolWindow::on_prev_layout_button_clicked() {
handleUICommand(QtUICommand::ROTATE_LEFT);
}
void ToolWindow::on_next_layout_button_clicked() {
handleUICommand(QtUICommand::ROTATE_RIGHT);
}
void ToolWindow::on_scrShot_button_clicked() {
handleUICommand(QtUICommand::TAKE_SCREENSHOT, true);
}
void ToolWindow::on_zoom_button_clicked() {
handleUICommand(QtUICommand::ENTER_ZOOM, true);
}
void ToolWindow::showOrRaiseExtendedWindow(ExtendedWindowPane pane) {
// Show the tabbed pane
assert(mExtendedWindow);
mExtendedWindow->showPane(pane);
mExtendedWindow->raise();
mExtendedWindow->activateWindow();
}
void ToolWindow::on_more_button_clicked() {
assert(mExtendedWindow);
mExtendedWindow->show();
mExtendedWindow->raise();
mExtendedWindow->activateWindow();
}
void ToolWindow::createExtendedWindow() {
mExtendedWindow.reset(
new ExtendedWindow(mEmulatorWindow,
this,
&mShortcutKeyStore));
if (auto recorder_ptr = mUIEventRecorder.lock()) {
recorder_ptr->startRecording(mExtendedWindow.get());
}
if (auto user_actions_counter = mUserActionsCounter.lock()) {
user_actions_counter->startCountingForExtendedWindow(
mExtendedWindow.get());
}
// The extended window is created before the "..." button is pressed, so it
// should be hid until that button is actually pressed.
mExtendedWindow->hide();
}
void ToolWindow::paintEvent(QPaintEvent*) {
QPainter p;
QPen pen(Qt::SolidLine);
pen.setColor(Qt::black);
pen.setWidth(1);
p.begin(this);
p.setPen(pen);
double dpr = 1.0;
int primary_screen_idx = qApp->desktop()->screenNumber(this);
QScreen* primary_screen = QApplication::screens().at(primary_screen_idx);
if (primary_screen) {
dpr = primary_screen->devicePixelRatio();
}
if (dpr > 1.0) {
// Normally you'd draw the border with a (0, 0 - w-1, h-1) rectangle.
// However, there's some weirdness going on with high-density displays
// that makes a single-pixel "slack" appear at the left and bottom
// of the border. This basically adds 1 to compensate for it.
p.drawRect(contentsRect());
} else {
p.drawRect(QRect(0, 0, width() - 1, height() - 1));
}
p.end();
}