mirror of https://github.com/aidygus/LinVAM.git
initial commit
This commit is contained in:
parent
cab4cd7eed
commit
821cee0db2
|
@ -0,0 +1,168 @@
|
||||||
|
from PyQt5.QtCore import *
|
||||||
|
from PyQt5.QtGui import *
|
||||||
|
from PyQt5.QtWidgets import *
|
||||||
|
from ui_commandeditwnd import Ui_CommandEditDialog
|
||||||
|
from keyactioneditwnd import KeyActionEditWnd
|
||||||
|
from mouseactioneditwnd import MouseActionEditWnd
|
||||||
|
from pauseactioneditwnd import PauseActionEditWnd
|
||||||
|
import json
|
||||||
|
import keyboard
|
||||||
|
|
||||||
|
class CommandEditWnd(QDialog):
|
||||||
|
def __init__(self, p_command, p_parent = None):
|
||||||
|
super().__init__(p_parent)
|
||||||
|
self.ui = Ui_CommandEditDialog()
|
||||||
|
self.ui.setupUi(self)
|
||||||
|
|
||||||
|
self.ui.deleteBut.clicked.connect(self.slotDelete)
|
||||||
|
self.ui.ok.clicked.connect(self.slotOK)
|
||||||
|
self.ui.cancel.clicked.connect(self.slotCancel)
|
||||||
|
self.ui.keyBut.clicked.connect(self.slotNewKeyEdit)
|
||||||
|
self.ui.mouseBut.clicked.connect(self.slotNewMouseEdit)
|
||||||
|
self.ui.pauseBut.clicked.connect(self.slotNewPauseEdit)
|
||||||
|
self.ui.upBut.clicked.connect(self.slotActionUp)
|
||||||
|
self.ui.downBut.clicked.connect(self.slotActionDown)
|
||||||
|
self.ui.editBut.clicked.connect(self.slotActionEdit)
|
||||||
|
self.ui.actionsListWidget.doubleClicked.connect(self.slotActionEdit)
|
||||||
|
|
||||||
|
w_otherMenu = QMenu()
|
||||||
|
w_otherMenu.addAction('Stop Another Command', self.slotStopAnotherCommand)
|
||||||
|
w_otherMenu.addAction('Execute Another Command', self.slotDoAnotherCommand)
|
||||||
|
self.ui.otherBut.setMenu(w_otherMenu)
|
||||||
|
|
||||||
|
self.m_command = {}
|
||||||
|
if p_command != None:
|
||||||
|
self.ui.say.setText(p_command['name'])
|
||||||
|
w_actions = p_command['actions']
|
||||||
|
for w_action in w_actions:
|
||||||
|
w_jsonAction = json.dumps(w_action)
|
||||||
|
w_item = QListWidgetItem(w_jsonAction)
|
||||||
|
w_item.setData(Qt.UserRole, w_jsonAction)
|
||||||
|
self.ui.actionsListWidget.addItem(w_item)
|
||||||
|
self.ui.asyncChk.setChecked(p_command['async'])
|
||||||
|
if p_command['repeat'] == -1:
|
||||||
|
self.ui.continueExe.setChecked(True)
|
||||||
|
elif p_command['repeat'] == 1:
|
||||||
|
self.ui.oneExe.setChecked(True)
|
||||||
|
else:
|
||||||
|
self.ui.repeatExe.setChecked(True)
|
||||||
|
self.ui.repeatCnt.setValue(p_command['repeat'])
|
||||||
|
else:
|
||||||
|
self.ui.asyncChk.setChecked(False)
|
||||||
|
self.ui.oneExe.setChecked(True)
|
||||||
|
|
||||||
|
def addAction(self, p_action):
|
||||||
|
w_jsonAction = json.dumps(p_action)
|
||||||
|
w_item = QListWidgetItem(w_jsonAction)
|
||||||
|
w_item.setData(Qt.UserRole, w_jsonAction)
|
||||||
|
self.ui.actionsListWidget.addItem(w_item)
|
||||||
|
|
||||||
|
def slotStopAnotherCommand(self):
|
||||||
|
text, okPressed = QInputDialog.getText(self, "Get Command Name", "Another command name:", QLineEdit.Normal, "")
|
||||||
|
if okPressed and text != '':
|
||||||
|
w_commandStopAction = {}
|
||||||
|
w_commandStopAction['name'] = 'command stop action'
|
||||||
|
w_commandStopAction['command name'] = text
|
||||||
|
self.addAction(w_commandStopAction)
|
||||||
|
|
||||||
|
def slotDoAnotherCommand(self):
|
||||||
|
text, okPressed = QInputDialog.getText(self, "Get Command Name", "Another command name:", QLineEdit.Normal, "")
|
||||||
|
if okPressed and text != '':
|
||||||
|
w_commandDoAction = {}
|
||||||
|
w_commandDoAction['name'] = 'command execute action'
|
||||||
|
w_commandDoAction['command name'] = text
|
||||||
|
self.addAction(w_commandDoAction)
|
||||||
|
|
||||||
|
def slotNewKeyEdit(self):
|
||||||
|
w_keyEditWnd = KeyActionEditWnd(None, self)
|
||||||
|
if w_keyEditWnd.exec() == QDialog.Accepted:
|
||||||
|
self.addAction(w_keyEditWnd.m_keyAction)
|
||||||
|
|
||||||
|
def slotNewMouseEdit(self):
|
||||||
|
w_mouseEditWnd = MouseActionEditWnd(None, self)
|
||||||
|
if w_mouseEditWnd.exec() == QDialog.Accepted:
|
||||||
|
self.addAction(w_mouseEditWnd.m_mouseAction)
|
||||||
|
|
||||||
|
def slotNewPauseEdit(self):
|
||||||
|
w_pauseEditWnd = PauseActionEditWnd(None, self)
|
||||||
|
if w_pauseEditWnd.exec() == QDialog.Accepted:
|
||||||
|
self.addAction(w_pauseEditWnd.m_pauseAction)
|
||||||
|
|
||||||
|
def slotActionUp(self):
|
||||||
|
currentIndex = self.ui.actionsListWidget.currentRow()
|
||||||
|
currentItem = self.ui.actionsListWidget.takeItem(currentIndex);
|
||||||
|
self.ui.actionsListWidget.insertItem(currentIndex - 1, currentItem);
|
||||||
|
self.ui.actionsListWidget.setCurrentRow(currentIndex - 1);
|
||||||
|
|
||||||
|
def slotActionDown(self):
|
||||||
|
currentIndex = self.ui.actionsListWidget.currentRow();
|
||||||
|
currentItem = self.ui.actionsListWidget.takeItem(currentIndex);
|
||||||
|
self.ui.actionsListWidget.insertItem(currentIndex + 1, currentItem);
|
||||||
|
self.ui.actionsListWidget.setCurrentRow(currentIndex + 1);
|
||||||
|
|
||||||
|
def slotActionEdit(self):
|
||||||
|
w_listItems = self.ui.actionsListWidget.selectedItems()
|
||||||
|
if not w_listItems: return
|
||||||
|
|
||||||
|
w_action = {}
|
||||||
|
|
||||||
|
for w_item in w_listItems:
|
||||||
|
w_jsonAction = w_item.data(Qt.UserRole)
|
||||||
|
w_action = json.loads(w_jsonAction)
|
||||||
|
break
|
||||||
|
|
||||||
|
if w_action['name'] == 'key action':
|
||||||
|
w_keyEditWnd = KeyActionEditWnd(w_action, self)
|
||||||
|
if w_keyEditWnd.exec() == QDialog.Accepted:
|
||||||
|
w_jsonAction = json.dumps(w_keyEditWnd.m_keyAction)
|
||||||
|
elif w_action['name'] == 'mouse click action' \
|
||||||
|
or w_action['name'] == 'mouse move action'\
|
||||||
|
or w_action['name'] == 'mouse scroll action':
|
||||||
|
w_mouseEditWnd = MouseActionEditWnd(w_action, self)
|
||||||
|
if w_mouseEditWnd.exec() == QDialog.Accepted:
|
||||||
|
w_jsonAction = json.dumps(w_mouseEditWnd.m_mouseAction)
|
||||||
|
elif w_action['name'] == 'pause action':
|
||||||
|
w_pauseEditWnd = PauseActionEditWnd(w_action, self)
|
||||||
|
if w_pauseEditWnd.exec() == QDialog.Accepted:
|
||||||
|
w_jsonAction = json.dumps(w_pauseEditWnd.m_pauseAction)
|
||||||
|
elif w_action['name'] == 'command stop action' \
|
||||||
|
or w_action['name'] == 'command execute action':
|
||||||
|
text, okPressed = QInputDialog.getText(self, "Get Command Name", "Another command name:", QLineEdit.Normal,
|
||||||
|
w_action['command name'])
|
||||||
|
if okPressed and text != '':
|
||||||
|
w_action['command name'] = text
|
||||||
|
w_jsonAction = json.dumps(w_action)
|
||||||
|
|
||||||
|
w_item.setText(w_jsonAction)
|
||||||
|
w_item.setData(Qt.UserRole, w_jsonAction)
|
||||||
|
|
||||||
|
def slotDelete(self):
|
||||||
|
w_listItems = self.ui.actionsListWidget.selectedItems()
|
||||||
|
if not w_listItems: return
|
||||||
|
for w_item in w_listItems:
|
||||||
|
self.ui.actionsListWidget.takeItem(self.ui.actionsListWidget.row(w_item))
|
||||||
|
|
||||||
|
def saveCommand(self):
|
||||||
|
w_actionCnt = self.ui.actionsListWidget.count()
|
||||||
|
self.m_command['name'] = self.ui.say.text()
|
||||||
|
w_actions = []
|
||||||
|
for w_idx in range(w_actionCnt):
|
||||||
|
w_jsonAction = self.ui.actionsListWidget.item(w_idx).data(Qt.UserRole)
|
||||||
|
w_action = json.loads(w_jsonAction)
|
||||||
|
w_actions.append(w_action)
|
||||||
|
self.m_command['actions'] = w_actions
|
||||||
|
self.m_command['async'] = self.ui.asyncChk.isChecked()
|
||||||
|
if self.ui.oneExe.isChecked():
|
||||||
|
self.m_command['repeat'] = 1
|
||||||
|
elif self.ui.continueExe.isChecked():
|
||||||
|
self.m_command['repeat'] = -1
|
||||||
|
elif self.ui.repeatExe.isChecked():
|
||||||
|
self.m_command['repeat'] = self.ui.repeatCnt.value()
|
||||||
|
|
||||||
|
def slotOK(self):
|
||||||
|
self.saveCommand()
|
||||||
|
super().accept()
|
||||||
|
|
||||||
|
def slotCancel(self):
|
||||||
|
super().reject()
|
||||||
|
|
|
@ -0,0 +1,390 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>CommandEditDialog</class>
|
||||||
|
<widget class="QDialog" name="CommandEditDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>927</width>
|
||||||
|
<height>419</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>10</pointsize>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Command Edit Dialog</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<property name="leftMargin">
|
||||||
|
<number>25</number>
|
||||||
|
</property>
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>20</number>
|
||||||
|
</property>
|
||||||
|
<property name="rightMargin">
|
||||||
|
<number>20</number>
|
||||||
|
</property>
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>15</number>
|
||||||
|
</property>
|
||||||
|
<item row="4" column="0" colspan="2">
|
||||||
|
<spacer name="verticalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0" colspan="2">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>When this command excutes, do the following sequence:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<spacer name="verticalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0" colspan="8">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>When I say :</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="say"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="0">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_9">
|
||||||
|
<item>
|
||||||
|
<widget class="QRadioButton" name="oneExe">
|
||||||
|
<property name="text">
|
||||||
|
<string>This command executes once</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoExclusive">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_4">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="7" column="0">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_10">
|
||||||
|
<item>
|
||||||
|
<widget class="QRadioButton" name="continueExe">
|
||||||
|
<property name="text">
|
||||||
|
<string>This command repeats continuously</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoExclusive">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_5">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="8" column="0">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_11">
|
||||||
|
<item>
|
||||||
|
<widget class="QRadioButton" name="repeatExe">
|
||||||
|
<property name="text">
|
||||||
|
<string>This command repeats</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoExclusive">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSpinBox" name="repeatCnt">
|
||||||
|
<property name="minimum">
|
||||||
|
<number>2</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_3">
|
||||||
|
<property name="text">
|
||||||
|
<string>times</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_6">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0" colspan="7">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="keyBut">
|
||||||
|
<property name="text">
|
||||||
|
<string>Key Press</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoDefault">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="mouseBut">
|
||||||
|
<property name="text">
|
||||||
|
<string>Mouse</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoDefault">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="pauseBut">
|
||||||
|
<property name="text">
|
||||||
|
<string>Pause</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoDefault">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="otherBut">
|
||||||
|
<property name="text">
|
||||||
|
<string>Other</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoDefault">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QListWidget" name="actionsListWidget">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>11</pointsize>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="upBut">
|
||||||
|
<property name="text">
|
||||||
|
<string>Up</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoDefault">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="downBut">
|
||||||
|
<property name="text">
|
||||||
|
<string>Down</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoDefault">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="editBut">
|
||||||
|
<property name="text">
|
||||||
|
<string>Edit</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoDefault">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="deleteBut">
|
||||||
|
<property name="text">
|
||||||
|
<string>Delete</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoDefault">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="0" colspan="3">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="asyncChk">
|
||||||
|
<property name="text">
|
||||||
|
<string>Allow other commands to execute while this one is running</string>
|
||||||
|
</property>
|
||||||
|
<property name="checked">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_3">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="12" column="0" colspan="3">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_12">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>20</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_7">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="ok">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>130</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>OK</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoDefault">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="cancel">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>130</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Cancel</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoDefault">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
|
@ -0,0 +1,152 @@
|
||||||
|
from PyQt5.QtCore import *
|
||||||
|
from PyQt5.QtGui import *
|
||||||
|
from PyQt5.QtWidgets import *
|
||||||
|
from ui_keyactioneditwnd import Ui_KeyActionEditDialog
|
||||||
|
import json
|
||||||
|
import keyboard
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
|
||||||
|
class KeyActionEditWnd(QDialog):
|
||||||
|
def __init__(self, p_keyAction, p_parent = None):
|
||||||
|
super().__init__(p_parent)
|
||||||
|
self.ui = Ui_KeyActionEditDialog()
|
||||||
|
self.ui.setupUi(self)
|
||||||
|
|
||||||
|
self.ui.ctrlBut.clicked.connect(self.slotCtrlClicked)
|
||||||
|
self.ui.altBut.clicked.connect(self.slotAltClicked)
|
||||||
|
self.ui.shiftBut.clicked.connect(self.slotShiftClicked)
|
||||||
|
self.ui.winBut.clicked.connect(self.slotWinClicked)
|
||||||
|
|
||||||
|
self.ui.ok.clicked.connect(self.slotOK)
|
||||||
|
self.ui.cancel.clicked.connect(self.slotCancel)
|
||||||
|
|
||||||
|
self.m_ctrlState = 0
|
||||||
|
self.m_altState = 0
|
||||||
|
self.m_shiftState = 0
|
||||||
|
self.m_winState = 0
|
||||||
|
|
||||||
|
self.m_keyAction = {}
|
||||||
|
self.ui.press_releaseKey.setChecked(True)
|
||||||
|
|
||||||
|
t = threading.Thread(target=self.keyInput)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
if p_keyAction == None:
|
||||||
|
return
|
||||||
|
|
||||||
|
w_hotKey = p_keyAction['key']
|
||||||
|
w_keys = w_hotKey.split('+')
|
||||||
|
|
||||||
|
for w_key in w_keys:
|
||||||
|
if w_key == 'left ctrl':
|
||||||
|
self.m_ctrlState = 1
|
||||||
|
elif w_key == 'right ctrl':
|
||||||
|
self.m_ctrlState = 2
|
||||||
|
elif w_key == 'left alt':
|
||||||
|
self.m_altState = 1
|
||||||
|
elif w_key == 'right alt':
|
||||||
|
self.m_altState = 2
|
||||||
|
elif w_key == 'left shift':
|
||||||
|
self.m_shiftState = 1
|
||||||
|
elif w_key == 'right shift':
|
||||||
|
self.m_shiftState = 2
|
||||||
|
elif w_key == 'left windows':
|
||||||
|
self.m_winState = 1
|
||||||
|
elif w_key == 'right windows':
|
||||||
|
self.m_winState = 2
|
||||||
|
else:
|
||||||
|
self.ui.keyEdit.setText(w_key)
|
||||||
|
|
||||||
|
w_stateText = ['Ctrl', 'Left Ctrl', 'Right Ctrl']
|
||||||
|
self.ui.ctrlBut.setText(w_stateText[self.m_ctrlState])
|
||||||
|
w_stateText = ['Alt', 'Left Alt', 'Right Alt']
|
||||||
|
self.ui.altBut.setText(w_stateText[self.m_altState])
|
||||||
|
w_stateText = ['Shift', 'Left Shift', 'Right Shift']
|
||||||
|
self.ui.shiftBut.setText(w_stateText[self.m_shiftState])
|
||||||
|
w_stateText = ['Win', 'Left Win', 'Right Win']
|
||||||
|
self.ui.winBut.setText(w_stateText[self.m_winState])
|
||||||
|
|
||||||
|
if p_keyAction['type'] == 10:
|
||||||
|
self.ui.press_releaseKey.setChecked(True)
|
||||||
|
elif p_keyAction['type'] == 0:
|
||||||
|
self.ui.releaseKey.setChecked(True)
|
||||||
|
elif p_keyAction['type'] == 1:
|
||||||
|
self.ui.pressKey.setChecked(True)
|
||||||
|
|
||||||
|
def slotOK(self):
|
||||||
|
w_ctrlStateText = ['', 'left ctrl', 'right ctrl']
|
||||||
|
w_altStateText = ['', 'left alt', 'right alt']
|
||||||
|
w_shiftStateText = ['', 'left shift', 'right shift']
|
||||||
|
w_winStateText = ['', 'left windows', 'right windows']
|
||||||
|
|
||||||
|
w_hotKey = ''
|
||||||
|
if self.m_ctrlState != 0:
|
||||||
|
w_hotKey = w_ctrlStateText[self.m_ctrlState]
|
||||||
|
|
||||||
|
if self.m_altState != 0:
|
||||||
|
if w_hotKey != '':
|
||||||
|
w_hotKey = w_hotKey + '+'
|
||||||
|
w_hotKey = w_hotKey + w_altStateText[self.m_altState]
|
||||||
|
|
||||||
|
if self.m_shiftState != 0:
|
||||||
|
if w_hotKey != '':
|
||||||
|
w_hotKey = w_hotKey + '+'
|
||||||
|
w_hotKey = w_hotKey + w_shiftStateText[self.m_shiftState]
|
||||||
|
|
||||||
|
if self.m_winState != 0:
|
||||||
|
if w_hotKey != '':
|
||||||
|
w_hotKey = w_hotKey + '+'
|
||||||
|
w_hotKey = w_hotKey + w_winStateText[self.m_winState]
|
||||||
|
|
||||||
|
if self.ui.keyEdit.text() != '':
|
||||||
|
if w_hotKey != '':
|
||||||
|
w_hotKey = w_hotKey + '+'
|
||||||
|
w_hotKey = w_hotKey + self.ui.keyEdit.text()
|
||||||
|
|
||||||
|
if w_hotKey == '':
|
||||||
|
return
|
||||||
|
|
||||||
|
self.m_keyAction = {}
|
||||||
|
self.m_keyAction['name'] = 'key action'
|
||||||
|
self.m_keyAction['key'] = w_hotKey
|
||||||
|
if self.ui.press_releaseKey.isChecked():
|
||||||
|
self.m_keyAction['type'] = 10
|
||||||
|
elif self.ui.pressKey.isChecked():
|
||||||
|
self.m_keyAction['type'] = 1
|
||||||
|
elif self.ui.releaseKey.isChecked():
|
||||||
|
self.m_keyAction['type'] = 0
|
||||||
|
|
||||||
|
return super().accept()
|
||||||
|
|
||||||
|
def slotCancel(self):
|
||||||
|
return super().reject()
|
||||||
|
|
||||||
|
def slotCtrlClicked(self):
|
||||||
|
self.m_ctrlState = (self.m_ctrlState + 1) % 3
|
||||||
|
w_stateText = ['Ctrl', 'Left Ctrl', 'Right Ctrl']
|
||||||
|
self.ui.ctrlBut.setText(w_stateText[self.m_ctrlState])
|
||||||
|
|
||||||
|
def slotAltClicked(self):
|
||||||
|
self.m_altState = (self.m_altState + 1) % 3
|
||||||
|
w_stateText = ['Alt', 'Left Alt', 'Right Alt']
|
||||||
|
self.ui.altBut.setText(w_stateText[self.m_altState])
|
||||||
|
|
||||||
|
def slotShiftClicked(self):
|
||||||
|
self.m_shiftState = (self.m_shiftState + 1) % 3
|
||||||
|
w_stateText = ['Shift', 'Left Shift', 'Right Shift']
|
||||||
|
self.ui.shiftBut.setText(w_stateText[self.m_shiftState])
|
||||||
|
|
||||||
|
def slotWinClicked(self):
|
||||||
|
self.m_winState = (self.m_winState + 1) % 3
|
||||||
|
w_stateText = ['Win', 'Left Win', 'Right Win']
|
||||||
|
self.ui.winBut.setText(w_stateText[self.m_winState])
|
||||||
|
|
||||||
|
def keyInput(self):
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
self.ui.keyEdit.setText(keyboard.read_hotkey(False))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
|
@ -0,0 +1,177 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>KeyActionEditDialog</class>
|
||||||
|
<widget class="QDialog" name="KeyActionEditDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>471</width>
|
||||||
|
<height>164</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Key Action Dialog</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Key (Combination):</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<spacer name="verticalSpacer_3">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="0">
|
||||||
|
<spacer name="verticalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>20</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QRadioButton" name="press_releaseKey">
|
||||||
|
<property name="text">
|
||||||
|
<string>Press and Release Key(s)</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QRadioButton" name="pressKey">
|
||||||
|
<property name="text">
|
||||||
|
<string>Press Key(s)</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QRadioButton" name="releaseKey">
|
||||||
|
<property name="text">
|
||||||
|
<string>Release Key(s)</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="0">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="ok">
|
||||||
|
<property name="text">
|
||||||
|
<string>OK</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoDefault">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="cancel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Cancel</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoDefault">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="ctrlBut">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<weight>50</weight>
|
||||||
|
<bold>false</bold>
|
||||||
|
<strikeout>false</strikeout>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Ctrl</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoDefault">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="altBut">
|
||||||
|
<property name="text">
|
||||||
|
<string>Alt</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoDefault">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="shiftBut">
|
||||||
|
<property name="text">
|
||||||
|
<string>Shift</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoDefault">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="winBut">
|
||||||
|
<property name="text">
|
||||||
|
<string>Win</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoDefault">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="keyEdit">
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,13 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import keyboard
|
||||||
|
import fileinput
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def print_event_json(event):
|
||||||
|
print(event.to_json(ensure_ascii=sys.stdout.encoding != 'utf-8'))
|
||||||
|
sys.stdout.flush()
|
||||||
|
keyboard.hook(print_event_json)
|
||||||
|
|
||||||
|
parse_event_json = lambda line: keyboard.KeyboardEvent(**json.loads(line))
|
||||||
|
keyboard.play(parse_event_json(line) for line in fileinput.input())
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,442 @@
|
||||||
|
import ctypes
|
||||||
|
import ctypes.util
|
||||||
|
import Quartz
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
from AppKit import NSEvent
|
||||||
|
from ._keyboard_event import KeyboardEvent, KEY_DOWN, KEY_UP
|
||||||
|
from ._canonical_names import normalize_name
|
||||||
|
|
||||||
|
try: # Python 2/3 compatibility
|
||||||
|
unichr
|
||||||
|
except NameError:
|
||||||
|
unichr = chr
|
||||||
|
|
||||||
|
Carbon = ctypes.cdll.LoadLibrary(ctypes.util.find_library('Carbon'))
|
||||||
|
|
||||||
|
class KeyMap(object):
|
||||||
|
non_layout_keys = dict((vk, normalize_name(name)) for vk, name in {
|
||||||
|
# Layout specific keys from https://stackoverflow.com/a/16125341/252218
|
||||||
|
# Unfortunately no source for layout-independent keys was found.
|
||||||
|
0x24: 'return',
|
||||||
|
0x30: 'tab',
|
||||||
|
0x31: 'space',
|
||||||
|
0x33: 'delete',
|
||||||
|
0x35: 'escape',
|
||||||
|
0x37: 'command',
|
||||||
|
0x38: 'shift',
|
||||||
|
0x39: 'capslock',
|
||||||
|
0x3a: 'option',
|
||||||
|
0x3b: 'control',
|
||||||
|
0x3c: 'right shift',
|
||||||
|
0x3d: 'right option',
|
||||||
|
0x3e: 'right control',
|
||||||
|
0x3f: 'function',
|
||||||
|
0x40: 'f17',
|
||||||
|
0x48: 'volume up',
|
||||||
|
0x49: 'volume down',
|
||||||
|
0x4a: 'mute',
|
||||||
|
0x4f: 'f18',
|
||||||
|
0x50: 'f19',
|
||||||
|
0x5a: 'f20',
|
||||||
|
0x60: 'f5',
|
||||||
|
0x61: 'f6',
|
||||||
|
0x62: 'f7',
|
||||||
|
0x63: 'f3',
|
||||||
|
0x64: 'f8',
|
||||||
|
0x65: 'f9',
|
||||||
|
0x67: 'f11',
|
||||||
|
0x69: 'f13',
|
||||||
|
0x6a: 'f16',
|
||||||
|
0x6b: 'f14',
|
||||||
|
0x6d: 'f10',
|
||||||
|
0x6f: 'f12',
|
||||||
|
0x71: 'f15',
|
||||||
|
0x72: 'help',
|
||||||
|
0x73: 'home',
|
||||||
|
0x74: 'page up',
|
||||||
|
0x75: 'forward delete',
|
||||||
|
0x76: 'f4',
|
||||||
|
0x77: 'end',
|
||||||
|
0x78: 'f2',
|
||||||
|
0x79: 'page down',
|
||||||
|
0x7a: 'f1',
|
||||||
|
0x7b: 'left',
|
||||||
|
0x7c: 'right',
|
||||||
|
0x7d: 'down',
|
||||||
|
0x7e: 'up',
|
||||||
|
}.items())
|
||||||
|
layout_specific_keys = {}
|
||||||
|
def __init__(self):
|
||||||
|
# Virtual key codes are usually the same for any given key, unless you have a different
|
||||||
|
# keyboard layout. The only way I've found to determine the layout relies on (supposedly
|
||||||
|
# deprecated) Carbon APIs. If there's a more modern way to do this, please update this
|
||||||
|
# section.
|
||||||
|
|
||||||
|
# Set up data types and exported values:
|
||||||
|
|
||||||
|
CFTypeRef = ctypes.c_void_p
|
||||||
|
CFDataRef = ctypes.c_void_p
|
||||||
|
CFIndex = ctypes.c_uint64
|
||||||
|
OptionBits = ctypes.c_uint32
|
||||||
|
UniCharCount = ctypes.c_uint8
|
||||||
|
UniChar = ctypes.c_uint16
|
||||||
|
UniChar4 = UniChar * 4
|
||||||
|
|
||||||
|
class CFRange(ctypes.Structure):
|
||||||
|
_fields_ = [('loc', CFIndex),
|
||||||
|
('len', CFIndex)]
|
||||||
|
|
||||||
|
kTISPropertyUnicodeKeyLayoutData = ctypes.c_void_p.in_dll(Carbon, 'kTISPropertyUnicodeKeyLayoutData')
|
||||||
|
shiftKey = 0x0200
|
||||||
|
alphaKey = 0x0400
|
||||||
|
optionKey = 0x0800
|
||||||
|
controlKey = 0x1000
|
||||||
|
kUCKeyActionDisplay = 3
|
||||||
|
kUCKeyTranslateNoDeadKeysBit = 0
|
||||||
|
|
||||||
|
# Set up function calls:
|
||||||
|
Carbon.CFDataGetBytes.argtypes = [CFDataRef] #, CFRange, UInt8
|
||||||
|
Carbon.CFDataGetBytes.restype = None
|
||||||
|
Carbon.CFDataGetLength.argtypes = [CFDataRef]
|
||||||
|
Carbon.CFDataGetLength.restype = CFIndex
|
||||||
|
Carbon.CFRelease.argtypes = [CFTypeRef]
|
||||||
|
Carbon.CFRelease.restype = None
|
||||||
|
Carbon.LMGetKbdType.argtypes = []
|
||||||
|
Carbon.LMGetKbdType.restype = ctypes.c_uint32
|
||||||
|
Carbon.TISCopyCurrentKeyboardInputSource.argtypes = []
|
||||||
|
Carbon.TISCopyCurrentKeyboardInputSource.restype = ctypes.c_void_p
|
||||||
|
Carbon.TISCopyCurrentASCIICapableKeyboardLayoutInputSource.argtypes = []
|
||||||
|
Carbon.TISCopyCurrentASCIICapableKeyboardLayoutInputSource.restype = ctypes.c_void_p
|
||||||
|
Carbon.TISGetInputSourceProperty.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
||||||
|
Carbon.TISGetInputSourceProperty.restype = ctypes.c_void_p
|
||||||
|
Carbon.UCKeyTranslate.argtypes = [ctypes.c_void_p,
|
||||||
|
ctypes.c_uint16,
|
||||||
|
ctypes.c_uint16,
|
||||||
|
ctypes.c_uint32,
|
||||||
|
ctypes.c_uint32,
|
||||||
|
OptionBits, # keyTranslateOptions
|
||||||
|
ctypes.POINTER(ctypes.c_uint32), # deadKeyState
|
||||||
|
UniCharCount, # maxStringLength
|
||||||
|
ctypes.POINTER(UniCharCount), # actualStringLength
|
||||||
|
UniChar4]
|
||||||
|
Carbon.UCKeyTranslate.restype = ctypes.c_uint32
|
||||||
|
|
||||||
|
# Get keyboard layout
|
||||||
|
klis = Carbon.TISCopyCurrentKeyboardInputSource()
|
||||||
|
k_layout = Carbon.TISGetInputSourceProperty(klis, kTISPropertyUnicodeKeyLayoutData)
|
||||||
|
if k_layout is None:
|
||||||
|
klis = Carbon.TISCopyCurrentASCIICapableKeyboardLayoutInputSource()
|
||||||
|
k_layout = Carbon.TISGetInputSourceProperty(klis, kTISPropertyUnicodeKeyLayoutData)
|
||||||
|
k_layout_size = Carbon.CFDataGetLength(k_layout)
|
||||||
|
k_layout_buffer = ctypes.create_string_buffer(k_layout_size) # TODO - Verify this works instead of initializing with empty string
|
||||||
|
Carbon.CFDataGetBytes(k_layout, CFRange(0, k_layout_size), ctypes.byref(k_layout_buffer))
|
||||||
|
|
||||||
|
# Generate character representations of key codes
|
||||||
|
for key_code in range(0, 128):
|
||||||
|
# TODO - Possibly add alt modifier to key map
|
||||||
|
non_shifted_char = UniChar4()
|
||||||
|
shifted_char = UniChar4()
|
||||||
|
keys_down = ctypes.c_uint32()
|
||||||
|
char_count = UniCharCount()
|
||||||
|
|
||||||
|
retval = Carbon.UCKeyTranslate(k_layout_buffer,
|
||||||
|
key_code,
|
||||||
|
kUCKeyActionDisplay,
|
||||||
|
0, # No modifier
|
||||||
|
Carbon.LMGetKbdType(),
|
||||||
|
kUCKeyTranslateNoDeadKeysBit,
|
||||||
|
ctypes.byref(keys_down),
|
||||||
|
4,
|
||||||
|
ctypes.byref(char_count),
|
||||||
|
non_shifted_char)
|
||||||
|
|
||||||
|
non_shifted_key = u''.join(unichr(non_shifted_char[i]) for i in range(char_count.value))
|
||||||
|
|
||||||
|
retval = Carbon.UCKeyTranslate(k_layout_buffer,
|
||||||
|
key_code,
|
||||||
|
kUCKeyActionDisplay,
|
||||||
|
shiftKey >> 8, # Shift
|
||||||
|
Carbon.LMGetKbdType(),
|
||||||
|
kUCKeyTranslateNoDeadKeysBit,
|
||||||
|
ctypes.byref(keys_down),
|
||||||
|
4,
|
||||||
|
ctypes.byref(char_count),
|
||||||
|
shifted_char)
|
||||||
|
|
||||||
|
shifted_key = u''.join(unichr(shifted_char[i]) for i in range(char_count.value))
|
||||||
|
|
||||||
|
self.layout_specific_keys[key_code] = (non_shifted_key, shifted_key)
|
||||||
|
# Cleanup
|
||||||
|
Carbon.CFRelease(klis)
|
||||||
|
|
||||||
|
def character_to_vk(self, character):
|
||||||
|
""" Returns a tuple of (scan_code, modifiers) where ``scan_code`` is a numeric scan code
|
||||||
|
and ``modifiers`` is an array of string modifier names (like 'shift') """
|
||||||
|
for vk in self.non_layout_keys:
|
||||||
|
if self.non_layout_keys[vk] == character.lower():
|
||||||
|
return (vk, [])
|
||||||
|
for vk in self.layout_specific_keys:
|
||||||
|
if self.layout_specific_keys[vk][0] == character:
|
||||||
|
return (vk, [])
|
||||||
|
elif self.layout_specific_keys[vk][1] == character:
|
||||||
|
return (vk, ['shift'])
|
||||||
|
raise ValueError("Unrecognized character: {}".format(character))
|
||||||
|
|
||||||
|
def vk_to_character(self, vk, modifiers=[]):
|
||||||
|
""" Returns a character corresponding to the specified scan code (with given
|
||||||
|
modifiers applied) """
|
||||||
|
if vk in self.non_layout_keys:
|
||||||
|
# Not a character
|
||||||
|
return self.non_layout_keys[vk]
|
||||||
|
elif vk in self.layout_specific_keys:
|
||||||
|
if 'shift' in modifiers:
|
||||||
|
return self.layout_specific_keys[vk][1]
|
||||||
|
return self.layout_specific_keys[vk][0]
|
||||||
|
else:
|
||||||
|
# Invalid vk
|
||||||
|
raise ValueError("Invalid scan code: {}".format(vk))
|
||||||
|
|
||||||
|
|
||||||
|
class KeyController(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.key_map = KeyMap()
|
||||||
|
self.current_modifiers = {
|
||||||
|
"shift": False,
|
||||||
|
"caps": False,
|
||||||
|
"alt": False,
|
||||||
|
"ctrl": False,
|
||||||
|
"cmd": False,
|
||||||
|
}
|
||||||
|
self.media_keys = {
|
||||||
|
'KEYTYPE_SOUND_UP': 0,
|
||||||
|
'KEYTYPE_SOUND_DOWN': 1,
|
||||||
|
'KEYTYPE_BRIGHTNESS_UP': 2,
|
||||||
|
'KEYTYPE_BRIGHTNESS_DOWN': 3,
|
||||||
|
'KEYTYPE_CAPS_LOCK': 4,
|
||||||
|
'KEYTYPE_HELP': 5,
|
||||||
|
'POWER_KEY': 6,
|
||||||
|
'KEYTYPE_MUTE': 7,
|
||||||
|
'UP_ARROW_KEY': 8,
|
||||||
|
'DOWN_ARROW_KEY': 9,
|
||||||
|
'KEYTYPE_NUM_LOCK': 10,
|
||||||
|
'KEYTYPE_CONTRAST_UP': 11,
|
||||||
|
'KEYTYPE_CONTRAST_DOWN': 12,
|
||||||
|
'KEYTYPE_LAUNCH_PANEL': 13,
|
||||||
|
'KEYTYPE_EJECT': 14,
|
||||||
|
'KEYTYPE_VIDMIRROR': 15,
|
||||||
|
'KEYTYPE_PLAY': 16,
|
||||||
|
'KEYTYPE_NEXT': 17,
|
||||||
|
'KEYTYPE_PREVIOUS': 18,
|
||||||
|
'KEYTYPE_FAST': 19,
|
||||||
|
'KEYTYPE_REWIND': 20,
|
||||||
|
'KEYTYPE_ILLUMINATION_UP': 21,
|
||||||
|
'KEYTYPE_ILLUMINATION_DOWN': 22,
|
||||||
|
'KEYTYPE_ILLUMINATION_TOGGLE': 23
|
||||||
|
}
|
||||||
|
|
||||||
|
def press(self, key_code):
|
||||||
|
""" Sends a 'down' event for the specified scan code """
|
||||||
|
if key_code >= 128:
|
||||||
|
# Media key
|
||||||
|
ev = NSEvent.otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_(
|
||||||
|
14, # type
|
||||||
|
(0, 0), # location
|
||||||
|
0xa00, # flags
|
||||||
|
0, # timestamp
|
||||||
|
0, # window
|
||||||
|
0, # ctx
|
||||||
|
8, # subtype
|
||||||
|
((key_code-128) << 16) | (0xa << 8), # data1
|
||||||
|
-1 # data2
|
||||||
|
)
|
||||||
|
Quartz.CGEventPost(0, ev.CGEvent())
|
||||||
|
else:
|
||||||
|
# Regular key
|
||||||
|
# Apply modifiers if necessary
|
||||||
|
event_flags = 0
|
||||||
|
if self.current_modifiers["shift"]:
|
||||||
|
event_flags += Quartz.kCGEventFlagMaskShift
|
||||||
|
if self.current_modifiers["caps"]:
|
||||||
|
event_flags += Quartz.kCGEventFlagMaskAlphaShift
|
||||||
|
if self.current_modifiers["alt"]:
|
||||||
|
event_flags += Quartz.kCGEventFlagMaskAlternate
|
||||||
|
if self.current_modifiers["ctrl"]:
|
||||||
|
event_flags += Quartz.kCGEventFlagMaskControl
|
||||||
|
if self.current_modifiers["cmd"]:
|
||||||
|
event_flags += Quartz.kCGEventFlagMaskCommand
|
||||||
|
|
||||||
|
# Update modifiers if necessary
|
||||||
|
if key_code == 0x37: # cmd
|
||||||
|
self.current_modifiers["cmd"] = True
|
||||||
|
elif key_code == 0x38 or key_code == 0x3C: # shift or right shift
|
||||||
|
self.current_modifiers["shift"] = True
|
||||||
|
elif key_code == 0x39: # caps lock
|
||||||
|
self.current_modifiers["caps"] = True
|
||||||
|
elif key_code == 0x3A: # alt
|
||||||
|
self.current_modifiers["alt"] = True
|
||||||
|
elif key_code == 0x3B: # ctrl
|
||||||
|
self.current_modifiers["ctrl"] = True
|
||||||
|
event = Quartz.CGEventCreateKeyboardEvent(None, key_code, True)
|
||||||
|
Quartz.CGEventSetFlags(event, event_flags)
|
||||||
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, event)
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
def release(self, key_code):
|
||||||
|
""" Sends an 'up' event for the specified scan code """
|
||||||
|
if key_code >= 128:
|
||||||
|
# Media key
|
||||||
|
ev = NSEvent.otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_(
|
||||||
|
14, # type
|
||||||
|
(0, 0), # location
|
||||||
|
0xb00, # flags
|
||||||
|
0, # timestamp
|
||||||
|
0, # window
|
||||||
|
0, # ctx
|
||||||
|
8, # subtype
|
||||||
|
((key_code-128) << 16) | (0xb << 8), # data1
|
||||||
|
-1 # data2
|
||||||
|
)
|
||||||
|
Quartz.CGEventPost(0, ev.CGEvent())
|
||||||
|
else:
|
||||||
|
# Regular key
|
||||||
|
# Update modifiers if necessary
|
||||||
|
if key_code == 0x37: # cmd
|
||||||
|
self.current_modifiers["cmd"] = False
|
||||||
|
elif key_code == 0x38 or key_code == 0x3C: # shift or right shift
|
||||||
|
self.current_modifiers["shift"] = False
|
||||||
|
elif key_code == 0x39: # caps lock
|
||||||
|
self.current_modifiers["caps"] = False
|
||||||
|
elif key_code == 0x3A: # alt
|
||||||
|
self.current_modifiers["alt"] = False
|
||||||
|
elif key_code == 0x3B: # ctrl
|
||||||
|
self.current_modifiers["ctrl"] = False
|
||||||
|
|
||||||
|
# Apply modifiers if necessary
|
||||||
|
event_flags = 0
|
||||||
|
if self.current_modifiers["shift"]:
|
||||||
|
event_flags += Quartz.kCGEventFlagMaskShift
|
||||||
|
if self.current_modifiers["caps"]:
|
||||||
|
event_flags += Quartz.kCGEventFlagMaskAlphaShift
|
||||||
|
if self.current_modifiers["alt"]:
|
||||||
|
event_flags += Quartz.kCGEventFlagMaskAlternate
|
||||||
|
if self.current_modifiers["ctrl"]:
|
||||||
|
event_flags += Quartz.kCGEventFlagMaskControl
|
||||||
|
if self.current_modifiers["cmd"]:
|
||||||
|
event_flags += Quartz.kCGEventFlagMaskCommand
|
||||||
|
event = Quartz.CGEventCreateKeyboardEvent(None, key_code, False)
|
||||||
|
Quartz.CGEventSetFlags(event, event_flags)
|
||||||
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, event)
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
def map_char(self, character):
|
||||||
|
if character in self.media_keys:
|
||||||
|
return (128+self.media_keys[character],[])
|
||||||
|
else:
|
||||||
|
return self.key_map.character_to_vk(character)
|
||||||
|
def map_scan_code(self, scan_code):
|
||||||
|
if scan_code >= 128:
|
||||||
|
character = [k for k, v in enumerate(self.media_keys) if v == scan_code-128]
|
||||||
|
if len(character):
|
||||||
|
return character[0]
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return self.key_map.vk_to_character(scan_code)
|
||||||
|
|
||||||
|
class KeyEventListener(object):
|
||||||
|
def __init__(self, callback, blocking=False):
|
||||||
|
self.blocking = blocking
|
||||||
|
self.callback = callback
|
||||||
|
self.listening = True
|
||||||
|
self.tap = None
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
""" Creates a listener and loops while waiting for an event. Intended to run as
|
||||||
|
a background thread. """
|
||||||
|
self.tap = Quartz.CGEventTapCreate(
|
||||||
|
Quartz.kCGSessionEventTap,
|
||||||
|
Quartz.kCGHeadInsertEventTap,
|
||||||
|
Quartz.kCGEventTapOptionDefault,
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventKeyDown) |
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventKeyUp) |
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventFlagsChanged),
|
||||||
|
self.handler,
|
||||||
|
None)
|
||||||
|
loopsource = Quartz.CFMachPortCreateRunLoopSource(None, self.tap, 0)
|
||||||
|
loop = Quartz.CFRunLoopGetCurrent()
|
||||||
|
Quartz.CFRunLoopAddSource(loop, loopsource, Quartz.kCFRunLoopDefaultMode)
|
||||||
|
Quartz.CGEventTapEnable(self.tap, True)
|
||||||
|
|
||||||
|
while self.listening:
|
||||||
|
Quartz.CFRunLoopRunInMode(Quartz.kCFRunLoopDefaultMode, 5, False)
|
||||||
|
|
||||||
|
def handler(self, proxy, e_type, event, refcon):
|
||||||
|
scan_code = Quartz.CGEventGetIntegerValueField(event, Quartz.kCGKeyboardEventKeycode)
|
||||||
|
key_name = name_from_scancode(scan_code)
|
||||||
|
flags = Quartz.CGEventGetFlags(event)
|
||||||
|
event_type = ""
|
||||||
|
is_keypad = (flags & Quartz.kCGEventFlagMaskNumericPad)
|
||||||
|
if e_type == Quartz.kCGEventKeyDown:
|
||||||
|
event_type = "down"
|
||||||
|
elif e_type == Quartz.kCGEventKeyUp:
|
||||||
|
event_type = "up"
|
||||||
|
elif e_type == Quartz.kCGEventFlagsChanged:
|
||||||
|
if key_name.endswith("shift") and (flags & Quartz.kCGEventFlagMaskShift):
|
||||||
|
event_type = "down"
|
||||||
|
elif key_name == "caps lock" and (flags & Quartz.kCGEventFlagMaskAlphaShift):
|
||||||
|
event_type = "down"
|
||||||
|
elif (key_name.endswith("option") or key_name.endswith("alt")) and (flags & Quartz.kCGEventFlagMaskAlternate):
|
||||||
|
event_type = "down"
|
||||||
|
elif key_name == "ctrl" and (flags & Quartz.kCGEventFlagMaskControl):
|
||||||
|
event_type = "down"
|
||||||
|
elif key_name == "command" and (flags & Quartz.kCGEventFlagMaskCommand):
|
||||||
|
event_type = "down"
|
||||||
|
else:
|
||||||
|
event_type = "up"
|
||||||
|
|
||||||
|
if self.blocking:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.callback(KeyboardEvent(event_type, scan_code, name=key_name, is_keypad=is_keypad))
|
||||||
|
return event
|
||||||
|
|
||||||
|
key_controller = KeyController()
|
||||||
|
|
||||||
|
""" Exported functions below """
|
||||||
|
|
||||||
|
def init():
|
||||||
|
key_controller = KeyController()
|
||||||
|
|
||||||
|
def press(scan_code):
|
||||||
|
""" Sends a 'down' event for the specified scan code """
|
||||||
|
key_controller.press(scan_code)
|
||||||
|
|
||||||
|
def release(scan_code):
|
||||||
|
""" Sends an 'up' event for the specified scan code """
|
||||||
|
key_controller.release(scan_code)
|
||||||
|
|
||||||
|
def map_name(name):
|
||||||
|
""" Returns a tuple of (scan_code, modifiers) where ``scan_code`` is a numeric scan code
|
||||||
|
and ``modifiers`` is an array of string modifier names (like 'shift') """
|
||||||
|
yield key_controller.map_char(name)
|
||||||
|
|
||||||
|
def name_from_scancode(scan_code):
|
||||||
|
""" Returns the name or character associated with the specified key code """
|
||||||
|
return key_controller.map_scan_code(scan_code)
|
||||||
|
|
||||||
|
def listen(callback):
|
||||||
|
if not os.geteuid() == 0:
|
||||||
|
raise OSError("Error 13 - Must be run as administrator")
|
||||||
|
KeyEventListener(callback).run()
|
||||||
|
|
||||||
|
def type_unicode(character):
|
||||||
|
OUTPUT_SOURCE = Quartz.CGEventSourceCreate(Quartz.kCGEventSourceStateHIDSystemState)
|
||||||
|
# Key down
|
||||||
|
event = Quartz.CGEventCreateKeyboardEvent(OUTPUT_SOURCE, 0, True)
|
||||||
|
Quartz.CGEventKeyboardSetUnicodeString(event, len(character.encode('utf-16-le')) // 2, character)
|
||||||
|
Quartz.CGEventPost(Quartz.kCGSessionEventTap, event)
|
||||||
|
# Key up
|
||||||
|
event = Quartz.CGEventCreateKeyboardEvent(OUTPUT_SOURCE, 0, False)
|
||||||
|
Quartz.CGEventKeyboardSetUnicodeString(event, len(character.encode('utf-16-le')) // 2, character)
|
||||||
|
Quartz.CGEventPost(Quartz.kCGSessionEventTap, event)
|
|
@ -0,0 +1,173 @@
|
||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
import threading
|
||||||
|
import Quartz
|
||||||
|
from ._mouse_event import ButtonEvent, WheelEvent, MoveEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN
|
||||||
|
|
||||||
|
_button_mapping = {
|
||||||
|
LEFT: (Quartz.kCGMouseButtonLeft, Quartz.kCGEventLeftMouseDown, Quartz.kCGEventLeftMouseUp, Quartz.kCGEventLeftMouseDragged),
|
||||||
|
RIGHT: (Quartz.kCGMouseButtonRight, Quartz.kCGEventRightMouseDown, Quartz.kCGEventRightMouseUp, Quartz.kCGEventRightMouseDragged),
|
||||||
|
MIDDLE: (Quartz.kCGMouseButtonCenter, Quartz.kCGEventOtherMouseDown, Quartz.kCGEventOtherMouseUp, Quartz.kCGEventOtherMouseDragged)
|
||||||
|
}
|
||||||
|
_button_state = {
|
||||||
|
LEFT: False,
|
||||||
|
RIGHT: False,
|
||||||
|
MIDDLE: False
|
||||||
|
}
|
||||||
|
_last_click = {
|
||||||
|
"time": None,
|
||||||
|
"button": None,
|
||||||
|
"position": None,
|
||||||
|
"click_count": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
class MouseEventListener(object):
|
||||||
|
def __init__(self, callback, blocking=False):
|
||||||
|
self.blocking = blocking
|
||||||
|
self.callback = callback
|
||||||
|
self.listening = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
""" Creates a listener and loops while waiting for an event. Intended to run as
|
||||||
|
a background thread. """
|
||||||
|
self.tap = Quartz.CGEventTapCreate(
|
||||||
|
Quartz.kCGSessionEventTap,
|
||||||
|
Quartz.kCGHeadInsertEventTap,
|
||||||
|
Quartz.kCGEventTapOptionDefault,
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseDown) |
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseUp) |
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseDown) |
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseUp) |
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseDown) |
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseUp) |
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventMouseMoved) |
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventScrollWheel),
|
||||||
|
self.handler,
|
||||||
|
None)
|
||||||
|
loopsource = Quartz.CFMachPortCreateRunLoopSource(None, self.tap, 0)
|
||||||
|
loop = Quartz.CFRunLoopGetCurrent()
|
||||||
|
Quartz.CFRunLoopAddSource(loop, loopsource, Quartz.kCFRunLoopDefaultMode)
|
||||||
|
Quartz.CGEventTapEnable(self.tap, True)
|
||||||
|
|
||||||
|
while self.listening:
|
||||||
|
Quartz.CFRunLoopRunInMode(Quartz.kCFRunLoopDefaultMode, 5, False)
|
||||||
|
|
||||||
|
def handler(self, proxy, e_type, event, refcon):
|
||||||
|
# TODO Separate event types by button/wheel/move
|
||||||
|
scan_code = Quartz.CGEventGetIntegerValueField(event, Quartz.kCGKeyboardEventKeycode)
|
||||||
|
key_name = name_from_scancode(scan_code)
|
||||||
|
flags = Quartz.CGEventGetFlags(event)
|
||||||
|
event_type = ""
|
||||||
|
is_keypad = (flags & Quartz.kCGEventFlagMaskNumericPad)
|
||||||
|
if e_type == Quartz.kCGEventKeyDown:
|
||||||
|
event_type = "down"
|
||||||
|
elif e_type == Quartz.kCGEventKeyUp:
|
||||||
|
event_type = "up"
|
||||||
|
|
||||||
|
if self.blocking:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.callback(KeyboardEvent(event_type, scan_code, name=key_name, is_keypad=is_keypad))
|
||||||
|
return event
|
||||||
|
|
||||||
|
# Exports
|
||||||
|
|
||||||
|
def init():
|
||||||
|
""" Initializes mouse state """
|
||||||
|
pass
|
||||||
|
|
||||||
|
def listen(queue):
|
||||||
|
""" Appends events to the queue (ButtonEvent, WheelEvent, and MoveEvent). """
|
||||||
|
if not os.geteuid() == 0:
|
||||||
|
raise OSError("Error 13 - Must be run as administrator")
|
||||||
|
listener = MouseEventListener(lambda e: queue.put(e) or is_allowed(e.name, e.event_type == KEY_UP))
|
||||||
|
t = threading.Thread(target=listener.run, args=())
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
def press(button=LEFT):
|
||||||
|
""" Sends a down event for the specified button, using the provided constants """
|
||||||
|
location = get_position()
|
||||||
|
button_code, button_down, _, _ = _button_mapping[button]
|
||||||
|
e = Quartz.CGEventCreateMouseEvent(
|
||||||
|
None,
|
||||||
|
button_down,
|
||||||
|
location,
|
||||||
|
button_code)
|
||||||
|
|
||||||
|
# Check if this is a double-click (same location within the last 300ms)
|
||||||
|
if _last_click["time"] is not None and datetime.datetime.now() - _last_click["time"] < datetime.timedelta(seconds=0.3) and _last_click["button"] == button and _last_click["position"] == location:
|
||||||
|
# Repeated Click
|
||||||
|
_last_click["click_count"] = min(3, _last_click["click_count"]+1)
|
||||||
|
else:
|
||||||
|
# Not a double-click - Reset last click
|
||||||
|
_last_click["click_count"] = 1
|
||||||
|
Quartz.CGEventSetIntegerValueField(
|
||||||
|
e,
|
||||||
|
Quartz.kCGMouseEventClickState,
|
||||||
|
_last_click["click_count"])
|
||||||
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, e)
|
||||||
|
_button_state[button] = True
|
||||||
|
_last_click["time"] = datetime.datetime.now()
|
||||||
|
_last_click["button"] = button
|
||||||
|
_last_click["position"] = location
|
||||||
|
|
||||||
|
def release(button=LEFT):
|
||||||
|
""" Sends an up event for the specified button, using the provided constants """
|
||||||
|
location = get_position()
|
||||||
|
button_code, _, button_up, _ = _button_mapping[button]
|
||||||
|
e = Quartz.CGEventCreateMouseEvent(
|
||||||
|
None,
|
||||||
|
button_up,
|
||||||
|
location,
|
||||||
|
button_code)
|
||||||
|
|
||||||
|
if _last_click["time"] is not None and _last_click["time"] > datetime.datetime.now() - datetime.timedelta(microseconds=300000) and _last_click["button"] == button and _last_click["position"] == location:
|
||||||
|
# Repeated Click
|
||||||
|
Quartz.CGEventSetIntegerValueField(
|
||||||
|
e,
|
||||||
|
Quartz.kCGMouseEventClickState,
|
||||||
|
_last_click["click_count"])
|
||||||
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, e)
|
||||||
|
_button_state[button] = False
|
||||||
|
|
||||||
|
def wheel(delta=1):
|
||||||
|
""" Sends a wheel event for the provided number of clicks. May be negative to reverse
|
||||||
|
direction. """
|
||||||
|
location = get_position()
|
||||||
|
e = Quartz.CGEventCreateMouseEvent(
|
||||||
|
None,
|
||||||
|
Quartz.kCGEventScrollWheel,
|
||||||
|
location,
|
||||||
|
Quartz.kCGMouseButtonLeft)
|
||||||
|
e2 = Quartz.CGEventCreateScrollWheelEvent(
|
||||||
|
None,
|
||||||
|
Quartz.kCGScrollEventUnitLine,
|
||||||
|
1,
|
||||||
|
delta)
|
||||||
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, e)
|
||||||
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, e2)
|
||||||
|
|
||||||
|
def move_to(x, y):
|
||||||
|
""" Sets the mouse's location to the specified coordinates. """
|
||||||
|
for b in _button_state:
|
||||||
|
if _button_state[b]:
|
||||||
|
e = Quartz.CGEventCreateMouseEvent(
|
||||||
|
None,
|
||||||
|
_button_mapping[b][3], # Drag Event
|
||||||
|
(x, y),
|
||||||
|
_button_mapping[b][0])
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
e = Quartz.CGEventCreateMouseEvent(
|
||||||
|
None,
|
||||||
|
Quartz.kCGEventMouseMoved,
|
||||||
|
(x, y),
|
||||||
|
Quartz.kCGMouseButtonLeft)
|
||||||
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, e)
|
||||||
|
|
||||||
|
def get_position():
|
||||||
|
""" Returns the mouse's location as a tuple of (x, y). """
|
||||||
|
e = Quartz.CGEventCreate(None)
|
||||||
|
point = Quartz.CGEventGetLocation(e)
|
||||||
|
return (point.x, point.y)
|
|
@ -0,0 +1,73 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from threading import Thread, Lock
|
||||||
|
import traceback
|
||||||
|
import functools
|
||||||
|
|
||||||
|
try:
|
||||||
|
from queue import Queue
|
||||||
|
except ImportError:
|
||||||
|
from Queue import Queue
|
||||||
|
|
||||||
|
class GenericListener(object):
|
||||||
|
lock = Lock()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.handlers = []
|
||||||
|
self.listening = False
|
||||||
|
self.queue = Queue()
|
||||||
|
|
||||||
|
def invoke_handlers(self, event):
|
||||||
|
for handler in self.handlers:
|
||||||
|
try:
|
||||||
|
if handler(event):
|
||||||
|
# Stop processing this hotkey.
|
||||||
|
return 1
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def start_if_necessary(self):
|
||||||
|
"""
|
||||||
|
Starts the listening thread if it wans't already.
|
||||||
|
"""
|
||||||
|
self.lock.acquire()
|
||||||
|
try:
|
||||||
|
if not self.listening:
|
||||||
|
self.init()
|
||||||
|
|
||||||
|
self.listening = True
|
||||||
|
self.listening_thread = Thread(target=self.listen)
|
||||||
|
self.listening_thread.daemon = True
|
||||||
|
self.listening_thread.start()
|
||||||
|
|
||||||
|
self.processing_thread = Thread(target=self.process)
|
||||||
|
self.processing_thread.daemon = True
|
||||||
|
self.processing_thread.start()
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
|
||||||
|
def pre_process_event(self, event):
|
||||||
|
raise NotImplementedError('This method should be implemented in the child class.')
|
||||||
|
|
||||||
|
def process(self):
|
||||||
|
"""
|
||||||
|
Loops over the underlying queue of events and processes them in order.
|
||||||
|
"""
|
||||||
|
assert self.queue is not None
|
||||||
|
while True:
|
||||||
|
event = self.queue.get()
|
||||||
|
if self.pre_process_event(event):
|
||||||
|
self.invoke_handlers(event)
|
||||||
|
self.queue.task_done()
|
||||||
|
|
||||||
|
def add_handler(self, handler):
|
||||||
|
"""
|
||||||
|
Adds a function to receive each event captured, starting the capturing
|
||||||
|
process if necessary.
|
||||||
|
"""
|
||||||
|
self.start_if_necessary()
|
||||||
|
self.handlers.append(handler)
|
||||||
|
|
||||||
|
def remove_handler(self, handler):
|
||||||
|
""" Removes a previously added event handler. """
|
||||||
|
while handler in self.handlers:
|
||||||
|
self.handlers.remove(handler)
|
|
@ -0,0 +1,53 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from time import time as now
|
||||||
|
import json
|
||||||
|
from ._canonical_names import canonical_names, normalize_name
|
||||||
|
|
||||||
|
try:
|
||||||
|
basestring
|
||||||
|
except NameError:
|
||||||
|
basestring = str
|
||||||
|
|
||||||
|
KEY_DOWN = 'down'
|
||||||
|
KEY_UP = 'up'
|
||||||
|
|
||||||
|
class KeyboardEvent(object):
|
||||||
|
event_type = None
|
||||||
|
scan_code = None
|
||||||
|
name = None
|
||||||
|
time = None
|
||||||
|
device = None
|
||||||
|
modifiers = None
|
||||||
|
is_keypad = None
|
||||||
|
|
||||||
|
def __init__(self, event_type, scan_code, name=None, time=None, device=None, modifiers=None, is_keypad=None):
|
||||||
|
self.event_type = event_type
|
||||||
|
self.scan_code = scan_code
|
||||||
|
self.time = now() if time is None else time
|
||||||
|
self.device = device
|
||||||
|
self.is_keypad = is_keypad
|
||||||
|
self.modifiers = modifiers
|
||||||
|
if name:
|
||||||
|
self.name = normalize_name(name)
|
||||||
|
|
||||||
|
def to_json(self, ensure_ascii=False):
|
||||||
|
attrs = dict(
|
||||||
|
(attr, getattr(self, attr)) for attr in ['event_type', 'scan_code', 'name', 'time', 'device', 'is_keypad']
|
||||||
|
if not attr.startswith('_') and getattr(self, attr) is not None
|
||||||
|
)
|
||||||
|
return json.dumps(attrs, ensure_ascii=ensure_ascii)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'KeyboardEvent({} {})'.format(self.name or 'Unknown {}'.format(self.scan_code), self.event_type)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (
|
||||||
|
isinstance(other, KeyboardEvent)
|
||||||
|
and self.event_type == other.event_type
|
||||||
|
and (
|
||||||
|
not self.scan_code or not other.scan_code or self.scan_code == other.scan_code
|
||||||
|
) and (
|
||||||
|
not self.name or not other.name or self.name == other.name
|
||||||
|
)
|
||||||
|
)
|
|
@ -0,0 +1,827 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Side effects are avoided using two techniques:
|
||||||
|
|
||||||
|
- Low level OS requests (keyboard._os_keyboard) are mocked out by rewriting
|
||||||
|
the functions at that namespace. This includes a list of dummy keys.
|
||||||
|
- Events are pumped manually by the main test class, and accepted events
|
||||||
|
are tested against expected values.
|
||||||
|
|
||||||
|
Fake user events are appended to `input_events`, passed through
|
||||||
|
keyboard,_listener.direct_callback, then, if accepted, appended to
|
||||||
|
`output_events`. Fake OS events (keyboard.press) are processed
|
||||||
|
and added to `output_events` immediately, mimicking real functionality.
|
||||||
|
"""
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import time
|
||||||
|
|
||||||
|
import keyboard
|
||||||
|
from ._keyboard_event import KeyboardEvent, KEY_DOWN, KEY_UP
|
||||||
|
|
||||||
|
dummy_keys = {
|
||||||
|
'space': [(0, [])],
|
||||||
|
|
||||||
|
'a': [(1, [])],
|
||||||
|
'b': [(2, [])],
|
||||||
|
'c': [(3, [])],
|
||||||
|
'A': [(1, ['shift']), (-1, [])],
|
||||||
|
'B': [(2, ['shift']), (-2, [])],
|
||||||
|
'C': [(3, ['shift']), (-3, [])],
|
||||||
|
|
||||||
|
'alt': [(4, [])],
|
||||||
|
'left alt': [(4, [])],
|
||||||
|
|
||||||
|
'left shift': [(5, [])],
|
||||||
|
'right shift': [(6, [])],
|
||||||
|
|
||||||
|
'left ctrl': [(7, [])],
|
||||||
|
|
||||||
|
'backspace': [(8, [])],
|
||||||
|
'caps lock': [(9, [])],
|
||||||
|
|
||||||
|
'+': [(10, [])],
|
||||||
|
',': [(11, [])],
|
||||||
|
'_': [(12, [])],
|
||||||
|
|
||||||
|
'none': [],
|
||||||
|
'duplicated': [(20, []), (20, [])],
|
||||||
|
}
|
||||||
|
|
||||||
|
def make_event(event_type, name, scan_code=None, time=0):
|
||||||
|
return KeyboardEvent(event_type=event_type, scan_code=scan_code or dummy_keys[name][0][0], name=name, time=time)
|
||||||
|
|
||||||
|
# Used when manually pumping events.
|
||||||
|
input_events = []
|
||||||
|
output_events = []
|
||||||
|
|
||||||
|
def send_instant_event(event):
|
||||||
|
if keyboard._listener.direct_callback(event):
|
||||||
|
output_events.append(event)
|
||||||
|
|
||||||
|
# Mock out side effects.
|
||||||
|
keyboard._os_keyboard.init = lambda: None
|
||||||
|
keyboard._os_keyboard.listen = lambda callback: None
|
||||||
|
keyboard._os_keyboard.map_name = dummy_keys.__getitem__
|
||||||
|
keyboard._os_keyboard.press = lambda scan_code: send_instant_event(make_event(KEY_DOWN, None, scan_code))
|
||||||
|
keyboard._os_keyboard.release = lambda scan_code: send_instant_event(make_event(KEY_UP, None, scan_code))
|
||||||
|
keyboard._os_keyboard.type_unicode = lambda char: output_events.append(KeyboardEvent(event_type=KEY_DOWN, scan_code=999, name=char))
|
||||||
|
|
||||||
|
# Shortcuts for defining test inputs and expected outputs.
|
||||||
|
# Usage: d_shift + d_a + u_a + u_shift
|
||||||
|
d_a = [make_event(KEY_DOWN, 'a')]
|
||||||
|
u_a = [make_event(KEY_UP, 'a')]
|
||||||
|
du_a = d_a+u_a
|
||||||
|
d_b = [make_event(KEY_DOWN, 'b')]
|
||||||
|
u_b = [make_event(KEY_UP, 'b')]
|
||||||
|
du_b = d_b+u_b
|
||||||
|
d_c = [make_event(KEY_DOWN, 'c')]
|
||||||
|
u_c = [make_event(KEY_UP, 'c')]
|
||||||
|
du_c = d_c+u_c
|
||||||
|
d_ctrl = [make_event(KEY_DOWN, 'left ctrl')]
|
||||||
|
u_ctrl = [make_event(KEY_UP, 'left ctrl')]
|
||||||
|
du_ctrl = d_ctrl+u_ctrl
|
||||||
|
d_shift = [make_event(KEY_DOWN, 'left shift')]
|
||||||
|
u_shift = [make_event(KEY_UP, 'left shift')]
|
||||||
|
du_shift = d_shift+u_shift
|
||||||
|
d_alt = [make_event(KEY_DOWN, 'alt')]
|
||||||
|
u_alt = [make_event(KEY_UP, 'alt')]
|
||||||
|
du_alt = d_alt+u_alt
|
||||||
|
du_backspace = [make_event(KEY_DOWN, 'backspace'), make_event(KEY_UP, 'backspace')]
|
||||||
|
du_capslock = [make_event(KEY_DOWN, 'caps lock'), make_event(KEY_UP, 'caps lock')]
|
||||||
|
d_space = [make_event(KEY_DOWN, 'space')]
|
||||||
|
u_space = [make_event(KEY_UP, 'space')]
|
||||||
|
du_space = [make_event(KEY_DOWN, 'space'), make_event(KEY_UP, 'space')]
|
||||||
|
|
||||||
|
trigger = lambda e=None: keyboard.press(999)
|
||||||
|
triggered_event = [KeyboardEvent(KEY_DOWN, scan_code=999)]
|
||||||
|
|
||||||
|
class TestKeyboard(unittest.TestCase):
|
||||||
|
def tearDown(self):
|
||||||
|
keyboard.unhook_all()
|
||||||
|
#self.assertEquals(keyboard._hooks, {})
|
||||||
|
#self.assertEquals(keyboard._hotkeys, {})
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
#keyboard._hooks.clear()
|
||||||
|
#keyboard._hotkeys.clear()
|
||||||
|
del input_events[:]
|
||||||
|
del output_events[:]
|
||||||
|
keyboard._recording = None
|
||||||
|
keyboard._pressed_events.clear()
|
||||||
|
keyboard._physically_pressed_keys.clear()
|
||||||
|
keyboard._logically_pressed_keys.clear()
|
||||||
|
keyboard._hotkeys.clear()
|
||||||
|
keyboard._listener.init()
|
||||||
|
keyboard._word_listeners = {}
|
||||||
|
|
||||||
|
def do(self, manual_events, expected=None):
|
||||||
|
input_events.extend(manual_events)
|
||||||
|
while input_events:
|
||||||
|
event = input_events.pop(0)
|
||||||
|
if keyboard._listener.direct_callback(event):
|
||||||
|
output_events.append(event)
|
||||||
|
if expected is not None:
|
||||||
|
to_names = lambda es: '+'.join(('d' if e.event_type == KEY_DOWN else 'u') + '_' + str(e.scan_code) for e in es)
|
||||||
|
self.assertEqual(to_names(output_events), to_names(expected))
|
||||||
|
del output_events[:]
|
||||||
|
|
||||||
|
keyboard._listener.queue.join()
|
||||||
|
|
||||||
|
def test_event_json(self):
|
||||||
|
event = make_event(KEY_DOWN, u'á \'"', 999)
|
||||||
|
import json
|
||||||
|
self.assertEqual(event, KeyboardEvent(**json.loads(event.to_json())))
|
||||||
|
|
||||||
|
def test_is_modifier_name(self):
|
||||||
|
for name in keyboard.all_modifiers:
|
||||||
|
self.assertTrue(keyboard.is_modifier(name))
|
||||||
|
def test_is_modifier_scan_code(self):
|
||||||
|
for i in range(10):
|
||||||
|
self.assertEqual(keyboard.is_modifier(i), i in [4, 5, 6, 7])
|
||||||
|
|
||||||
|
def test_key_to_scan_codes_brute(self):
|
||||||
|
for name, entries in dummy_keys.items():
|
||||||
|
if name in ['none', 'duplicated']: continue
|
||||||
|
expected = tuple(scan_code for scan_code, modifiers in entries)
|
||||||
|
self.assertEqual(keyboard.key_to_scan_codes(name), expected)
|
||||||
|
def test_key_to_scan_code_from_scan_code(self):
|
||||||
|
for i in range(10):
|
||||||
|
self.assertEqual(keyboard.key_to_scan_codes(i), (i,))
|
||||||
|
def test_key_to_scan_code_from_letter(self):
|
||||||
|
self.assertEqual(keyboard.key_to_scan_codes('a'), (1,))
|
||||||
|
self.assertEqual(keyboard.key_to_scan_codes('A'), (1,-1))
|
||||||
|
def test_key_to_scan_code_from_normalized(self):
|
||||||
|
self.assertEqual(keyboard.key_to_scan_codes('shift'), (5,6))
|
||||||
|
self.assertEqual(keyboard.key_to_scan_codes('SHIFT'), (5,6))
|
||||||
|
self.assertEqual(keyboard.key_to_scan_codes('ctrl'), keyboard.key_to_scan_codes('CONTROL'))
|
||||||
|
def test_key_to_scan_code_from_sided_modifier(self):
|
||||||
|
self.assertEqual(keyboard.key_to_scan_codes('left shift'), (5,))
|
||||||
|
self.assertEqual(keyboard.key_to_scan_codes('right shift'), (6,))
|
||||||
|
def test_key_to_scan_code_underscores(self):
|
||||||
|
self.assertEqual(keyboard.key_to_scan_codes('_'), (12,))
|
||||||
|
self.assertEqual(keyboard.key_to_scan_codes('right_shift'), (6,))
|
||||||
|
def test_key_to_scan_code_error_none(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
keyboard.key_to_scan_codes(None)
|
||||||
|
def test_key_to_scan_code_error_empty(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
keyboard.key_to_scan_codes('')
|
||||||
|
def test_key_to_scan_code_error_other(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
keyboard.key_to_scan_codes({})
|
||||||
|
def test_key_to_scan_code_list(self):
|
||||||
|
self.assertEqual(keyboard.key_to_scan_codes([10, 5, 'a']), (10, 5, 1))
|
||||||
|
def test_key_to_scan_code_empty(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
keyboard.key_to_scan_codes('none')
|
||||||
|
def test_key_to_scan_code_duplicated(self):
|
||||||
|
self.assertEqual(keyboard.key_to_scan_codes('duplicated'), (20,))
|
||||||
|
|
||||||
|
def test_parse_hotkey_simple(self):
|
||||||
|
self.assertEqual(keyboard.parse_hotkey('a'), (((1,),),))
|
||||||
|
self.assertEqual(keyboard.parse_hotkey('A'), (((1,-1),),))
|
||||||
|
def test_parse_hotkey_separators(self):
|
||||||
|
self.assertEqual(keyboard.parse_hotkey('+'), keyboard.parse_hotkey('plus'))
|
||||||
|
self.assertEqual(keyboard.parse_hotkey(','), keyboard.parse_hotkey('comma'))
|
||||||
|
def test_parse_hotkey_keys(self):
|
||||||
|
self.assertEqual(keyboard.parse_hotkey('left shift + a'), (((5,), (1,),),))
|
||||||
|
self.assertEqual(keyboard.parse_hotkey('left shift+a'), (((5,), (1,),),))
|
||||||
|
def test_parse_hotkey_simple_steps(self):
|
||||||
|
self.assertEqual(keyboard.parse_hotkey('a,b'), (((1,),),((2,),)))
|
||||||
|
self.assertEqual(keyboard.parse_hotkey('a, b'), (((1,),),((2,),)))
|
||||||
|
def test_parse_hotkey_steps(self):
|
||||||
|
self.assertEqual(keyboard.parse_hotkey('a+b, b+c'), (((1,),(2,)),((2,),(3,))))
|
||||||
|
def test_parse_hotkey_example(self):
|
||||||
|
alt_codes = keyboard.key_to_scan_codes('alt')
|
||||||
|
shift_codes = keyboard.key_to_scan_codes('shift')
|
||||||
|
a_codes = keyboard.key_to_scan_codes('a')
|
||||||
|
b_codes = keyboard.key_to_scan_codes('b')
|
||||||
|
c_codes = keyboard.key_to_scan_codes('c')
|
||||||
|
self.assertEqual(keyboard.parse_hotkey("alt+shift+a, alt+b, c"), ((alt_codes, shift_codes, a_codes), (alt_codes, b_codes), (c_codes,)))
|
||||||
|
def test_parse_hotkey_list_scan_codes(self):
|
||||||
|
self.assertEqual(keyboard.parse_hotkey([1, 2, 3]), (((1,), (2,), (3,)),))
|
||||||
|
def test_parse_hotkey_deep_list_scan_codes(self):
|
||||||
|
result = keyboard.parse_hotkey('a')
|
||||||
|
self.assertEqual(keyboard.parse_hotkey(result), (((1,),),))
|
||||||
|
def test_parse_hotkey_list_names(self):
|
||||||
|
self.assertEqual(keyboard.parse_hotkey(['a', 'b', 'c']), (((1,), (2,), (3,)),))
|
||||||
|
|
||||||
|
def test_is_pressed_none(self):
|
||||||
|
self.assertFalse(keyboard.is_pressed('a'))
|
||||||
|
def test_is_pressed_true(self):
|
||||||
|
self.do(d_a)
|
||||||
|
self.assertTrue(keyboard.is_pressed('a'))
|
||||||
|
def test_is_pressed_true_scan_code_true(self):
|
||||||
|
self.do(d_a)
|
||||||
|
self.assertTrue(keyboard.is_pressed(1))
|
||||||
|
def test_is_pressed_true_scan_code_false(self):
|
||||||
|
self.do(d_a)
|
||||||
|
self.assertFalse(keyboard.is_pressed(2))
|
||||||
|
def test_is_pressed_true_scan_code_invalid(self):
|
||||||
|
self.do(d_a)
|
||||||
|
self.assertFalse(keyboard.is_pressed(-1))
|
||||||
|
def test_is_pressed_false(self):
|
||||||
|
self.do(d_a+u_a+d_b)
|
||||||
|
self.assertFalse(keyboard.is_pressed('a'))
|
||||||
|
self.assertTrue(keyboard.is_pressed('b'))
|
||||||
|
def test_is_pressed_hotkey_true(self):
|
||||||
|
self.do(d_shift+d_a)
|
||||||
|
self.assertTrue(keyboard.is_pressed('shift+a'))
|
||||||
|
def test_is_pressed_hotkey_false(self):
|
||||||
|
self.do(d_shift+d_a+u_a)
|
||||||
|
self.assertFalse(keyboard.is_pressed('shift+a'))
|
||||||
|
def test_is_pressed_multi_step_fail(self):
|
||||||
|
self.do(u_a+d_a)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
keyboard.is_pressed('a, b')
|
||||||
|
|
||||||
|
def test_send_single_press_release(self):
|
||||||
|
keyboard.send('a', do_press=True, do_release=True)
|
||||||
|
self.do([], d_a+u_a)
|
||||||
|
def test_send_single_press(self):
|
||||||
|
keyboard.send('a', do_press=True, do_release=False)
|
||||||
|
self.do([], d_a)
|
||||||
|
def test_send_single_release(self):
|
||||||
|
keyboard.send('a', do_press=False, do_release=True)
|
||||||
|
self.do([], u_a)
|
||||||
|
def test_send_single_none(self):
|
||||||
|
keyboard.send('a', do_press=False, do_release=False)
|
||||||
|
self.do([], [])
|
||||||
|
def test_press(self):
|
||||||
|
keyboard.press('a')
|
||||||
|
self.do([], d_a)
|
||||||
|
def test_release(self):
|
||||||
|
keyboard.release('a')
|
||||||
|
self.do([], u_a)
|
||||||
|
def test_press_and_release(self):
|
||||||
|
keyboard.press_and_release('a')
|
||||||
|
self.do([], d_a+u_a)
|
||||||
|
|
||||||
|
def test_send_modifier_press_release(self):
|
||||||
|
keyboard.send('ctrl+a', do_press=True, do_release=True)
|
||||||
|
self.do([], d_ctrl+d_a+u_a+u_ctrl)
|
||||||
|
def test_send_modifiers_release(self):
|
||||||
|
keyboard.send('ctrl+shift+a', do_press=False, do_release=True)
|
||||||
|
self.do([], u_a+u_shift+u_ctrl)
|
||||||
|
|
||||||
|
def test_call_later(self):
|
||||||
|
triggered = []
|
||||||
|
def fn(arg1, arg2):
|
||||||
|
assert arg1 == 1 and arg2 == 2
|
||||||
|
triggered.append(True)
|
||||||
|
keyboard.call_later(fn, (1, 2), 0.01)
|
||||||
|
self.assertFalse(triggered)
|
||||||
|
time.sleep(0.05)
|
||||||
|
self.assertTrue(triggered)
|
||||||
|
|
||||||
|
def test_hook_nonblocking(self):
|
||||||
|
self.i = 0
|
||||||
|
def count(e):
|
||||||
|
self.assertEqual(e.name, 'a')
|
||||||
|
self.i += 1
|
||||||
|
hook = keyboard.hook(count, suppress=False)
|
||||||
|
self.do(d_a+u_a, d_a+u_a)
|
||||||
|
self.assertEqual(self.i, 2)
|
||||||
|
keyboard.unhook(hook)
|
||||||
|
self.do(d_a+u_a, d_a+u_a)
|
||||||
|
self.assertEqual(self.i, 2)
|
||||||
|
keyboard.hook(count, suppress=False)
|
||||||
|
self.do(d_a+u_a, d_a+u_a)
|
||||||
|
self.assertEqual(self.i, 4)
|
||||||
|
keyboard.unhook_all()
|
||||||
|
self.do(d_a+u_a, d_a+u_a)
|
||||||
|
self.assertEqual(self.i, 4)
|
||||||
|
def test_hook_blocking(self):
|
||||||
|
self.i = 0
|
||||||
|
def count(e):
|
||||||
|
self.assertIn(e.name, ['a', 'b'])
|
||||||
|
self.i += 1
|
||||||
|
return e.name == 'b'
|
||||||
|
hook = keyboard.hook(count, suppress=True)
|
||||||
|
self.do(d_a+d_b, d_b)
|
||||||
|
self.assertEqual(self.i, 2)
|
||||||
|
keyboard.unhook(hook)
|
||||||
|
self.do(d_a+d_b, d_a+d_b)
|
||||||
|
self.assertEqual(self.i, 2)
|
||||||
|
keyboard.hook(count, suppress=True)
|
||||||
|
self.do(d_a+d_b, d_b)
|
||||||
|
self.assertEqual(self.i, 4)
|
||||||
|
keyboard.unhook_all()
|
||||||
|
self.do(d_a+d_b, d_a+d_b)
|
||||||
|
self.assertEqual(self.i, 4)
|
||||||
|
def test_on_press_nonblocking(self):
|
||||||
|
keyboard.on_press(lambda e: self.assertEqual(e.name, 'a') and self.assertEqual(e.event_type, KEY_DOWN))
|
||||||
|
self.do(d_a+u_a)
|
||||||
|
def test_on_press_blocking(self):
|
||||||
|
keyboard.on_press(lambda e: e.scan_code == 1, suppress=True)
|
||||||
|
self.do([make_event(KEY_DOWN, 'A', -1)] + d_a, d_a)
|
||||||
|
def test_on_release(self):
|
||||||
|
keyboard.on_release(lambda e: self.assertEqual(e.name, 'a') and self.assertEqual(e.event_type, KEY_UP))
|
||||||
|
self.do(d_a+u_a)
|
||||||
|
|
||||||
|
def test_hook_key_invalid(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
keyboard.hook_key('invalid', lambda e: None)
|
||||||
|
def test_hook_key_nonblocking(self):
|
||||||
|
self.i = 0
|
||||||
|
def count(event):
|
||||||
|
self.i += 1
|
||||||
|
hook = keyboard.hook_key('A', count)
|
||||||
|
self.do(d_a)
|
||||||
|
self.assertEqual(self.i, 1)
|
||||||
|
self.do(u_a+d_b)
|
||||||
|
self.assertEqual(self.i, 2)
|
||||||
|
self.do([make_event(KEY_DOWN, 'A', -1)])
|
||||||
|
self.assertEqual(self.i, 3)
|
||||||
|
keyboard.unhook_key(hook)
|
||||||
|
self.do(d_a)
|
||||||
|
self.assertEqual(self.i, 3)
|
||||||
|
def test_hook_key_blocking(self):
|
||||||
|
self.i = 0
|
||||||
|
def count(event):
|
||||||
|
self.i += 1
|
||||||
|
return event.scan_code == 1
|
||||||
|
hook = keyboard.hook_key('A', count, suppress=True)
|
||||||
|
self.do(d_a, d_a)
|
||||||
|
self.assertEqual(self.i, 1)
|
||||||
|
self.do(u_a+d_b, u_a+d_b)
|
||||||
|
self.assertEqual(self.i, 2)
|
||||||
|
self.do([make_event(KEY_DOWN, 'A', -1)], [])
|
||||||
|
self.assertEqual(self.i, 3)
|
||||||
|
keyboard.unhook_key(hook)
|
||||||
|
self.do([make_event(KEY_DOWN, 'A', -1)], [make_event(KEY_DOWN, 'A', -1)])
|
||||||
|
self.assertEqual(self.i, 3)
|
||||||
|
def test_on_press_key_nonblocking(self):
|
||||||
|
keyboard.on_press_key('A', lambda e: self.assertEqual(e.name, 'a') and self.assertEqual(e.event_type, KEY_DOWN))
|
||||||
|
self.do(d_a+u_a+d_b+u_b)
|
||||||
|
def test_on_press_key_blocking(self):
|
||||||
|
keyboard.on_press_key('A', lambda e: e.scan_code == 1, suppress=True)
|
||||||
|
self.do([make_event(KEY_DOWN, 'A', -1)] + d_a, d_a)
|
||||||
|
def test_on_release_key(self):
|
||||||
|
keyboard.on_release_key('a', lambda e: self.assertEqual(e.name, 'a') and self.assertEqual(e.event_type, KEY_UP))
|
||||||
|
self.do(d_a+u_a)
|
||||||
|
|
||||||
|
def test_block_key(self):
|
||||||
|
blocked = keyboard.block_key('a')
|
||||||
|
self.do(d_a+d_b, d_b)
|
||||||
|
self.do([make_event(KEY_DOWN, 'A', -1)], [make_event(KEY_DOWN, 'A', -1)])
|
||||||
|
keyboard.unblock_key(blocked)
|
||||||
|
self.do(d_a+d_b, d_a+d_b)
|
||||||
|
def test_block_key_ambiguous(self):
|
||||||
|
keyboard.block_key('A')
|
||||||
|
self.do(d_a+d_b, d_b)
|
||||||
|
self.do([make_event(KEY_DOWN, 'A', -1)], [])
|
||||||
|
|
||||||
|
def test_remap_key_simple(self):
|
||||||
|
mapped = keyboard.remap_key('a', 'b')
|
||||||
|
self.do(d_a+d_c+u_a, d_b+d_c+u_b)
|
||||||
|
keyboard.unremap_key(mapped)
|
||||||
|
self.do(d_a+d_c+u_a, d_a+d_c+u_a)
|
||||||
|
def test_remap_key_ambiguous(self):
|
||||||
|
keyboard.remap_key('A', 'b')
|
||||||
|
self.do(d_a+d_b, d_b+d_b)
|
||||||
|
self.do([make_event(KEY_DOWN, 'A', -1)], d_b)
|
||||||
|
def test_remap_key_multiple(self):
|
||||||
|
mapped = keyboard.remap_key('a', 'shift+b')
|
||||||
|
self.do(d_a+d_c+u_a, d_shift+d_b+d_c+u_b+u_shift)
|
||||||
|
keyboard.unremap_key(mapped)
|
||||||
|
self.do(d_a+d_c+u_a, d_a+d_c+u_a)
|
||||||
|
|
||||||
|
def test_stash_state(self):
|
||||||
|
self.do(d_a+d_shift)
|
||||||
|
self.assertEqual(sorted(keyboard.stash_state()), [1, 5])
|
||||||
|
self.do([], u_a+u_shift)
|
||||||
|
def test_restore_state(self):
|
||||||
|
self.do(d_b)
|
||||||
|
keyboard.restore_state([1, 5])
|
||||||
|
self.do([], u_b+d_a+d_shift)
|
||||||
|
def test_restore_modifieres(self):
|
||||||
|
self.do(d_b)
|
||||||
|
keyboard.restore_modifiers([1, 5])
|
||||||
|
self.do([], u_b+d_shift)
|
||||||
|
|
||||||
|
def test_write_simple(self):
|
||||||
|
keyboard.write('a', exact=False)
|
||||||
|
self.do([], d_a+u_a)
|
||||||
|
def test_write_multiple(self):
|
||||||
|
keyboard.write('ab', exact=False)
|
||||||
|
self.do([], d_a+u_a+d_b+u_b)
|
||||||
|
def test_write_modifiers(self):
|
||||||
|
keyboard.write('Ab', exact=False)
|
||||||
|
self.do([], d_shift+d_a+u_a+u_shift+d_b+u_b)
|
||||||
|
# restore_state_after has been removed after the introduction of `restore_modifiers`.
|
||||||
|
#def test_write_stash_not_restore(self):
|
||||||
|
# self.do(d_shift)
|
||||||
|
# keyboard.write('a', restore_state_after=False, exact=False)
|
||||||
|
# self.do([], u_shift+d_a+u_a)
|
||||||
|
def test_write_stash_restore(self):
|
||||||
|
self.do(d_shift)
|
||||||
|
keyboard.write('a', exact=False)
|
||||||
|
self.do([], u_shift+d_a+u_a+d_shift)
|
||||||
|
def test_write_multiple(self):
|
||||||
|
last_time = time.time()
|
||||||
|
keyboard.write('ab', delay=0.01, exact=False)
|
||||||
|
self.do([], d_a+u_a+d_b+u_b)
|
||||||
|
self.assertGreater(time.time() - last_time, 0.015)
|
||||||
|
def test_write_unicode_explicit(self):
|
||||||
|
keyboard.write('ab', exact=True)
|
||||||
|
self.do([], [KeyboardEvent(event_type=KEY_DOWN, scan_code=999, name='a'), KeyboardEvent(event_type=KEY_DOWN, scan_code=999, name='b')])
|
||||||
|
def test_write_unicode_fallback(self):
|
||||||
|
keyboard.write(u'áb', exact=False)
|
||||||
|
self.do([], [KeyboardEvent(event_type=KEY_DOWN, scan_code=999, name=u'á')]+d_b+u_b)
|
||||||
|
|
||||||
|
def test_start_stop_recording(self):
|
||||||
|
keyboard.start_recording()
|
||||||
|
self.do(d_a+u_a)
|
||||||
|
self.assertEqual(keyboard.stop_recording(), d_a+u_a)
|
||||||
|
def test_stop_recording_error(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
keyboard.stop_recording()
|
||||||
|
|
||||||
|
def test_record(self):
|
||||||
|
queue = keyboard._queue.Queue()
|
||||||
|
def process():
|
||||||
|
queue.put(keyboard.record('space', suppress=True))
|
||||||
|
from threading import Thread
|
||||||
|
t = Thread(target=process)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
# 0.01s sleep failed once already. Better solutions?
|
||||||
|
time.sleep(0.01)
|
||||||
|
self.do(du_a+du_b+du_space, du_a+du_b)
|
||||||
|
self.assertEqual(queue.get(timeout=0.5), du_a+du_b+du_space)
|
||||||
|
|
||||||
|
def test_play_nodelay(self):
|
||||||
|
keyboard.play(d_a+u_a, 0)
|
||||||
|
self.do([], d_a+u_a)
|
||||||
|
def test_play_stash(self):
|
||||||
|
self.do(d_ctrl)
|
||||||
|
keyboard.play(d_a+u_a, 0)
|
||||||
|
self.do([], u_ctrl+d_a+u_a+d_ctrl)
|
||||||
|
def test_play_delay(self):
|
||||||
|
last_time = time.time()
|
||||||
|
events = [make_event(KEY_DOWN, 'a', 1, 100), make_event(KEY_UP, 'a', 1, 100.01)]
|
||||||
|
keyboard.play(events, 1)
|
||||||
|
self.do([], d_a+u_a)
|
||||||
|
self.assertGreater(time.time() - last_time, 0.005)
|
||||||
|
|
||||||
|
def test_get_typed_strings_simple(self):
|
||||||
|
events = du_a+du_b+du_backspace+d_shift+du_a+u_shift+du_space+du_ctrl+du_a
|
||||||
|
self.assertEqual(list(keyboard.get_typed_strings(events)), ['aA ', 'a'])
|
||||||
|
def test_get_typed_strings_backspace(self):
|
||||||
|
events = du_a+du_b+du_backspace
|
||||||
|
self.assertEqual(list(keyboard.get_typed_strings(events)), ['a'])
|
||||||
|
events = du_backspace+du_a+du_b
|
||||||
|
self.assertEqual(list(keyboard.get_typed_strings(events)), ['ab'])
|
||||||
|
def test_get_typed_strings_shift(self):
|
||||||
|
events = d_shift+du_a+du_b+u_shift+du_space+du_ctrl+du_a
|
||||||
|
self.assertEqual(list(keyboard.get_typed_strings(events)), ['AB ', 'a'])
|
||||||
|
def test_get_typed_strings_all(self):
|
||||||
|
events = du_a+du_b+du_backspace+d_shift+du_a+du_capslock+du_b+u_shift+du_space+du_ctrl+du_a
|
||||||
|
self.assertEqual(list(keyboard.get_typed_strings(events)), ['aAb ', 'A'])
|
||||||
|
|
||||||
|
def test_get_hotkey_name_simple(self):
|
||||||
|
self.assertEqual(keyboard.get_hotkey_name(['a']), 'a')
|
||||||
|
def test_get_hotkey_name_modifiers(self):
|
||||||
|
self.assertEqual(keyboard.get_hotkey_name(['a', 'shift', 'ctrl']), 'ctrl+shift+a')
|
||||||
|
def test_get_hotkey_name_normalize(self):
|
||||||
|
self.assertEqual(keyboard.get_hotkey_name(['SHIFT', 'left ctrl']), 'ctrl+shift')
|
||||||
|
def test_get_hotkey_name_plus(self):
|
||||||
|
self.assertEqual(keyboard.get_hotkey_name(['+']), 'plus')
|
||||||
|
def test_get_hotkey_name_duplicated(self):
|
||||||
|
self.assertEqual(keyboard.get_hotkey_name(['+', 'plus']), 'plus')
|
||||||
|
def test_get_hotkey_name_full(self):
|
||||||
|
self.assertEqual(keyboard.get_hotkey_name(['+', 'left ctrl', 'shift', 'WIN', 'right alt']), 'ctrl+alt+shift+windows+plus')
|
||||||
|
def test_get_hotkey_name_multiple(self):
|
||||||
|
self.assertEqual(keyboard.get_hotkey_name(['ctrl', 'b', '!', 'a']), 'ctrl+!+a+b')
|
||||||
|
def test_get_hotkey_name_from_pressed(self):
|
||||||
|
self.do(du_c+d_ctrl+d_a+d_b)
|
||||||
|
self.assertEqual(keyboard.get_hotkey_name(), 'ctrl+a+b')
|
||||||
|
|
||||||
|
def test_read_hotkey(self):
|
||||||
|
queue = keyboard._queue.Queue()
|
||||||
|
def process():
|
||||||
|
queue.put(keyboard.read_hotkey())
|
||||||
|
from threading import Thread
|
||||||
|
t = Thread(target=process)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
time.sleep(0.01)
|
||||||
|
self.do(d_ctrl+d_a+d_b+u_ctrl)
|
||||||
|
self.assertEqual(queue.get(timeout=0.5), 'ctrl+a+b')
|
||||||
|
|
||||||
|
def test_read_event(self):
|
||||||
|
queue = keyboard._queue.Queue()
|
||||||
|
def process():
|
||||||
|
queue.put(keyboard.read_event(suppress=True))
|
||||||
|
from threading import Thread
|
||||||
|
t = Thread(target=process)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
time.sleep(0.01)
|
||||||
|
self.do(d_a, [])
|
||||||
|
self.assertEqual(queue.get(timeout=0.5), d_a[0])
|
||||||
|
|
||||||
|
def test_read_key(self):
|
||||||
|
queue = keyboard._queue.Queue()
|
||||||
|
def process():
|
||||||
|
queue.put(keyboard.read_key(suppress=True))
|
||||||
|
from threading import Thread
|
||||||
|
t = Thread(target=process)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
time.sleep(0.01)
|
||||||
|
self.do(d_a, [])
|
||||||
|
self.assertEqual(queue.get(timeout=0.5), 'a')
|
||||||
|
|
||||||
|
def test_wait_infinite(self):
|
||||||
|
self.triggered = False
|
||||||
|
def process():
|
||||||
|
keyboard.wait()
|
||||||
|
self.triggered = True
|
||||||
|
from threading import Thread
|
||||||
|
t = Thread(target=process)
|
||||||
|
t.daemon = True # Yep, we are letting this thread loose.
|
||||||
|
t.start()
|
||||||
|
time.sleep(0.01)
|
||||||
|
self.assertFalse(self.triggered)
|
||||||
|
|
||||||
|
def test_wait_until_success(self):
|
||||||
|
queue = keyboard._queue.Queue()
|
||||||
|
def process():
|
||||||
|
queue.put(keyboard.wait(queue.get(timeout=0.5), suppress=True) or True)
|
||||||
|
from threading import Thread
|
||||||
|
t = Thread(target=process)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
queue.put('a')
|
||||||
|
time.sleep(0.01)
|
||||||
|
self.do(d_a, [])
|
||||||
|
self.assertTrue(queue.get(timeout=0.5))
|
||||||
|
def test_wait_until_fail(self):
|
||||||
|
def process():
|
||||||
|
keyboard.wait('a', suppress=True)
|
||||||
|
self.fail()
|
||||||
|
from threading import Thread
|
||||||
|
t = Thread(target=process)
|
||||||
|
t.daemon = True # Yep, we are letting this thread loose.
|
||||||
|
t.start()
|
||||||
|
time.sleep(0.01)
|
||||||
|
self.do(d_b)
|
||||||
|
|
||||||
|
def test_add_hotkey_single_step_suppress_allow(self):
|
||||||
|
keyboard.add_hotkey('a', lambda: trigger() or True, suppress=True)
|
||||||
|
self.do(d_a, triggered_event+d_a)
|
||||||
|
def test_add_hotkey_single_step_suppress_args_allow(self):
|
||||||
|
arg = object()
|
||||||
|
keyboard.add_hotkey('a', lambda a: self.assertIs(a, arg) or trigger() or True, args=(arg,), suppress=True)
|
||||||
|
self.do(d_a, triggered_event+d_a)
|
||||||
|
def test_add_hotkey_single_step_suppress_single(self):
|
||||||
|
keyboard.add_hotkey('a', trigger, suppress=True)
|
||||||
|
self.do(d_a, triggered_event)
|
||||||
|
def test_add_hotkey_single_step_suppress_removed(self):
|
||||||
|
keyboard.remove_hotkey(keyboard.add_hotkey('a', trigger, suppress=True))
|
||||||
|
self.do(d_a, d_a)
|
||||||
|
def test_add_hotkey_single_step_suppress_removed(self):
|
||||||
|
keyboard.remove_hotkey(keyboard.add_hotkey('ctrl+a', trigger, suppress=True))
|
||||||
|
self.do(d_ctrl+d_a, d_ctrl+d_a)
|
||||||
|
self.assertEqual(keyboard._listener.filtered_modifiers[dummy_keys['left ctrl'][0][0]], 0)
|
||||||
|
def test_remove_hotkey_internal(self):
|
||||||
|
remove = keyboard.add_hotkey('shift+a', trigger, suppress=True)
|
||||||
|
self.assertTrue(all(keyboard._listener.blocking_hotkeys.values()))
|
||||||
|
self.assertTrue(all(keyboard._listener.filtered_modifiers.values()))
|
||||||
|
self.assertNotEqual(keyboard._hotkeys, {})
|
||||||
|
remove()
|
||||||
|
self.assertTrue(not any(keyboard._listener.filtered_modifiers.values()))
|
||||||
|
self.assertTrue(not any(keyboard._listener.blocking_hotkeys.values()))
|
||||||
|
self.assertEqual(keyboard._hotkeys, {})
|
||||||
|
def test_remove_hotkey_internal_multistep_start(self):
|
||||||
|
remove = keyboard.add_hotkey('shift+a, b', trigger, suppress=True)
|
||||||
|
self.assertTrue(all(keyboard._listener.blocking_hotkeys.values()))
|
||||||
|
self.assertTrue(all(keyboard._listener.filtered_modifiers.values()))
|
||||||
|
self.assertNotEqual(keyboard._hotkeys, {})
|
||||||
|
remove()
|
||||||
|
self.assertTrue(not any(keyboard._listener.filtered_modifiers.values()))
|
||||||
|
self.assertTrue(not any(keyboard._listener.blocking_hotkeys.values()))
|
||||||
|
self.assertEqual(keyboard._hotkeys, {})
|
||||||
|
def test_remove_hotkey_internal_multistep_end(self):
|
||||||
|
remove = keyboard.add_hotkey('shift+a, b', trigger, suppress=True)
|
||||||
|
self.do(d_shift+du_a+u_shift)
|
||||||
|
self.assertTrue(any(keyboard._listener.blocking_hotkeys.values()))
|
||||||
|
self.assertTrue(not any(keyboard._listener.filtered_modifiers.values()))
|
||||||
|
self.assertNotEqual(keyboard._hotkeys, {})
|
||||||
|
remove()
|
||||||
|
self.assertTrue(not any(keyboard._listener.filtered_modifiers.values()))
|
||||||
|
self.assertTrue(not any(keyboard._listener.blocking_hotkeys.values()))
|
||||||
|
self.assertEqual(keyboard._hotkeys, {})
|
||||||
|
def test_add_hotkey_single_step_suppress_with_modifiers(self):
|
||||||
|
keyboard.add_hotkey('ctrl+shift+a', trigger, suppress=True)
|
||||||
|
self.do(d_ctrl+d_shift+d_a, triggered_event)
|
||||||
|
def test_add_hotkey_single_step_suppress_with_modifiers_fail_unrelated_modifier(self):
|
||||||
|
keyboard.add_hotkey('ctrl+shift+a', trigger, suppress=True)
|
||||||
|
self.do(d_ctrl+d_shift+u_shift+d_a, d_shift+u_shift+d_ctrl+d_a)
|
||||||
|
def test_add_hotkey_single_step_suppress_with_modifiers_fail_unrelated_key(self):
|
||||||
|
keyboard.add_hotkey('ctrl+shift+a', trigger, suppress=True)
|
||||||
|
self.do(d_ctrl+d_shift+du_b, d_shift+d_ctrl+du_b)
|
||||||
|
def test_add_hotkey_single_step_suppress_with_modifiers_unrelated_key(self):
|
||||||
|
keyboard.add_hotkey('ctrl+shift+a', trigger, suppress=True)
|
||||||
|
self.do(d_ctrl+d_shift+du_b+d_a, d_shift+d_ctrl+du_b+triggered_event)
|
||||||
|
def test_add_hotkey_single_step_suppress_with_modifiers_release(self):
|
||||||
|
keyboard.add_hotkey('ctrl+shift+a', trigger, suppress=True)
|
||||||
|
self.do(d_ctrl+d_shift+du_b+d_a+u_ctrl+u_shift, d_shift+d_ctrl+du_b+triggered_event+u_ctrl+u_shift)
|
||||||
|
def test_add_hotkey_single_step_suppress_with_modifiers_out_of_order(self):
|
||||||
|
keyboard.add_hotkey('ctrl+shift+a', trigger, suppress=True)
|
||||||
|
self.do(d_shift+d_ctrl+d_a, triggered_event)
|
||||||
|
def test_add_hotkey_single_step_suppress_with_modifiers_repeated(self):
|
||||||
|
keyboard.add_hotkey('ctrl+a', trigger, suppress=True)
|
||||||
|
self.do(d_ctrl+du_a+du_b+du_a, triggered_event+d_ctrl+du_b+triggered_event)
|
||||||
|
def test_add_hotkey_single_step_suppress_with_modifiers_release(self):
|
||||||
|
keyboard.add_hotkey('ctrl+a', trigger, suppress=True, trigger_on_release=True)
|
||||||
|
self.do(d_ctrl+du_a+du_b+du_a, triggered_event+d_ctrl+du_b+triggered_event)
|
||||||
|
def test_add_hotkey_single_step_suppress_with_modifier_superset_release(self):
|
||||||
|
keyboard.add_hotkey('ctrl+a', trigger, suppress=True, trigger_on_release=True)
|
||||||
|
self.do(d_ctrl+d_shift+du_a+u_shift+u_ctrl, d_ctrl+d_shift+du_a+u_shift+u_ctrl)
|
||||||
|
def test_add_hotkey_single_step_suppress_with_modifier_superset(self):
|
||||||
|
keyboard.add_hotkey('ctrl+a', trigger, suppress=True)
|
||||||
|
self.do(d_ctrl+d_shift+du_a+u_shift+u_ctrl, d_ctrl+d_shift+du_a+u_shift+u_ctrl)
|
||||||
|
def test_add_hotkey_single_step_timeout(self):
|
||||||
|
keyboard.add_hotkey('a', trigger, timeout=1, suppress=True)
|
||||||
|
self.do(du_a, triggered_event)
|
||||||
|
def test_add_hotkey_multi_step_first_timeout(self):
|
||||||
|
keyboard.add_hotkey('a, b', trigger, timeout=0.01, suppress=True)
|
||||||
|
time.sleep(0.03)
|
||||||
|
self.do(du_a+du_b, triggered_event)
|
||||||
|
def test_add_hotkey_multi_step_last_timeout(self):
|
||||||
|
keyboard.add_hotkey('a, b', trigger, timeout=0.01, suppress=True)
|
||||||
|
self.do(du_a, [])
|
||||||
|
time.sleep(0.05)
|
||||||
|
self.do(du_b, du_a+du_b)
|
||||||
|
def test_add_hotkey_multi_step_success_timeout(self):
|
||||||
|
keyboard.add_hotkey('a, b', trigger, timeout=0.05, suppress=True)
|
||||||
|
self.do(du_a, [])
|
||||||
|
time.sleep(0.01)
|
||||||
|
self.do(du_b, triggered_event)
|
||||||
|
def test_add_hotkey_multi_step_suffix_timeout(self):
|
||||||
|
keyboard.add_hotkey('a, b, a', trigger, timeout=0.01, suppress=True)
|
||||||
|
self.do(du_a+du_b, [])
|
||||||
|
time.sleep(0.05)
|
||||||
|
self.do(du_a, du_a+du_b)
|
||||||
|
self.do(du_b+du_a, triggered_event)
|
||||||
|
def test_add_hotkey_multi_step_allow(self):
|
||||||
|
keyboard.add_hotkey('a, b', lambda: trigger() or True, suppress=True)
|
||||||
|
self.do(du_a+du_b, triggered_event+du_a+du_b)
|
||||||
|
|
||||||
|
def test_add_hotkey_single_step_nonsuppress(self):
|
||||||
|
queue = keyboard._queue.Queue()
|
||||||
|
keyboard.add_hotkey('ctrl+shift+a+b', lambda: queue.put(True), suppress=False)
|
||||||
|
self.do(d_shift+d_ctrl+d_a+d_b)
|
||||||
|
self.assertTrue(queue.get(timeout=0.5))
|
||||||
|
def test_add_hotkey_single_step_nonsuppress_repeated(self):
|
||||||
|
queue = keyboard._queue.Queue()
|
||||||
|
keyboard.add_hotkey('ctrl+shift+a+b', lambda: queue.put(True), suppress=False)
|
||||||
|
self.do(d_shift+d_ctrl+d_a+d_b)
|
||||||
|
self.do(d_shift+d_ctrl+d_a+d_b)
|
||||||
|
self.assertTrue(queue.get(timeout=0.5))
|
||||||
|
self.assertTrue(queue.get(timeout=0.5))
|
||||||
|
def test_add_hotkey_single_step_nosuppress_with_modifiers_out_of_order(self):
|
||||||
|
queue = keyboard._queue.Queue()
|
||||||
|
keyboard.add_hotkey('ctrl+shift+a', lambda: queue.put(True), suppress=False)
|
||||||
|
self.do(d_shift+d_ctrl+d_a)
|
||||||
|
self.assertTrue(queue.get(timeout=0.5))
|
||||||
|
def test_add_hotkey_single_step_suppress_regression_1(self):
|
||||||
|
keyboard.add_hotkey('a', trigger, suppress=True)
|
||||||
|
self.do(d_c+d_a+u_c+u_a, d_c+d_a+u_c+u_a)
|
||||||
|
|
||||||
|
def test_remap_hotkey_single(self):
|
||||||
|
keyboard.remap_hotkey('a', 'b')
|
||||||
|
self.do(d_a+u_a, d_b+u_b)
|
||||||
|
def test_remap_hotkey_complex_dst(self):
|
||||||
|
keyboard.remap_hotkey('a', 'ctrl+b, c')
|
||||||
|
self.do(d_a+u_a, d_ctrl+du_b+u_ctrl+du_c)
|
||||||
|
def test_remap_hotkey_modifiers(self):
|
||||||
|
keyboard.remap_hotkey('ctrl+shift+a', 'b')
|
||||||
|
self.do(d_ctrl+d_shift+d_a+u_a, du_b)
|
||||||
|
def test_remap_hotkey_modifiers_repeat(self):
|
||||||
|
keyboard.remap_hotkey('ctrl+shift+a', 'b')
|
||||||
|
self.do(d_ctrl+d_shift+du_a+du_a, du_b+du_b)
|
||||||
|
def test_remap_hotkey_modifiers_state(self):
|
||||||
|
keyboard.remap_hotkey('ctrl+shift+a', 'b')
|
||||||
|
self.do(d_ctrl+d_shift+du_c+du_a+du_a, d_shift+d_ctrl+du_c+u_shift+u_ctrl+du_b+d_ctrl+d_shift+u_shift+u_ctrl+du_b+d_ctrl+d_shift)
|
||||||
|
def test_remap_hotkey_release_incomplete(self):
|
||||||
|
keyboard.remap_hotkey('a', 'b', trigger_on_release=True)
|
||||||
|
self.do(d_a, [])
|
||||||
|
def test_remap_hotkey_release_complete(self):
|
||||||
|
keyboard.remap_hotkey('a', 'b', trigger_on_release=True)
|
||||||
|
self.do(du_a, du_b)
|
||||||
|
|
||||||
|
def test_parse_hotkey_combinations_scan_code(self):
|
||||||
|
self.assertEqual(keyboard.parse_hotkey_combinations(30), (((30,),),))
|
||||||
|
def test_parse_hotkey_combinations_single(self):
|
||||||
|
self.assertEqual(keyboard.parse_hotkey_combinations('a'), (((1,),),))
|
||||||
|
def test_parse_hotkey_combinations_single_modifier(self):
|
||||||
|
self.assertEqual(keyboard.parse_hotkey_combinations('shift+a'), (((1, 5), (1, 6)),))
|
||||||
|
def test_parse_hotkey_combinations_single_modifiers(self):
|
||||||
|
self.assertEqual(keyboard.parse_hotkey_combinations('shift+ctrl+a'), (((1, 5, 7), (1, 6, 7)),))
|
||||||
|
def test_parse_hotkey_combinations_multi(self):
|
||||||
|
self.assertEqual(keyboard.parse_hotkey_combinations('a, b'), (((1,),), ((2,),)))
|
||||||
|
def test_parse_hotkey_combinations_multi_modifier(self):
|
||||||
|
self.assertEqual(keyboard.parse_hotkey_combinations('shift+a, b'), (((1, 5), (1, 6)), ((2,),)))
|
||||||
|
def test_parse_hotkey_combinations_list_list(self):
|
||||||
|
self.assertEqual(keyboard.parse_hotkey_combinations(keyboard.parse_hotkey_combinations('a, b')), keyboard.parse_hotkey_combinations('a, b'))
|
||||||
|
def test_parse_hotkey_combinations_fail_empty(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
keyboard.parse_hotkey_combinations('')
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_hotkey_multistep_suppress_incomplete(self):
|
||||||
|
keyboard.add_hotkey('a, b', trigger, suppress=True)
|
||||||
|
self.do(du_a, [])
|
||||||
|
self.assertEqual(keyboard._listener.blocking_hotkeys[(1,)], [])
|
||||||
|
self.assertEqual(len(keyboard._listener.blocking_hotkeys[(2,)]), 1)
|
||||||
|
def test_add_hotkey_multistep_suppress_incomplete(self):
|
||||||
|
keyboard.add_hotkey('a, b', trigger, suppress=True)
|
||||||
|
self.do(du_a+du_b, triggered_event)
|
||||||
|
def test_add_hotkey_multistep_suppress_modifier(self):
|
||||||
|
keyboard.add_hotkey('shift+a, b', trigger, suppress=True)
|
||||||
|
self.do(d_shift+du_a+u_shift+du_b, triggered_event)
|
||||||
|
def test_add_hotkey_multistep_suppress_fail(self):
|
||||||
|
keyboard.add_hotkey('a, b', trigger, suppress=True)
|
||||||
|
self.do(du_a+du_c, du_a+du_c)
|
||||||
|
def test_add_hotkey_multistep_suppress_three_steps(self):
|
||||||
|
keyboard.add_hotkey('a, b, c', trigger, suppress=True)
|
||||||
|
self.do(du_a+du_b+du_c, triggered_event)
|
||||||
|
def test_add_hotkey_multistep_suppress_repeated_prefix(self):
|
||||||
|
keyboard.add_hotkey('a, a, c', trigger, suppress=True, trigger_on_release=True)
|
||||||
|
self.do(du_a+du_a+du_c, triggered_event)
|
||||||
|
def test_add_hotkey_multistep_suppress_repeated_key(self):
|
||||||
|
keyboard.add_hotkey('a, b', trigger, suppress=True)
|
||||||
|
self.do(du_a+du_a+du_b, du_a+triggered_event)
|
||||||
|
self.assertEqual(keyboard._listener.blocking_hotkeys[(2,)], [])
|
||||||
|
self.assertEqual(len(keyboard._listener.blocking_hotkeys[(1,)]), 1)
|
||||||
|
def test_add_hotkey_multi_step_suppress_regression_1(self):
|
||||||
|
keyboard.add_hotkey('a, b', trigger, suppress=True)
|
||||||
|
self.do(d_c+d_a+u_c+u_a+du_c, d_c+d_a+u_c+u_a+du_c)
|
||||||
|
def test_add_hotkey_multi_step_suppress_replays(self):
|
||||||
|
keyboard.add_hotkey('a, b, c', trigger, suppress=True)
|
||||||
|
self.do(du_a+du_b+du_a+du_b+du_space, du_a+du_b+du_a+du_b+du_space)
|
||||||
|
|
||||||
|
def test_add_word_listener_success(self):
|
||||||
|
queue = keyboard._queue.Queue()
|
||||||
|
def free():
|
||||||
|
queue.put(1)
|
||||||
|
keyboard.add_word_listener('abc', free)
|
||||||
|
self.do(du_a+du_b+du_c+du_space)
|
||||||
|
self.assertTrue(queue.get(timeout=0.5))
|
||||||
|
def test_add_word_listener_no_trigger_fail(self):
|
||||||
|
queue = keyboard._queue.Queue()
|
||||||
|
def free():
|
||||||
|
queue.put(1)
|
||||||
|
keyboard.add_word_listener('abc', free)
|
||||||
|
self.do(du_a+du_b+du_c)
|
||||||
|
with self.assertRaises(keyboard._queue.Empty):
|
||||||
|
queue.get(timeout=0.01)
|
||||||
|
def test_add_word_listener_timeout_fail(self):
|
||||||
|
queue = keyboard._queue.Queue()
|
||||||
|
def free():
|
||||||
|
queue.put(1)
|
||||||
|
keyboard.add_word_listener('abc', free, timeout=1)
|
||||||
|
self.do(du_a+du_b+du_c+[make_event(KEY_DOWN, name='space', time=2)])
|
||||||
|
with self.assertRaises(keyboard._queue.Empty):
|
||||||
|
queue.get(timeout=0.01)
|
||||||
|
def test_duplicated_word_listener(self):
|
||||||
|
keyboard.add_word_listener('abc', trigger)
|
||||||
|
keyboard.add_word_listener('abc', trigger)
|
||||||
|
def test_add_word_listener_remove(self):
|
||||||
|
queue = keyboard._queue.Queue()
|
||||||
|
def free():
|
||||||
|
queue.put(1)
|
||||||
|
keyboard.add_word_listener('abc', free)
|
||||||
|
keyboard.remove_word_listener('abc')
|
||||||
|
self.do(du_a+du_b+du_c+du_space)
|
||||||
|
with self.assertRaises(keyboard._queue.Empty):
|
||||||
|
queue.get(timeout=0.01)
|
||||||
|
def test_add_word_listener_suffix_success(self):
|
||||||
|
queue = keyboard._queue.Queue()
|
||||||
|
def free():
|
||||||
|
queue.put(1)
|
||||||
|
keyboard.add_word_listener('abc', free, match_suffix=True)
|
||||||
|
self.do(du_a+du_a+du_b+du_c+du_space)
|
||||||
|
self.assertTrue(queue.get(timeout=0.5))
|
||||||
|
def test_add_word_listener_suffix_fail(self):
|
||||||
|
queue = keyboard._queue.Queue()
|
||||||
|
def free():
|
||||||
|
queue.put(1)
|
||||||
|
keyboard.add_word_listener('abc', free)
|
||||||
|
self.do(du_a+du_a+du_b+du_c)
|
||||||
|
with self.assertRaises(keyboard._queue.Empty):
|
||||||
|
queue.get(timeout=0.01)
|
||||||
|
|
||||||
|
#def test_add_abbreviation(self):
|
||||||
|
# keyboard.add_abbreviation('abc', 'aaa')
|
||||||
|
# self.do(du_a+du_b+du_c+du_space, [])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
LEFT = 'left'
|
||||||
|
RIGHT = 'right'
|
||||||
|
MIDDLE = 'middle'
|
||||||
|
WHEEL = 'wheel'
|
||||||
|
X = 'x'
|
||||||
|
X2 = 'x2'
|
||||||
|
|
||||||
|
UP = 'up'
|
||||||
|
DOWN = 'down'
|
||||||
|
DOUBLE = 'double'
|
||||||
|
VERTICAL = 'vertical'
|
||||||
|
HORIZONTAL = 'horizontal'
|
||||||
|
|
||||||
|
|
||||||
|
ButtonEvent = namedtuple('ButtonEvent', ['event_type', 'button', 'time'])
|
||||||
|
WheelEvent = namedtuple('WheelEvent', ['delta', 'time'])
|
||||||
|
MoveEvent = namedtuple('MoveEvent', ['x', 'y', 'time'])
|
|
@ -0,0 +1,271 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import unittest
|
||||||
|
import time
|
||||||
|
|
||||||
|
from ._mouse_event import MoveEvent, ButtonEvent, WheelEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN, DOUBLE
|
||||||
|
from keyboard import mouse
|
||||||
|
|
||||||
|
class FakeOsMouse(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.append = None
|
||||||
|
self.position = (0, 0)
|
||||||
|
self.queue = None
|
||||||
|
self.init = lambda: None
|
||||||
|
|
||||||
|
def listen(self, queue):
|
||||||
|
self.listening = True
|
||||||
|
self.queue = queue
|
||||||
|
|
||||||
|
def press(self, button):
|
||||||
|
self.append((DOWN, button))
|
||||||
|
|
||||||
|
def release(self, button):
|
||||||
|
self.append((UP, button))
|
||||||
|
|
||||||
|
def get_position(self):
|
||||||
|
return self.position
|
||||||
|
|
||||||
|
def move_to(self, x, y):
|
||||||
|
self.append(('move', (x, y)))
|
||||||
|
self.position = (x, y)
|
||||||
|
|
||||||
|
def wheel(self, delta):
|
||||||
|
self.append(('wheel', delta))
|
||||||
|
|
||||||
|
def move_relative(self, x, y):
|
||||||
|
self.position = (self.position[0] + x, self.position[1] + y)
|
||||||
|
|
||||||
|
class TestMouse(unittest.TestCase):
|
||||||
|
@staticmethod
|
||||||
|
def setUpClass():
|
||||||
|
mouse._os_mouse= FakeOsMouse()
|
||||||
|
mouse._listener.start_if_necessary()
|
||||||
|
assert mouse._os_mouse.listening
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.events = []
|
||||||
|
mouse._pressed_events.clear()
|
||||||
|
mouse._os_mouse.append = self.events.append
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
mouse.unhook_all()
|
||||||
|
# Make sure there's no spill over between tests.
|
||||||
|
self.wait_for_events_queue()
|
||||||
|
|
||||||
|
def wait_for_events_queue(self):
|
||||||
|
mouse._listener.queue.join()
|
||||||
|
|
||||||
|
def flush_events(self):
|
||||||
|
self.wait_for_events_queue()
|
||||||
|
events = list(self.events)
|
||||||
|
# Ugly, but requried to work in Python2. Python3 has list.clear
|
||||||
|
del self.events[:]
|
||||||
|
return events
|
||||||
|
|
||||||
|
def press(self, button=LEFT):
|
||||||
|
mouse._os_mouse.queue.put(ButtonEvent(DOWN, button, time.time()))
|
||||||
|
self.wait_for_events_queue()
|
||||||
|
|
||||||
|
def release(self, button=LEFT):
|
||||||
|
mouse._os_mouse.queue.put(ButtonEvent(UP, button, time.time()))
|
||||||
|
self.wait_for_events_queue()
|
||||||
|
|
||||||
|
def double_click(self, button=LEFT):
|
||||||
|
mouse._os_mouse.queue.put(ButtonEvent(DOUBLE, button, time.time()))
|
||||||
|
self.wait_for_events_queue()
|
||||||
|
|
||||||
|
def click(self, button=LEFT):
|
||||||
|
self.press(button)
|
||||||
|
self.release(button)
|
||||||
|
|
||||||
|
def wheel(self, delta=1):
|
||||||
|
mouse._os_mouse.queue.put(WheelEvent(delta, time.time()))
|
||||||
|
self.wait_for_events_queue()
|
||||||
|
|
||||||
|
def move(self, x=0, y=0):
|
||||||
|
mouse._os_mouse.queue.put(MoveEvent(x, y, time.time()))
|
||||||
|
self.wait_for_events_queue()
|
||||||
|
|
||||||
|
def test_hook(self):
|
||||||
|
events = []
|
||||||
|
self.press()
|
||||||
|
mouse.hook(events.append)
|
||||||
|
self.press()
|
||||||
|
mouse.unhook(events.append)
|
||||||
|
self.press()
|
||||||
|
self.assertEqual(len(events), 1)
|
||||||
|
|
||||||
|
def test_is_pressed(self):
|
||||||
|
self.assertFalse(mouse.is_pressed())
|
||||||
|
self.press()
|
||||||
|
self.assertTrue(mouse.is_pressed())
|
||||||
|
self.release()
|
||||||
|
self.press(X2)
|
||||||
|
self.assertFalse(mouse.is_pressed())
|
||||||
|
|
||||||
|
self.assertTrue(mouse.is_pressed(X2))
|
||||||
|
self.press(X2)
|
||||||
|
self.assertTrue(mouse.is_pressed(X2))
|
||||||
|
self.release(X2)
|
||||||
|
self.release(X2)
|
||||||
|
self.assertFalse(mouse.is_pressed(X2))
|
||||||
|
|
||||||
|
def test_buttons(self):
|
||||||
|
mouse.press()
|
||||||
|
self.assertEqual(self.flush_events(), [(DOWN, LEFT)])
|
||||||
|
mouse.release()
|
||||||
|
self.assertEqual(self.flush_events(), [(UP, LEFT)])
|
||||||
|
mouse.click()
|
||||||
|
self.assertEqual(self.flush_events(), [(DOWN, LEFT), (UP, LEFT)])
|
||||||
|
mouse.double_click()
|
||||||
|
self.assertEqual(self.flush_events(), [(DOWN, LEFT), (UP, LEFT), (DOWN, LEFT), (UP, LEFT)])
|
||||||
|
mouse.right_click()
|
||||||
|
self.assertEqual(self.flush_events(), [(DOWN, RIGHT), (UP, RIGHT)])
|
||||||
|
mouse.click(RIGHT)
|
||||||
|
self.assertEqual(self.flush_events(), [(DOWN, RIGHT), (UP, RIGHT)])
|
||||||
|
mouse.press(X2)
|
||||||
|
self.assertEqual(self.flush_events(), [(DOWN, X2)])
|
||||||
|
|
||||||
|
def test_position(self):
|
||||||
|
self.assertEqual(mouse.get_position(), mouse._os_mouse.get_position())
|
||||||
|
|
||||||
|
def test_move(self):
|
||||||
|
mouse.move(0, 0)
|
||||||
|
self.assertEqual(mouse._os_mouse.get_position(), (0, 0))
|
||||||
|
mouse.move(100, 500)
|
||||||
|
self.assertEqual(mouse._os_mouse.get_position(), (100, 500))
|
||||||
|
mouse.move(1, 2, False)
|
||||||
|
self.assertEqual(mouse._os_mouse.get_position(), (101, 502))
|
||||||
|
|
||||||
|
mouse.move(0, 0)
|
||||||
|
mouse.move(100, 499, True, duration=0.01)
|
||||||
|
self.assertEqual(mouse._os_mouse.get_position(), (100, 499))
|
||||||
|
mouse.move(100, 1, False, duration=0.01)
|
||||||
|
self.assertEqual(mouse._os_mouse.get_position(), (200, 500))
|
||||||
|
mouse.move(0, 0, False, duration=0.01)
|
||||||
|
self.assertEqual(mouse._os_mouse.get_position(), (200, 500))
|
||||||
|
|
||||||
|
def triggers(self, fn, events, **kwargs):
|
||||||
|
self.triggered = False
|
||||||
|
def callback():
|
||||||
|
self.triggered = True
|
||||||
|
handler = fn(callback, **kwargs)
|
||||||
|
|
||||||
|
for event_type, arg in events:
|
||||||
|
if event_type == DOWN:
|
||||||
|
self.press(arg)
|
||||||
|
elif event_type == UP:
|
||||||
|
self.release(arg)
|
||||||
|
elif event_type == DOUBLE:
|
||||||
|
self.double_click(arg)
|
||||||
|
elif event_type == 'WHEEL':
|
||||||
|
self.wheel()
|
||||||
|
|
||||||
|
mouse._listener.remove_handler(handler)
|
||||||
|
return self.triggered
|
||||||
|
|
||||||
|
def test_on_button(self):
|
||||||
|
self.assertTrue(self.triggers(mouse.on_button, [(DOWN, LEFT)]))
|
||||||
|
self.assertTrue(self.triggers(mouse.on_button, [(DOWN, RIGHT)]))
|
||||||
|
self.assertTrue(self.triggers(mouse.on_button, [(DOWN, X)]))
|
||||||
|
|
||||||
|
self.assertFalse(self.triggers(mouse.on_button, [('WHEEL', '')]))
|
||||||
|
|
||||||
|
self.assertFalse(self.triggers(mouse.on_button, [(DOWN, X)], buttons=MIDDLE))
|
||||||
|
self.assertTrue(self.triggers(mouse.on_button, [(DOWN, MIDDLE)], buttons=MIDDLE))
|
||||||
|
self.assertTrue(self.triggers(mouse.on_button, [(DOWN, MIDDLE)], buttons=MIDDLE))
|
||||||
|
self.assertFalse(self.triggers(mouse.on_button, [(DOWN, MIDDLE)], buttons=MIDDLE, types=UP))
|
||||||
|
self.assertTrue(self.triggers(mouse.on_button, [(UP, MIDDLE)], buttons=MIDDLE, types=UP))
|
||||||
|
|
||||||
|
self.assertTrue(self.triggers(mouse.on_button, [(UP, MIDDLE)], buttons=[MIDDLE, LEFT], types=[UP, DOWN]))
|
||||||
|
self.assertTrue(self.triggers(mouse.on_button, [(DOWN, LEFT)], buttons=[MIDDLE, LEFT], types=[UP, DOWN]))
|
||||||
|
self.assertFalse(self.triggers(mouse.on_button, [(UP, X)], buttons=[MIDDLE, LEFT], types=[UP, DOWN]))
|
||||||
|
|
||||||
|
def test_ons(self):
|
||||||
|
self.assertTrue(self.triggers(mouse.on_click, [(UP, LEFT)]))
|
||||||
|
self.assertFalse(self.triggers(mouse.on_click, [(UP, RIGHT)]))
|
||||||
|
self.assertFalse(self.triggers(mouse.on_click, [(DOWN, LEFT)]))
|
||||||
|
self.assertFalse(self.triggers(mouse.on_click, [(DOWN, RIGHT)]))
|
||||||
|
|
||||||
|
self.assertTrue(self.triggers(mouse.on_double_click, [(DOUBLE, LEFT)]))
|
||||||
|
self.assertFalse(self.triggers(mouse.on_double_click, [(DOUBLE, RIGHT)]))
|
||||||
|
self.assertFalse(self.triggers(mouse.on_double_click, [(DOWN, RIGHT)]))
|
||||||
|
|
||||||
|
self.assertTrue(self.triggers(mouse.on_right_click, [(UP, RIGHT)]))
|
||||||
|
self.assertTrue(self.triggers(mouse.on_middle_click, [(UP, MIDDLE)]))
|
||||||
|
|
||||||
|
def test_wait(self):
|
||||||
|
# If this fails it blocks. Unfortunately, but I see no other way of testing.
|
||||||
|
from threading import Thread, Lock
|
||||||
|
lock = Lock()
|
||||||
|
lock.acquire()
|
||||||
|
def t():
|
||||||
|
mouse.wait()
|
||||||
|
lock.release()
|
||||||
|
Thread(target=t).start()
|
||||||
|
self.press()
|
||||||
|
lock.acquire()
|
||||||
|
|
||||||
|
def test_record_play(self):
|
||||||
|
from threading import Thread, Lock
|
||||||
|
lock = Lock()
|
||||||
|
lock.acquire()
|
||||||
|
def t():
|
||||||
|
self.recorded = mouse.record(RIGHT)
|
||||||
|
lock.release()
|
||||||
|
Thread(target=t).start()
|
||||||
|
self.click()
|
||||||
|
self.wheel(5)
|
||||||
|
self.move(100, 50)
|
||||||
|
self.press(RIGHT)
|
||||||
|
lock.acquire()
|
||||||
|
|
||||||
|
self.assertEqual(len(self.recorded), 5)
|
||||||
|
self.assertEqual(self.recorded[0]._replace(time=None), ButtonEvent(DOWN, LEFT, None))
|
||||||
|
self.assertEqual(self.recorded[1]._replace(time=None), ButtonEvent(UP, LEFT, None))
|
||||||
|
self.assertEqual(self.recorded[2]._replace(time=None), WheelEvent(5, None))
|
||||||
|
self.assertEqual(self.recorded[3]._replace(time=None), MoveEvent(100, 50, None))
|
||||||
|
self.assertEqual(self.recorded[4]._replace(time=None), ButtonEvent(DOWN, RIGHT, None))
|
||||||
|
|
||||||
|
mouse.play(self.recorded, speed_factor=0)
|
||||||
|
events = self.flush_events()
|
||||||
|
self.assertEqual(len(events), 5)
|
||||||
|
self.assertEqual(events[0], (DOWN, LEFT))
|
||||||
|
self.assertEqual(events[1], (UP, LEFT))
|
||||||
|
self.assertEqual(events[2], ('wheel', 5))
|
||||||
|
self.assertEqual(events[3], ('move', (100, 50)))
|
||||||
|
self.assertEqual(events[4], (DOWN, RIGHT))
|
||||||
|
|
||||||
|
mouse.play(self.recorded)
|
||||||
|
events = self.flush_events()
|
||||||
|
self.assertEqual(len(events), 5)
|
||||||
|
self.assertEqual(events[0], (DOWN, LEFT))
|
||||||
|
self.assertEqual(events[1], (UP, LEFT))
|
||||||
|
self.assertEqual(events[2], ('wheel', 5))
|
||||||
|
self.assertEqual(events[3], ('move', (100, 50)))
|
||||||
|
self.assertEqual(events[4], (DOWN, RIGHT))
|
||||||
|
|
||||||
|
mouse.play(self.recorded, include_clicks=False)
|
||||||
|
events = self.flush_events()
|
||||||
|
self.assertEqual(len(events), 2)
|
||||||
|
self.assertEqual(events[0], ('wheel', 5))
|
||||||
|
self.assertEqual(events[1], ('move', (100, 50)))
|
||||||
|
|
||||||
|
mouse.play(self.recorded, include_moves=False)
|
||||||
|
events = self.flush_events()
|
||||||
|
self.assertEqual(len(events), 4)
|
||||||
|
self.assertEqual(events[0], (DOWN, LEFT))
|
||||||
|
self.assertEqual(events[1], (UP, LEFT))
|
||||||
|
self.assertEqual(events[2], ('wheel', 5))
|
||||||
|
self.assertEqual(events[3], (DOWN, RIGHT))
|
||||||
|
|
||||||
|
mouse.play(self.recorded, include_wheel=False)
|
||||||
|
events = self.flush_events()
|
||||||
|
self.assertEqual(len(events), 4)
|
||||||
|
self.assertEqual(events[0], (DOWN, LEFT))
|
||||||
|
self.assertEqual(events[1], (UP, LEFT))
|
||||||
|
self.assertEqual(events[2], ('move', (100, 50)))
|
||||||
|
self.assertEqual(events[3], (DOWN, RIGHT))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
|
@ -0,0 +1,174 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import struct
|
||||||
|
import os
|
||||||
|
import atexit
|
||||||
|
from time import time as now
|
||||||
|
from threading import Thread
|
||||||
|
from glob import glob
|
||||||
|
try:
|
||||||
|
from queue import Queue
|
||||||
|
except ImportError:
|
||||||
|
from Queue import Queue
|
||||||
|
|
||||||
|
event_bin_format = 'llHHI'
|
||||||
|
|
||||||
|
# Taken from include/linux/input.h
|
||||||
|
# https://www.kernel.org/doc/Documentation/input/event-codes.txt
|
||||||
|
EV_SYN = 0x00
|
||||||
|
EV_KEY = 0x01
|
||||||
|
EV_REL = 0x02
|
||||||
|
EV_ABS = 0x03
|
||||||
|
EV_MSC = 0x04
|
||||||
|
|
||||||
|
def make_uinput():
|
||||||
|
if not os.path.exists('/dev/uinput'):
|
||||||
|
raise IOError('No uinput module found.')
|
||||||
|
|
||||||
|
import fcntl, struct
|
||||||
|
|
||||||
|
# Requires uinput driver, but it's usually available.
|
||||||
|
uinput = open("/dev/uinput", 'wb')
|
||||||
|
UI_SET_EVBIT = 0x40045564
|
||||||
|
fcntl.ioctl(uinput, UI_SET_EVBIT, EV_KEY)
|
||||||
|
|
||||||
|
UI_SET_KEYBIT = 0x40045565
|
||||||
|
for i in range(256):
|
||||||
|
fcntl.ioctl(uinput, UI_SET_KEYBIT, i)
|
||||||
|
|
||||||
|
BUS_USB = 0x03
|
||||||
|
uinput_user_dev = "80sHHHHi64i64i64i64i"
|
||||||
|
axis = [0] * 64 * 4
|
||||||
|
uinput.write(struct.pack(uinput_user_dev, b"Virtual Keyboard", BUS_USB, 1, 1, 1, 0, *axis))
|
||||||
|
uinput.flush() # Without this you may get Errno 22: Invalid argument.
|
||||||
|
|
||||||
|
UI_DEV_CREATE = 0x5501
|
||||||
|
fcntl.ioctl(uinput, UI_DEV_CREATE)
|
||||||
|
UI_DEV_DESTROY = 0x5502
|
||||||
|
#fcntl.ioctl(uinput, UI_DEV_DESTROY)
|
||||||
|
|
||||||
|
return uinput
|
||||||
|
|
||||||
|
class EventDevice(object):
|
||||||
|
def __init__(self, path):
|
||||||
|
self.path = path
|
||||||
|
self._input_file = None
|
||||||
|
self._output_file = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def input_file(self):
|
||||||
|
if self._input_file is None:
|
||||||
|
try:
|
||||||
|
self._input_file = open(self.path, 'rb')
|
||||||
|
except IOError as e:
|
||||||
|
if e.strerror == 'Permission denied':
|
||||||
|
print('Permission denied ({}). You must be sudo to access global events.'.format(self.path))
|
||||||
|
exit()
|
||||||
|
|
||||||
|
def try_close():
|
||||||
|
try:
|
||||||
|
self._input_file.close
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
atexit.register(try_close)
|
||||||
|
return self._input_file
|
||||||
|
|
||||||
|
@property
|
||||||
|
def output_file(self):
|
||||||
|
if self._output_file is None:
|
||||||
|
self._output_file = open(self.path, 'wb')
|
||||||
|
atexit.register(self._output_file.close)
|
||||||
|
return self._output_file
|
||||||
|
|
||||||
|
def read_event(self):
|
||||||
|
data = self.input_file.read(struct.calcsize(event_bin_format))
|
||||||
|
seconds, microseconds, type, code, value = struct.unpack(event_bin_format, data)
|
||||||
|
return seconds + microseconds / 1e6, type, code, value, self.path
|
||||||
|
|
||||||
|
def write_event(self, type, code, value):
|
||||||
|
integer, fraction = divmod(now(), 1)
|
||||||
|
seconds = int(integer)
|
||||||
|
microseconds = int(fraction * 1e6)
|
||||||
|
data_event = struct.pack(event_bin_format, seconds, microseconds, type, code, value)
|
||||||
|
|
||||||
|
# Send a sync event to ensure other programs update.
|
||||||
|
sync_event = struct.pack(event_bin_format, seconds, microseconds, EV_SYN, 0, 0)
|
||||||
|
|
||||||
|
self.output_file.write(data_event + sync_event)
|
||||||
|
self.output_file.flush()
|
||||||
|
|
||||||
|
class AggregatedEventDevice(object):
|
||||||
|
def __init__(self, devices, output=None):
|
||||||
|
self.event_queue = Queue()
|
||||||
|
self.devices = devices
|
||||||
|
self.output = output or self.devices[0]
|
||||||
|
def start_reading(device):
|
||||||
|
while True:
|
||||||
|
self.event_queue.put(device.read_event())
|
||||||
|
for device in self.devices:
|
||||||
|
thread = Thread(target=start_reading, args=[device])
|
||||||
|
thread.setDaemon(True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def read_event(self):
|
||||||
|
return self.event_queue.get(block=True)
|
||||||
|
|
||||||
|
def write_event(self, type, code, value):
|
||||||
|
self.output.write_event(type, code, value)
|
||||||
|
|
||||||
|
import re
|
||||||
|
from collections import namedtuple
|
||||||
|
DeviceDescription = namedtuple('DeviceDescription', 'event_file is_mouse is_keyboard')
|
||||||
|
device_pattern = r"""N: Name="([^"]+?)".+?H: Handlers=([^\n]+)"""
|
||||||
|
def list_devices_from_proc(type_name):
|
||||||
|
try:
|
||||||
|
with open('/proc/bus/input/devices') as f:
|
||||||
|
description = f.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
return
|
||||||
|
|
||||||
|
devices = {}
|
||||||
|
for name, handlers in re.findall(device_pattern, description, re.DOTALL):
|
||||||
|
path = '/dev/input/event' + re.search(r'event(\d+)', handlers).group(1)
|
||||||
|
if type_name in handlers:
|
||||||
|
yield EventDevice(path)
|
||||||
|
|
||||||
|
def list_devices_from_by_id(name_suffix, by_id=True):
|
||||||
|
for path in glob('/dev/input/{}/*-event-{}'.format('by-id' if by_id else 'by-path', name_suffix)):
|
||||||
|
yield EventDevice(path)
|
||||||
|
|
||||||
|
def aggregate_devices(type_name):
|
||||||
|
# Some systems have multiple keyboards with different range of allowed keys
|
||||||
|
# on each one, like a notebook with a "keyboard" device exclusive for the
|
||||||
|
# power button. Instead of figuring out which keyboard allows which key to
|
||||||
|
# send events, we create a fake device and send all events through there.
|
||||||
|
try:
|
||||||
|
uinput = make_uinput()
|
||||||
|
fake_device = EventDevice('uinput Fake Device')
|
||||||
|
fake_device._input_file = uinput
|
||||||
|
fake_device._output_file = uinput
|
||||||
|
except IOError as e:
|
||||||
|
import warnings
|
||||||
|
warnings.warn('Failed to create a device file using `uinput` module. Sending of events may be limited or unavailable depending on plugged-in devices.', stacklevel=2)
|
||||||
|
fake_device = None
|
||||||
|
|
||||||
|
# We don't aggregate devices from different sources to avoid
|
||||||
|
# duplicates.
|
||||||
|
|
||||||
|
devices_from_proc = list(list_devices_from_proc(type_name))
|
||||||
|
if devices_from_proc:
|
||||||
|
return AggregatedEventDevice(devices_from_proc, output=fake_device)
|
||||||
|
|
||||||
|
# breaks on mouse for virtualbox
|
||||||
|
# was getting /dev/input/by-id/usb-VirtualBox_USB_Tablet-event-mouse
|
||||||
|
devices_from_by_id = list(list_devices_from_by_id(type_name)) or list(list_devices_from_by_id(type_name, by_id=False))
|
||||||
|
if devices_from_by_id:
|
||||||
|
return AggregatedEventDevice(devices_from_by_id, output=fake_device)
|
||||||
|
|
||||||
|
# If no keyboards were found we can only use the fake device to send keys.
|
||||||
|
assert fake_device
|
||||||
|
return fake_device
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_root():
|
||||||
|
if os.geteuid() != 0:
|
||||||
|
raise ImportError('You must be root to use this library on linux.')
|
|
@ -0,0 +1,183 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import struct
|
||||||
|
import traceback
|
||||||
|
from time import time as now
|
||||||
|
from collections import namedtuple
|
||||||
|
from ._keyboard_event import KeyboardEvent, KEY_DOWN, KEY_UP
|
||||||
|
from ._canonical_names import all_modifiers, normalize_name
|
||||||
|
from ._nixcommon import EV_KEY, aggregate_devices, ensure_root
|
||||||
|
|
||||||
|
# TODO: start by reading current keyboard state, as to not missing any already pressed keys.
|
||||||
|
# See: http://stackoverflow.com/questions/3649874/how-to-get-keyboard-state-in-linux
|
||||||
|
|
||||||
|
def cleanup_key(name):
|
||||||
|
""" Formats a dumpkeys format to our standard. """
|
||||||
|
name = name.lstrip('+')
|
||||||
|
is_keypad = name.startswith('KP_')
|
||||||
|
for mod in ('Meta_', 'Control_', 'dead_', 'KP_'):
|
||||||
|
if name.startswith(mod):
|
||||||
|
name = name[len(mod):]
|
||||||
|
|
||||||
|
# Dumpkeys is weird like that.
|
||||||
|
if name == 'Remove':
|
||||||
|
name = 'Delete'
|
||||||
|
elif name == 'Delete':
|
||||||
|
name = 'Backspace'
|
||||||
|
|
||||||
|
if name.endswith('_r'):
|
||||||
|
name = 'right ' + name[:-2]
|
||||||
|
if name.endswith('_l'):
|
||||||
|
name = 'left ' + name[:-2]
|
||||||
|
|
||||||
|
|
||||||
|
return normalize_name(name), is_keypad
|
||||||
|
|
||||||
|
def cleanup_modifier(modifier):
|
||||||
|
modifier = normalize_name(modifier)
|
||||||
|
if modifier in all_modifiers:
|
||||||
|
return modifier
|
||||||
|
if modifier[:-1] in all_modifiers:
|
||||||
|
return modifier[:-1]
|
||||||
|
raise ValueError('Unknown modifier {}'.format(modifier))
|
||||||
|
|
||||||
|
"""
|
||||||
|
Use `dumpkeys --keys-only` to list all scan codes and their names. We
|
||||||
|
then parse the output and built a table. For each scan code and modifiers we
|
||||||
|
have a list of names and vice-versa.
|
||||||
|
"""
|
||||||
|
from subprocess import check_output
|
||||||
|
from collections import defaultdict
|
||||||
|
import re
|
||||||
|
|
||||||
|
to_name = defaultdict(list)
|
||||||
|
from_name = defaultdict(list)
|
||||||
|
keypad_scan_codes = set()
|
||||||
|
|
||||||
|
def register_key(key_and_modifiers, name):
|
||||||
|
if name not in to_name[key_and_modifiers]:
|
||||||
|
to_name[key_and_modifiers].append(name)
|
||||||
|
if key_and_modifiers not in from_name[name]:
|
||||||
|
from_name[name].append(key_and_modifiers)
|
||||||
|
|
||||||
|
def build_tables():
|
||||||
|
if to_name and from_name: return
|
||||||
|
ensure_root()
|
||||||
|
|
||||||
|
modifiers_bits = {
|
||||||
|
'shift': 1,
|
||||||
|
'alt gr': 2,
|
||||||
|
'ctrl': 4,
|
||||||
|
'alt': 8,
|
||||||
|
}
|
||||||
|
keycode_template = r'^keycode\s+(\d+)\s+=(.*?)$'
|
||||||
|
dump = check_output(['dumpkeys', '--keys-only'], universal_newlines=True)
|
||||||
|
for str_scan_code, str_names in re.findall(keycode_template, dump, re.MULTILINE):
|
||||||
|
scan_code = int(str_scan_code)
|
||||||
|
for i, str_name in enumerate(str_names.strip().split()):
|
||||||
|
modifiers = tuple(sorted(modifier for modifier, bit in modifiers_bits.items() if i & bit))
|
||||||
|
name, is_keypad = cleanup_key(str_name)
|
||||||
|
register_key((scan_code, modifiers), name)
|
||||||
|
if is_keypad:
|
||||||
|
keypad_scan_codes.add(scan_code)
|
||||||
|
register_key((scan_code, modifiers), 'keypad ' + name)
|
||||||
|
|
||||||
|
# dumpkeys consistently misreports the Windows key, sometimes
|
||||||
|
# skipping it completely or reporting as 'alt. 125 = left win,
|
||||||
|
# 126 = right win.
|
||||||
|
if (125, ()) not in to_name or to_name[(125, ())] == 'alt':
|
||||||
|
register_key((125, ()), 'windows')
|
||||||
|
if (126, ()) not in to_name or to_name[(126, ())] == 'alt':
|
||||||
|
register_key((126, ()), 'windows')
|
||||||
|
|
||||||
|
# The menu key is usually skipped altogether, so we also add it manually.
|
||||||
|
if (127, ()) not in to_name:
|
||||||
|
register_key((127, ()), 'menu')
|
||||||
|
|
||||||
|
synonyms_template = r'^(\S+)\s+for (.+)$'
|
||||||
|
dump = check_output(['dumpkeys', '--long-info'], universal_newlines=True)
|
||||||
|
for synonym_str, original_str in re.findall(synonyms_template, dump, re.MULTILINE):
|
||||||
|
synonym, _ = cleanup_key(synonym_str)
|
||||||
|
original, _ = cleanup_key(original_str)
|
||||||
|
if synonym != original:
|
||||||
|
from_name[original].extend(from_name[synonym])
|
||||||
|
from_name[synonym].extend(from_name[original])
|
||||||
|
|
||||||
|
device = None
|
||||||
|
def build_device():
|
||||||
|
global device
|
||||||
|
if device: return
|
||||||
|
ensure_root()
|
||||||
|
device = aggregate_devices('kbd')
|
||||||
|
|
||||||
|
def init():
|
||||||
|
build_device()
|
||||||
|
build_tables()
|
||||||
|
|
||||||
|
pressed_modifiers = set()
|
||||||
|
|
||||||
|
def listen(callback):
|
||||||
|
build_device()
|
||||||
|
build_tables()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
time, type, code, value, device_id = device.read_event()
|
||||||
|
if type != EV_KEY:
|
||||||
|
continue
|
||||||
|
|
||||||
|
scan_code = code
|
||||||
|
event_type = KEY_DOWN if value else KEY_UP # 0 = UP, 1 = DOWN, 2 = HOLD
|
||||||
|
|
||||||
|
pressed_modifiers_tuple = tuple(sorted(pressed_modifiers))
|
||||||
|
names = to_name[(scan_code, pressed_modifiers_tuple)] or to_name[(scan_code, ())] or ['unknown']
|
||||||
|
name = names[0]
|
||||||
|
|
||||||
|
if name in all_modifiers:
|
||||||
|
if event_type == KEY_DOWN:
|
||||||
|
pressed_modifiers.add(name)
|
||||||
|
else:
|
||||||
|
pressed_modifiers.discard(name)
|
||||||
|
|
||||||
|
is_keypad = scan_code in keypad_scan_codes
|
||||||
|
callback(KeyboardEvent(event_type=event_type, scan_code=scan_code, name=name, time=time, device=device_id, is_keypad=is_keypad, modifiers=pressed_modifiers_tuple))
|
||||||
|
|
||||||
|
def write_event(scan_code, is_down):
|
||||||
|
build_device()
|
||||||
|
device.write_event(EV_KEY, scan_code, int(is_down))
|
||||||
|
|
||||||
|
def map_name(name):
|
||||||
|
build_tables()
|
||||||
|
for entry in from_name[name]:
|
||||||
|
yield entry
|
||||||
|
|
||||||
|
parts = name.split(' ', 1)
|
||||||
|
if len(parts) > 1 and parts[0] in ('left', 'right'):
|
||||||
|
for entry in from_name[parts[1]]:
|
||||||
|
yield entry
|
||||||
|
|
||||||
|
def press(scan_code):
|
||||||
|
write_event(scan_code, True)
|
||||||
|
|
||||||
|
def release(scan_code):
|
||||||
|
write_event(scan_code, False)
|
||||||
|
|
||||||
|
def type_unicode(character):
|
||||||
|
codepoint = ord(character)
|
||||||
|
hexadecimal = hex(codepoint)[len('0x'):]
|
||||||
|
|
||||||
|
for key in ['ctrl', 'shift', 'u']:
|
||||||
|
scan_code, _ = next(map_name(key))
|
||||||
|
press(scan_code)
|
||||||
|
|
||||||
|
for key in hexadecimal:
|
||||||
|
scan_code, _ = next(map_name(key))
|
||||||
|
press(scan_code)
|
||||||
|
release(scan_code)
|
||||||
|
|
||||||
|
for key in ['ctrl', 'shift', 'u']:
|
||||||
|
scan_code, _ = next(map_name(key))
|
||||||
|
release(scan_code)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
def p(e):
|
||||||
|
print(e)
|
||||||
|
listen(p)
|
|
@ -0,0 +1,130 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import struct
|
||||||
|
from subprocess import check_output
|
||||||
|
import re
|
||||||
|
from ._nixcommon import EV_KEY, EV_REL, EV_MSC, EV_SYN, EV_ABS, aggregate_devices, ensure_root
|
||||||
|
from ._mouse_event import ButtonEvent, WheelEvent, MoveEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
import ctypes.util
|
||||||
|
from ctypes import c_uint32, c_uint, c_int, byref
|
||||||
|
|
||||||
|
display = None
|
||||||
|
window = None
|
||||||
|
x11 = None
|
||||||
|
def build_display():
|
||||||
|
global display, window, x11
|
||||||
|
if display and window and x11: return
|
||||||
|
x11 = ctypes.cdll.LoadLibrary(ctypes.util.find_library('X11'))
|
||||||
|
# Required because we will have multiple threads calling x11,
|
||||||
|
# such as the listener thread and then main using "move_to".
|
||||||
|
x11.XInitThreads()
|
||||||
|
display = x11.XOpenDisplay(None)
|
||||||
|
# Known to cause segfault in Fedora 23 64bits, no known workarounds.
|
||||||
|
# http://stackoverflow.com/questions/35137007/get-mouse-position-on-linux-pure-python
|
||||||
|
window = x11.XDefaultRootWindow(display)
|
||||||
|
|
||||||
|
def get_position():
|
||||||
|
build_display()
|
||||||
|
root_id, child_id = c_uint32(), c_uint32()
|
||||||
|
root_x, root_y, win_x, win_y = c_int(), c_int(), c_int(), c_int()
|
||||||
|
mask = c_uint()
|
||||||
|
ret = x11.XQueryPointer(display, c_uint32(window), byref(root_id), byref(child_id),
|
||||||
|
byref(root_x), byref(root_y),
|
||||||
|
byref(win_x), byref(win_y), byref(mask))
|
||||||
|
return root_x.value, root_y.value
|
||||||
|
|
||||||
|
def move_to(x, y):
|
||||||
|
build_display()
|
||||||
|
x11.XWarpPointer(display, None, window, 0, 0, 0, 0, x, y)
|
||||||
|
x11.XFlush(display)
|
||||||
|
|
||||||
|
REL_X = 0x00
|
||||||
|
REL_Y = 0x01
|
||||||
|
REL_Z = 0x02
|
||||||
|
REL_HWHEEL = 0x06
|
||||||
|
REL_WHEEL = 0x08
|
||||||
|
|
||||||
|
ABS_X = 0x00
|
||||||
|
ABS_Y = 0x01
|
||||||
|
|
||||||
|
BTN_MOUSE = 0x110
|
||||||
|
BTN_LEFT = 0x110
|
||||||
|
BTN_RIGHT = 0x111
|
||||||
|
BTN_MIDDLE = 0x112
|
||||||
|
BTN_SIDE = 0x113
|
||||||
|
BTN_EXTRA = 0x114
|
||||||
|
|
||||||
|
button_by_code = {
|
||||||
|
BTN_LEFT: LEFT,
|
||||||
|
BTN_RIGHT: RIGHT,
|
||||||
|
BTN_MIDDLE: MIDDLE,
|
||||||
|
BTN_SIDE: X,
|
||||||
|
BTN_EXTRA: X2,
|
||||||
|
}
|
||||||
|
code_by_button = {button: code for code, button in button_by_code.items()}
|
||||||
|
|
||||||
|
device = None
|
||||||
|
def build_device():
|
||||||
|
global device
|
||||||
|
if device: return
|
||||||
|
ensure_root()
|
||||||
|
device = aggregate_devices('mouse')
|
||||||
|
init = build_device
|
||||||
|
|
||||||
|
def listen(queue):
|
||||||
|
build_device()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
time, type, code, value, device_id = device.read_event()
|
||||||
|
if type == EV_SYN or type == EV_MSC:
|
||||||
|
continue
|
||||||
|
|
||||||
|
event = None
|
||||||
|
arg = None
|
||||||
|
|
||||||
|
if type == EV_KEY:
|
||||||
|
event = ButtonEvent(DOWN if value else UP, button_by_code.get(code, '?'), time)
|
||||||
|
elif type == EV_REL:
|
||||||
|
value, = struct.unpack('i', struct.pack('I', value))
|
||||||
|
|
||||||
|
if code == REL_WHEEL:
|
||||||
|
event = WheelEvent(value, time)
|
||||||
|
elif code in (REL_X, REL_Y):
|
||||||
|
x, y = get_position()
|
||||||
|
event = MoveEvent(x, y, time)
|
||||||
|
|
||||||
|
if event is None:
|
||||||
|
# Unknown event type.
|
||||||
|
continue
|
||||||
|
|
||||||
|
queue.put(event)
|
||||||
|
|
||||||
|
def press(button=LEFT):
|
||||||
|
build_device()
|
||||||
|
device.write_event(EV_KEY, code_by_button[button], 0x01)
|
||||||
|
|
||||||
|
def release(button=LEFT):
|
||||||
|
build_device()
|
||||||
|
device.write_event(EV_KEY, code_by_button[button], 0x00)
|
||||||
|
|
||||||
|
def move_relative(x, y):
|
||||||
|
build_device()
|
||||||
|
# Note relative events are not in terms of pixels, but millimeters.
|
||||||
|
if x < 0:
|
||||||
|
x += 2**32
|
||||||
|
if y < 0:
|
||||||
|
y += 2**32
|
||||||
|
device.write_event(EV_REL, REL_X, x)
|
||||||
|
device.write_event(EV_REL, REL_Y, y)
|
||||||
|
|
||||||
|
def wheel(delta=1):
|
||||||
|
build_device()
|
||||||
|
if delta < 0:
|
||||||
|
delta += 2**32
|
||||||
|
device.write_event(EV_REL, REL_WHEEL, delta)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
#listen(print)
|
||||||
|
move_to(100, 200)
|
|
@ -0,0 +1,620 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This is the Windows backend for keyboard events, and is implemented by
|
||||||
|
invoking the Win32 API through the ctypes module. This is error prone
|
||||||
|
and can introduce very unpythonic failure modes, such as segfaults and
|
||||||
|
low level memory leaks. But it is also dependency-free, very performant
|
||||||
|
well documented on Microsoft's webstie and scattered examples.
|
||||||
|
|
||||||
|
# TODO:
|
||||||
|
- Keypad numbers still print as numbers even when numlock is off.
|
||||||
|
- No way to specify if user wants a keypad key or not in `map_char`.
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import re
|
||||||
|
import atexit
|
||||||
|
import traceback
|
||||||
|
from threading import Lock
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from ._keyboard_event import KeyboardEvent, KEY_DOWN, KEY_UP
|
||||||
|
from ._canonical_names import normalize_name
|
||||||
|
try:
|
||||||
|
# Force Python2 to convert to unicode and not to str.
|
||||||
|
chr = unichr
|
||||||
|
except NameError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# This part is just declaring Win32 API structures using ctypes. In C
|
||||||
|
# this would be simply #include "windows.h".
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
from ctypes import c_short, c_char, c_uint8, c_int32, c_int, c_uint, c_uint32, c_long, Structure, CFUNCTYPE, POINTER
|
||||||
|
from ctypes.wintypes import WORD, DWORD, BOOL, HHOOK, MSG, LPWSTR, WCHAR, WPARAM, LPARAM, LONG, HMODULE, LPCWSTR, HINSTANCE, HWND
|
||||||
|
LPMSG = POINTER(MSG)
|
||||||
|
ULONG_PTR = POINTER(DWORD)
|
||||||
|
|
||||||
|
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
|
||||||
|
GetModuleHandleW = kernel32.GetModuleHandleW
|
||||||
|
GetModuleHandleW.restype = HMODULE
|
||||||
|
GetModuleHandleW.argtypes = [LPCWSTR]
|
||||||
|
|
||||||
|
#https://github.com/boppreh/mouse/issues/1
|
||||||
|
#user32 = ctypes.windll.user32
|
||||||
|
user32 = ctypes.WinDLL('user32', use_last_error = True)
|
||||||
|
|
||||||
|
VK_PACKET = 0xE7
|
||||||
|
|
||||||
|
INPUT_MOUSE = 0
|
||||||
|
INPUT_KEYBOARD = 1
|
||||||
|
INPUT_HARDWARE = 2
|
||||||
|
|
||||||
|
KEYEVENTF_KEYUP = 0x02
|
||||||
|
KEYEVENTF_UNICODE = 0x04
|
||||||
|
|
||||||
|
class KBDLLHOOKSTRUCT(Structure):
|
||||||
|
_fields_ = [("vk_code", DWORD),
|
||||||
|
("scan_code", DWORD),
|
||||||
|
("flags", DWORD),
|
||||||
|
("time", c_int),
|
||||||
|
("dwExtraInfo", ULONG_PTR)]
|
||||||
|
|
||||||
|
# Included for completeness.
|
||||||
|
class MOUSEINPUT(ctypes.Structure):
|
||||||
|
_fields_ = (('dx', LONG),
|
||||||
|
('dy', LONG),
|
||||||
|
('mouseData', DWORD),
|
||||||
|
('dwFlags', DWORD),
|
||||||
|
('time', DWORD),
|
||||||
|
('dwExtraInfo', ULONG_PTR))
|
||||||
|
|
||||||
|
class KEYBDINPUT(ctypes.Structure):
|
||||||
|
_fields_ = (('wVk', WORD),
|
||||||
|
('wScan', WORD),
|
||||||
|
('dwFlags', DWORD),
|
||||||
|
('time', DWORD),
|
||||||
|
('dwExtraInfo', ULONG_PTR))
|
||||||
|
|
||||||
|
class HARDWAREINPUT(ctypes.Structure):
|
||||||
|
_fields_ = (('uMsg', DWORD),
|
||||||
|
('wParamL', WORD),
|
||||||
|
('wParamH', WORD))
|
||||||
|
|
||||||
|
class _INPUTunion(ctypes.Union):
|
||||||
|
_fields_ = (('mi', MOUSEINPUT),
|
||||||
|
('ki', KEYBDINPUT),
|
||||||
|
('hi', HARDWAREINPUT))
|
||||||
|
|
||||||
|
class INPUT(ctypes.Structure):
|
||||||
|
_fields_ = (('type', DWORD),
|
||||||
|
('union', _INPUTunion))
|
||||||
|
|
||||||
|
LowLevelKeyboardProc = CFUNCTYPE(c_int, WPARAM, LPARAM, POINTER(KBDLLHOOKSTRUCT))
|
||||||
|
|
||||||
|
SetWindowsHookEx = user32.SetWindowsHookExW
|
||||||
|
SetWindowsHookEx.argtypes = [c_int, LowLevelKeyboardProc, HINSTANCE , DWORD]
|
||||||
|
SetWindowsHookEx.restype = HHOOK
|
||||||
|
|
||||||
|
CallNextHookEx = user32.CallNextHookEx
|
||||||
|
#CallNextHookEx.argtypes = [c_int , c_int, c_int, POINTER(KBDLLHOOKSTRUCT)]
|
||||||
|
CallNextHookEx.restype = c_int
|
||||||
|
|
||||||
|
UnhookWindowsHookEx = user32.UnhookWindowsHookEx
|
||||||
|
UnhookWindowsHookEx.argtypes = [HHOOK]
|
||||||
|
UnhookWindowsHookEx.restype = BOOL
|
||||||
|
|
||||||
|
GetMessage = user32.GetMessageW
|
||||||
|
GetMessage.argtypes = [LPMSG, HWND, c_uint, c_uint]
|
||||||
|
GetMessage.restype = BOOL
|
||||||
|
|
||||||
|
TranslateMessage = user32.TranslateMessage
|
||||||
|
TranslateMessage.argtypes = [LPMSG]
|
||||||
|
TranslateMessage.restype = BOOL
|
||||||
|
|
||||||
|
DispatchMessage = user32.DispatchMessageA
|
||||||
|
DispatchMessage.argtypes = [LPMSG]
|
||||||
|
|
||||||
|
|
||||||
|
keyboard_state_type = c_uint8 * 256
|
||||||
|
|
||||||
|
GetKeyboardState = user32.GetKeyboardState
|
||||||
|
GetKeyboardState.argtypes = [keyboard_state_type]
|
||||||
|
GetKeyboardState.restype = BOOL
|
||||||
|
|
||||||
|
GetKeyNameText = user32.GetKeyNameTextW
|
||||||
|
GetKeyNameText.argtypes = [c_long, LPWSTR, c_int]
|
||||||
|
GetKeyNameText.restype = c_int
|
||||||
|
|
||||||
|
MapVirtualKey = user32.MapVirtualKeyW
|
||||||
|
MapVirtualKey.argtypes = [c_uint, c_uint]
|
||||||
|
MapVirtualKey.restype = c_uint
|
||||||
|
|
||||||
|
ToUnicode = user32.ToUnicode
|
||||||
|
ToUnicode.argtypes = [c_uint, c_uint, keyboard_state_type, LPWSTR, c_int, c_uint]
|
||||||
|
ToUnicode.restype = c_int
|
||||||
|
|
||||||
|
SendInput = user32.SendInput
|
||||||
|
SendInput.argtypes = [c_uint, POINTER(INPUT), c_int]
|
||||||
|
SendInput.restype = c_uint
|
||||||
|
|
||||||
|
# https://msdn.microsoft.com/en-us/library/windows/desktop/ms646307(v=vs.85).aspx
|
||||||
|
MAPVK_VK_TO_CHAR = 2
|
||||||
|
MAPVK_VK_TO_VSC = 0
|
||||||
|
MAPVK_VSC_TO_VK = 1
|
||||||
|
MAPVK_VK_TO_VSC_EX = 4
|
||||||
|
MAPVK_VSC_TO_VK_EX = 3
|
||||||
|
|
||||||
|
VkKeyScan = user32.VkKeyScanW
|
||||||
|
VkKeyScan.argtypes = [WCHAR]
|
||||||
|
VkKeyScan.restype = c_short
|
||||||
|
|
||||||
|
LLKHF_INJECTED = 0x00000010
|
||||||
|
|
||||||
|
WM_KEYDOWN = 0x0100
|
||||||
|
WM_KEYUP = 0x0101
|
||||||
|
WM_SYSKEYDOWN = 0x104 # Used for ALT key
|
||||||
|
WM_SYSKEYUP = 0x105
|
||||||
|
|
||||||
|
|
||||||
|
# This marks the end of Win32 API declarations. The rest is ours.
|
||||||
|
|
||||||
|
keyboard_event_types = {
|
||||||
|
WM_KEYDOWN: KEY_DOWN,
|
||||||
|
WM_KEYUP: KEY_UP,
|
||||||
|
WM_SYSKEYDOWN: KEY_DOWN,
|
||||||
|
WM_SYSKEYUP: KEY_UP,
|
||||||
|
}
|
||||||
|
|
||||||
|
# List taken from the official documentation, but stripped of the OEM-specific keys.
|
||||||
|
# Keys are virtual key codes, values are pairs (name, is_keypad).
|
||||||
|
official_virtual_keys = {
|
||||||
|
0x03: ('control-break processing', False),
|
||||||
|
0x08: ('backspace', False),
|
||||||
|
0x09: ('tab', False),
|
||||||
|
0x0c: ('clear', False),
|
||||||
|
0x0d: ('enter', False),
|
||||||
|
0x10: ('shift', False),
|
||||||
|
0x11: ('ctrl', False),
|
||||||
|
0x12: ('alt', False),
|
||||||
|
0x13: ('pause', False),
|
||||||
|
0x14: ('caps lock', False),
|
||||||
|
0x15: ('ime kana mode', False),
|
||||||
|
0x15: ('ime hanguel mode', False),
|
||||||
|
0x15: ('ime hangul mode', False),
|
||||||
|
0x17: ('ime junja mode', False),
|
||||||
|
0x18: ('ime final mode', False),
|
||||||
|
0x19: ('ime hanja mode', False),
|
||||||
|
0x19: ('ime kanji mode', False),
|
||||||
|
0x1b: ('esc', False),
|
||||||
|
0x1c: ('ime convert', False),
|
||||||
|
0x1d: ('ime nonconvert', False),
|
||||||
|
0x1e: ('ime accept', False),
|
||||||
|
0x1f: ('ime mode change request', False),
|
||||||
|
0x20: ('spacebar', False),
|
||||||
|
0x21: ('page up', False),
|
||||||
|
0x22: ('page down', False),
|
||||||
|
0x23: ('end', False),
|
||||||
|
0x24: ('home', False),
|
||||||
|
0x25: ('left', False),
|
||||||
|
0x26: ('up', False),
|
||||||
|
0x27: ('right', False),
|
||||||
|
0x28: ('down', False),
|
||||||
|
0x29: ('select', False),
|
||||||
|
0x2a: ('print', False),
|
||||||
|
0x2b: ('execute', False),
|
||||||
|
0x2c: ('print screen', False),
|
||||||
|
0x2d: ('insert', False),
|
||||||
|
0x2e: ('delete', False),
|
||||||
|
0x2f: ('help', False),
|
||||||
|
0x30: ('0', False),
|
||||||
|
0x31: ('1', False),
|
||||||
|
0x32: ('2', False),
|
||||||
|
0x33: ('3', False),
|
||||||
|
0x34: ('4', False),
|
||||||
|
0x35: ('5', False),
|
||||||
|
0x36: ('6', False),
|
||||||
|
0x37: ('7', False),
|
||||||
|
0x38: ('8', False),
|
||||||
|
0x39: ('9', False),
|
||||||
|
0x41: ('a', False),
|
||||||
|
0x42: ('b', False),
|
||||||
|
0x43: ('c', False),
|
||||||
|
0x44: ('d', False),
|
||||||
|
0x45: ('e', False),
|
||||||
|
0x46: ('f', False),
|
||||||
|
0x47: ('g', False),
|
||||||
|
0x48: ('h', False),
|
||||||
|
0x49: ('i', False),
|
||||||
|
0x4a: ('j', False),
|
||||||
|
0x4b: ('k', False),
|
||||||
|
0x4c: ('l', False),
|
||||||
|
0x4d: ('m', False),
|
||||||
|
0x4e: ('n', False),
|
||||||
|
0x4f: ('o', False),
|
||||||
|
0x50: ('p', False),
|
||||||
|
0x51: ('q', False),
|
||||||
|
0x52: ('r', False),
|
||||||
|
0x53: ('s', False),
|
||||||
|
0x54: ('t', False),
|
||||||
|
0x55: ('u', False),
|
||||||
|
0x56: ('v', False),
|
||||||
|
0x57: ('w', False),
|
||||||
|
0x58: ('x', False),
|
||||||
|
0x59: ('y', False),
|
||||||
|
0x5a: ('z', False),
|
||||||
|
0x5b: ('left windows', False),
|
||||||
|
0x5c: ('right windows', False),
|
||||||
|
0x5d: ('applications', False),
|
||||||
|
0x5f: ('sleep', False),
|
||||||
|
0x60: ('0', True),
|
||||||
|
0x61: ('1', True),
|
||||||
|
0x62: ('2', True),
|
||||||
|
0x63: ('3', True),
|
||||||
|
0x64: ('4', True),
|
||||||
|
0x65: ('5', True),
|
||||||
|
0x66: ('6', True),
|
||||||
|
0x67: ('7', True),
|
||||||
|
0x68: ('8', True),
|
||||||
|
0x69: ('9', True),
|
||||||
|
0x6a: ('*', True),
|
||||||
|
0x6b: ('+', True),
|
||||||
|
0x6c: ('separator', True),
|
||||||
|
0x6d: ('-', True),
|
||||||
|
0x6e: ('decimal', True),
|
||||||
|
0x6f: ('/', True),
|
||||||
|
0x70: ('f1', False),
|
||||||
|
0x71: ('f2', False),
|
||||||
|
0x72: ('f3', False),
|
||||||
|
0x73: ('f4', False),
|
||||||
|
0x74: ('f5', False),
|
||||||
|
0x75: ('f6', False),
|
||||||
|
0x76: ('f7', False),
|
||||||
|
0x77: ('f8', False),
|
||||||
|
0x78: ('f9', False),
|
||||||
|
0x79: ('f10', False),
|
||||||
|
0x7a: ('f11', False),
|
||||||
|
0x7b: ('f12', False),
|
||||||
|
0x7c: ('f13', False),
|
||||||
|
0x7d: ('f14', False),
|
||||||
|
0x7e: ('f15', False),
|
||||||
|
0x7f: ('f16', False),
|
||||||
|
0x80: ('f17', False),
|
||||||
|
0x81: ('f18', False),
|
||||||
|
0x82: ('f19', False),
|
||||||
|
0x83: ('f20', False),
|
||||||
|
0x84: ('f21', False),
|
||||||
|
0x85: ('f22', False),
|
||||||
|
0x86: ('f23', False),
|
||||||
|
0x87: ('f24', False),
|
||||||
|
0x90: ('num lock', False),
|
||||||
|
0x91: ('scroll lock', False),
|
||||||
|
0xa0: ('left shift', False),
|
||||||
|
0xa1: ('right shift', False),
|
||||||
|
0xa2: ('left ctrl', False),
|
||||||
|
0xa3: ('right ctrl', False),
|
||||||
|
0xa4: ('left menu', False),
|
||||||
|
0xa5: ('right menu', False),
|
||||||
|
0xa6: ('browser back', False),
|
||||||
|
0xa7: ('browser forward', False),
|
||||||
|
0xa8: ('browser refresh', False),
|
||||||
|
0xa9: ('browser stop', False),
|
||||||
|
0xaa: ('browser search key', False),
|
||||||
|
0xab: ('browser favorites', False),
|
||||||
|
0xac: ('browser start and home', False),
|
||||||
|
0xad: ('volume mute', False),
|
||||||
|
0xae: ('volume down', False),
|
||||||
|
0xaf: ('volume up', False),
|
||||||
|
0xb0: ('next track', False),
|
||||||
|
0xb1: ('previous track', False),
|
||||||
|
0xb2: ('stop media', False),
|
||||||
|
0xb3: ('play/pause media', False),
|
||||||
|
0xb4: ('start mail', False),
|
||||||
|
0xb5: ('select media', False),
|
||||||
|
0xb6: ('start application 1', False),
|
||||||
|
0xb7: ('start application 2', False),
|
||||||
|
0xbb: ('+', False),
|
||||||
|
0xbc: (',', False),
|
||||||
|
0xbd: ('-', False),
|
||||||
|
0xbe: ('.', False),
|
||||||
|
#0xbe:('/', False), # Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '/?.
|
||||||
|
0xe5: ('ime process', False),
|
||||||
|
0xf6: ('attn', False),
|
||||||
|
0xf7: ('crsel', False),
|
||||||
|
0xf8: ('exsel', False),
|
||||||
|
0xf9: ('erase eof', False),
|
||||||
|
0xfa: ('play', False),
|
||||||
|
0xfb: ('zoom', False),
|
||||||
|
0xfc: ('reserved ', False),
|
||||||
|
0xfd: ('pa1', False),
|
||||||
|
0xfe: ('clear', False),
|
||||||
|
}
|
||||||
|
|
||||||
|
tables_lock = Lock()
|
||||||
|
to_name = defaultdict(list)
|
||||||
|
from_name = defaultdict(list)
|
||||||
|
scan_code_to_vk = {}
|
||||||
|
|
||||||
|
distinct_modifiers = [
|
||||||
|
(),
|
||||||
|
('shift',),
|
||||||
|
('alt gr',),
|
||||||
|
('num lock',),
|
||||||
|
('shift', 'num lock'),
|
||||||
|
('caps lock',),
|
||||||
|
('shift', 'caps lock'),
|
||||||
|
('alt gr', 'num lock'),
|
||||||
|
]
|
||||||
|
|
||||||
|
name_buffer = ctypes.create_unicode_buffer(32)
|
||||||
|
unicode_buffer = ctypes.create_unicode_buffer(32)
|
||||||
|
keyboard_state = keyboard_state_type()
|
||||||
|
def get_event_names(scan_code, vk, is_extended, modifiers):
|
||||||
|
is_keypad = (scan_code, vk, is_extended) in keypad_keys
|
||||||
|
is_official = vk in official_virtual_keys
|
||||||
|
if is_keypad and is_official:
|
||||||
|
yield official_virtual_keys[vk][0]
|
||||||
|
|
||||||
|
keyboard_state[0x10] = 0x80 * ('shift' in modifiers)
|
||||||
|
keyboard_state[0x11] = 0x80 * ('alt gr' in modifiers)
|
||||||
|
keyboard_state[0x12] = 0x80 * ('alt gr' in modifiers)
|
||||||
|
keyboard_state[0x14] = 0x01 * ('caps lock' in modifiers)
|
||||||
|
keyboard_state[0x90] = 0x01 * ('num lock' in modifiers)
|
||||||
|
keyboard_state[0x91] = 0x01 * ('scroll lock' in modifiers)
|
||||||
|
unicode_ret = ToUnicode(vk, scan_code, keyboard_state, unicode_buffer, len(unicode_buffer), 0)
|
||||||
|
if unicode_ret and unicode_buffer.value:
|
||||||
|
yield unicode_buffer.value
|
||||||
|
# unicode_ret == -1 -> is dead key
|
||||||
|
# ToUnicode has the side effect of setting global flags for dead keys.
|
||||||
|
# Therefore we need to call it twice to clear those flags.
|
||||||
|
# If your 6 and 7 keys are named "^6" and "^7", this is the reason.
|
||||||
|
ToUnicode(vk, scan_code, keyboard_state, unicode_buffer, len(unicode_buffer), 0)
|
||||||
|
|
||||||
|
name_ret = GetKeyNameText(scan_code << 16 | is_extended << 24, name_buffer, 1024)
|
||||||
|
if name_ret and name_buffer.value:
|
||||||
|
yield name_buffer.value
|
||||||
|
|
||||||
|
char = user32.MapVirtualKeyW(vk, MAPVK_VK_TO_CHAR) & 0xFF
|
||||||
|
if char != 0:
|
||||||
|
yield chr(char)
|
||||||
|
|
||||||
|
if not is_keypad and is_official:
|
||||||
|
yield official_virtual_keys[vk][0]
|
||||||
|
|
||||||
|
def _setup_name_tables():
|
||||||
|
"""
|
||||||
|
Ensures the scan code/virtual key code/name translation tables are
|
||||||
|
filled.
|
||||||
|
"""
|
||||||
|
with tables_lock:
|
||||||
|
if to_name: return
|
||||||
|
|
||||||
|
# Go through every possible scan code, and map them to virtual key codes.
|
||||||
|
# Then vice-versa.
|
||||||
|
all_scan_codes = [(sc, user32.MapVirtualKeyExW(sc, MAPVK_VSC_TO_VK_EX, 0)) for sc in range(0x100)]
|
||||||
|
all_vks = [(user32.MapVirtualKeyExW(vk, MAPVK_VK_TO_VSC_EX, 0), vk) for vk in range(0x100)]
|
||||||
|
for scan_code, vk in all_scan_codes + all_vks:
|
||||||
|
# `to_name` and `from_name` entries will be a tuple (scan_code, vk, extended, shift_state).
|
||||||
|
if (scan_code, vk, 0, 0, 0) in to_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if scan_code not in scan_code_to_vk:
|
||||||
|
scan_code_to_vk[scan_code] = vk
|
||||||
|
|
||||||
|
# Brute force all combinations to find all possible names.
|
||||||
|
for extended in [0, 1]:
|
||||||
|
for modifiers in distinct_modifiers:
|
||||||
|
entry = (scan_code, vk, extended, modifiers)
|
||||||
|
# Get key names from ToUnicode, GetKeyNameText, MapVirtualKeyW and official virtual keys.
|
||||||
|
names = list(get_event_names(*entry))
|
||||||
|
if names:
|
||||||
|
# Also map lowercased key names, but only after the properly cased ones.
|
||||||
|
lowercase_names = [name.lower() for name in names]
|
||||||
|
to_name[entry] = names + lowercase_names
|
||||||
|
# Remember the "id" of the name, as the first techniques
|
||||||
|
# have better results and therefore priority.
|
||||||
|
for i, name in enumerate(map(normalize_name, names + lowercase_names)):
|
||||||
|
from_name[name].append((i, entry))
|
||||||
|
|
||||||
|
# TODO: single quotes on US INTL is returning the dead key (?), and therefore
|
||||||
|
# not typing properly.
|
||||||
|
|
||||||
|
# Alt gr is way outside the usual range of keys (0..127) and on my
|
||||||
|
# computer is named as 'ctrl'. Therefore we add it manually and hope
|
||||||
|
# Windows is consistent in its inconsistency.
|
||||||
|
for extended in [0, 1]:
|
||||||
|
for modifiers in distinct_modifiers:
|
||||||
|
to_name[(541, 162, extended, modifiers)] = ['alt gr']
|
||||||
|
from_name['alt gr'].append((1, (541, 162, extended, modifiers)))
|
||||||
|
|
||||||
|
modifiers_preference = defaultdict(lambda: 10)
|
||||||
|
modifiers_preference.update({(): 0, ('shift',): 1, ('alt gr',): 2, ('ctrl',): 3, ('alt',): 4})
|
||||||
|
def order_key(line):
|
||||||
|
i, entry = line
|
||||||
|
scan_code, vk, extended, modifiers = entry
|
||||||
|
return modifiers_preference[modifiers], i, extended, vk, scan_code
|
||||||
|
for name, entries in list(from_name.items()):
|
||||||
|
from_name[name] = sorted(set(entries), key=order_key)
|
||||||
|
|
||||||
|
# Called by keyboard/__init__.py
|
||||||
|
init = _setup_name_tables
|
||||||
|
|
||||||
|
# List created manually.
|
||||||
|
keypad_keys = [
|
||||||
|
# (scan_code, virtual_key_code, is_extended)
|
||||||
|
(126, 194, 0),
|
||||||
|
(126, 194, 0),
|
||||||
|
(28, 13, 1),
|
||||||
|
(28, 13, 1),
|
||||||
|
(53, 111, 1),
|
||||||
|
(53, 111, 1),
|
||||||
|
(55, 106, 0),
|
||||||
|
(55, 106, 0),
|
||||||
|
(69, 144, 1),
|
||||||
|
(69, 144, 1),
|
||||||
|
(71, 103, 0),
|
||||||
|
(71, 36, 0),
|
||||||
|
(72, 104, 0),
|
||||||
|
(72, 38, 0),
|
||||||
|
(73, 105, 0),
|
||||||
|
(73, 33, 0),
|
||||||
|
(74, 109, 0),
|
||||||
|
(74, 109, 0),
|
||||||
|
(75, 100, 0),
|
||||||
|
(75, 37, 0),
|
||||||
|
(76, 101, 0),
|
||||||
|
(76, 12, 0),
|
||||||
|
(77, 102, 0),
|
||||||
|
(77, 39, 0),
|
||||||
|
(78, 107, 0),
|
||||||
|
(78, 107, 0),
|
||||||
|
(79, 35, 0),
|
||||||
|
(79, 97, 0),
|
||||||
|
(80, 40, 0),
|
||||||
|
(80, 98, 0),
|
||||||
|
(81, 34, 0),
|
||||||
|
(81, 99, 0),
|
||||||
|
(82, 45, 0),
|
||||||
|
(82, 96, 0),
|
||||||
|
(83, 110, 0),
|
||||||
|
(83, 46, 0),
|
||||||
|
]
|
||||||
|
|
||||||
|
shift_is_pressed = False
|
||||||
|
altgr_is_pressed = False
|
||||||
|
ignore_next_right_alt = False
|
||||||
|
shift_vks = set([0x10, 0xa0, 0xa1])
|
||||||
|
def prepare_intercept(callback):
|
||||||
|
"""
|
||||||
|
Registers a Windows low level keyboard hook. The provided callback will
|
||||||
|
be invoked for each high-level keyboard event, and is expected to return
|
||||||
|
True if the key event should be passed to the next program, or False if
|
||||||
|
the event is to be blocked.
|
||||||
|
|
||||||
|
No event is processed until the Windows messages are pumped (see
|
||||||
|
start_intercept).
|
||||||
|
"""
|
||||||
|
_setup_name_tables()
|
||||||
|
|
||||||
|
def process_key(event_type, vk, scan_code, is_extended):
|
||||||
|
global shift_is_pressed, altgr_is_pressed, ignore_next_right_alt
|
||||||
|
#print(event_type, vk, scan_code, is_extended)
|
||||||
|
|
||||||
|
# Pressing alt-gr also generates an extra "right alt" event
|
||||||
|
if vk == 0xA5 and ignore_next_right_alt:
|
||||||
|
ignore_next_right_alt = False
|
||||||
|
return True
|
||||||
|
|
||||||
|
modifiers = (
|
||||||
|
('shift',) * shift_is_pressed +
|
||||||
|
('alt gr',) * altgr_is_pressed +
|
||||||
|
('num lock',) * (user32.GetKeyState(0x90) & 1) +
|
||||||
|
('caps lock',) * (user32.GetKeyState(0x14) & 1) +
|
||||||
|
('scroll lock',) * (user32.GetKeyState(0x91) & 1)
|
||||||
|
)
|
||||||
|
entry = (scan_code, vk, is_extended, modifiers)
|
||||||
|
if entry not in to_name:
|
||||||
|
to_name[entry] = list(get_event_names(*entry))
|
||||||
|
|
||||||
|
names = to_name[entry]
|
||||||
|
name = names[0] if names else None
|
||||||
|
|
||||||
|
# TODO: inaccurate when holding multiple different shifts.
|
||||||
|
if vk in shift_vks:
|
||||||
|
shift_is_pressed = event_type == KEY_DOWN
|
||||||
|
if scan_code == 541 and vk == 162:
|
||||||
|
ignore_next_right_alt = True
|
||||||
|
altgr_is_pressed = event_type == KEY_DOWN
|
||||||
|
|
||||||
|
is_keypad = (scan_code, vk, is_extended) in keypad_keys
|
||||||
|
return callback(KeyboardEvent(event_type=event_type, scan_code=scan_code or -vk, name=name, is_keypad=is_keypad))
|
||||||
|
|
||||||
|
def low_level_keyboard_handler(nCode, wParam, lParam):
|
||||||
|
try:
|
||||||
|
vk = lParam.contents.vk_code
|
||||||
|
# Ignore the second `alt` DOWN observed in some cases.
|
||||||
|
fake_alt = (LLKHF_INJECTED | 0x20)
|
||||||
|
# Ignore events generated by SendInput with Unicode.
|
||||||
|
if vk != VK_PACKET and lParam.contents.flags & fake_alt != fake_alt:
|
||||||
|
event_type = keyboard_event_types[wParam]
|
||||||
|
is_extended = lParam.contents.flags & 1
|
||||||
|
scan_code = lParam.contents.scan_code
|
||||||
|
should_continue = process_key(event_type, vk, scan_code, is_extended)
|
||||||
|
if not should_continue:
|
||||||
|
return -1
|
||||||
|
except Exception as e:
|
||||||
|
print('Error in keyboard hook:')
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
return CallNextHookEx(None, nCode, wParam, lParam)
|
||||||
|
|
||||||
|
WH_KEYBOARD_LL = c_int(13)
|
||||||
|
keyboard_callback = LowLevelKeyboardProc(low_level_keyboard_handler)
|
||||||
|
handle = GetModuleHandleW(None)
|
||||||
|
thread_id = DWORD(0)
|
||||||
|
keyboard_hook = SetWindowsHookEx(WH_KEYBOARD_LL, keyboard_callback, handle, thread_id)
|
||||||
|
|
||||||
|
# Register to remove the hook when the interpreter exits. Unfortunately a
|
||||||
|
# try/finally block doesn't seem to work here.
|
||||||
|
atexit.register(UnhookWindowsHookEx, keyboard_callback)
|
||||||
|
|
||||||
|
def listen(callback):
|
||||||
|
prepare_intercept(callback)
|
||||||
|
msg = LPMSG()
|
||||||
|
while not GetMessage(msg, 0, 0, 0):
|
||||||
|
TranslateMessage(msg)
|
||||||
|
DispatchMessage(msg)
|
||||||
|
|
||||||
|
def map_name(name):
|
||||||
|
_setup_name_tables()
|
||||||
|
|
||||||
|
entries = from_name.get(name)
|
||||||
|
if not entries:
|
||||||
|
raise ValueError('Key name {} is not mapped to any known key.'.format(repr(name)))
|
||||||
|
for i, entry in entries:
|
||||||
|
scan_code, vk, is_extended, modifiers = entry
|
||||||
|
yield scan_code or -vk, modifiers
|
||||||
|
|
||||||
|
def _send_event(code, event_type):
|
||||||
|
if code == 541:
|
||||||
|
# Alt-gr is made of ctrl+alt. Just sending even 541 doesn't do anything.
|
||||||
|
user32.keybd_event(0x11, code, event_type, 0)
|
||||||
|
user32.keybd_event(0x12, code, event_type, 0)
|
||||||
|
elif code > 0:
|
||||||
|
vk = scan_code_to_vk.get(code, 0)
|
||||||
|
user32.keybd_event(vk, code, event_type, 0)
|
||||||
|
else:
|
||||||
|
# Negative scan code is a way to indicate we don't have a scan code,
|
||||||
|
# and the value actually contains the Virtual key code.
|
||||||
|
user32.keybd_event(-code, 0, event_type, 0)
|
||||||
|
|
||||||
|
def press(code):
|
||||||
|
_send_event(code, 0)
|
||||||
|
|
||||||
|
def release(code):
|
||||||
|
_send_event(code, 2)
|
||||||
|
|
||||||
|
def type_unicode(character):
|
||||||
|
# This code and related structures are based on
|
||||||
|
# http://stackoverflow.com/a/11910555/252218
|
||||||
|
surrogates = bytearray(character.encode('utf-16le'))
|
||||||
|
presses = []
|
||||||
|
releases = []
|
||||||
|
for i in range(0, len(surrogates), 2):
|
||||||
|
higher, lower = surrogates[i:i+2]
|
||||||
|
structure = KEYBDINPUT(0, (lower << 8) + higher, KEYEVENTF_UNICODE, 0, None)
|
||||||
|
presses.append(INPUT(INPUT_KEYBOARD, _INPUTunion(ki=structure)))
|
||||||
|
structure = KEYBDINPUT(0, (lower << 8) + higher, KEYEVENTF_UNICODE | KEYEVENTF_KEYUP, 0, None)
|
||||||
|
releases.append(INPUT(INPUT_KEYBOARD, _INPUTunion(ki=structure)))
|
||||||
|
inputs = presses + releases
|
||||||
|
nInputs = len(inputs)
|
||||||
|
LPINPUT = INPUT * nInputs
|
||||||
|
pInputs = LPINPUT(*inputs)
|
||||||
|
cbSize = c_int(ctypes.sizeof(INPUT))
|
||||||
|
SendInput(nInputs, pInputs, cbSize)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
_setup_name_tables()
|
||||||
|
import pprint
|
||||||
|
pprint.pprint(to_name)
|
||||||
|
pprint.pprint(from_name)
|
||||||
|
#listen(lambda e: print(e.to_json()) or True)
|
|
@ -0,0 +1,201 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import ctypes
|
||||||
|
import time
|
||||||
|
from ctypes import c_short, c_char, c_uint8, c_int32, c_int, c_uint, c_uint32, c_long, byref, Structure, CFUNCTYPE, POINTER
|
||||||
|
from ctypes.wintypes import DWORD, BOOL, HHOOK, MSG, LPWSTR, WCHAR, WPARAM, LPARAM
|
||||||
|
LPMSG = POINTER(MSG)
|
||||||
|
|
||||||
|
import atexit
|
||||||
|
|
||||||
|
from ._mouse_event import ButtonEvent, WheelEvent, MoveEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN, DOUBLE, WHEEL, HORIZONTAL, VERTICAL
|
||||||
|
|
||||||
|
#https://github.com/boppreh/mouse/issues/1
|
||||||
|
#user32 = ctypes.windll.user32
|
||||||
|
user32 = ctypes.WinDLL('user32', use_last_error = True)
|
||||||
|
|
||||||
|
class MSLLHOOKSTRUCT(Structure):
|
||||||
|
_fields_ = [("x", c_long),
|
||||||
|
("y", c_long),
|
||||||
|
('data', c_int32),
|
||||||
|
('reserved', c_int32),
|
||||||
|
("flags", DWORD),
|
||||||
|
("time", c_int),
|
||||||
|
]
|
||||||
|
|
||||||
|
LowLevelMouseProc = CFUNCTYPE(c_int, WPARAM, LPARAM, POINTER(MSLLHOOKSTRUCT))
|
||||||
|
|
||||||
|
SetWindowsHookEx = user32.SetWindowsHookExA
|
||||||
|
#SetWindowsHookEx.argtypes = [c_int, LowLevelMouseProc, c_int, c_int]
|
||||||
|
SetWindowsHookEx.restype = HHOOK
|
||||||
|
|
||||||
|
CallNextHookEx = user32.CallNextHookEx
|
||||||
|
#CallNextHookEx.argtypes = [c_int , c_int, c_int, POINTER(MSLLHOOKSTRUCT)]
|
||||||
|
CallNextHookEx.restype = c_int
|
||||||
|
|
||||||
|
UnhookWindowsHookEx = user32.UnhookWindowsHookEx
|
||||||
|
UnhookWindowsHookEx.argtypes = [HHOOK]
|
||||||
|
UnhookWindowsHookEx.restype = BOOL
|
||||||
|
|
||||||
|
GetMessage = user32.GetMessageW
|
||||||
|
GetMessage.argtypes = [LPMSG, c_int, c_int, c_int]
|
||||||
|
GetMessage.restype = BOOL
|
||||||
|
|
||||||
|
TranslateMessage = user32.TranslateMessage
|
||||||
|
TranslateMessage.argtypes = [LPMSG]
|
||||||
|
TranslateMessage.restype = BOOL
|
||||||
|
|
||||||
|
DispatchMessage = user32.DispatchMessageA
|
||||||
|
DispatchMessage.argtypes = [LPMSG]
|
||||||
|
|
||||||
|
# Beware, as of 2016-01-30 the official docs have a very incomplete list.
|
||||||
|
# This one was compiled from experience and may be incomplete.
|
||||||
|
WM_MOUSEMOVE = 0x200
|
||||||
|
WM_LBUTTONDOWN = 0x201
|
||||||
|
WM_LBUTTONUP = 0x202
|
||||||
|
WM_LBUTTONDBLCLK = 0x203
|
||||||
|
WM_RBUTTONDOWN = 0x204
|
||||||
|
WM_RBUTTONUP = 0x205
|
||||||
|
WM_RBUTTONDBLCLK = 0x206
|
||||||
|
WM_MBUTTONDOWN = 0x207
|
||||||
|
WM_MBUTTONUP = 0x208
|
||||||
|
WM_MBUTTONDBLCLK = 0x209
|
||||||
|
WM_MOUSEWHEEL = 0x20A
|
||||||
|
WM_XBUTTONDOWN = 0x20B
|
||||||
|
WM_XBUTTONUP = 0x20C
|
||||||
|
WM_XBUTTONDBLCLK = 0x20D
|
||||||
|
WM_NCXBUTTONDOWN = 0x00AB
|
||||||
|
WM_NCXBUTTONUP = 0x00AC
|
||||||
|
WM_NCXBUTTONDBLCLK = 0x00AD
|
||||||
|
WM_MOUSEHWHEEL = 0x20E
|
||||||
|
WM_LBUTTONDOWN = 0x0201
|
||||||
|
WM_LBUTTONUP = 0x0202
|
||||||
|
WM_MOUSEMOVE = 0x0200
|
||||||
|
WM_MOUSEWHEEL = 0x020A
|
||||||
|
WM_MOUSEHWHEEL = 0x020E
|
||||||
|
WM_RBUTTONDOWN = 0x0204
|
||||||
|
WM_RBUTTONUP = 0x0205
|
||||||
|
|
||||||
|
buttons_by_wm_code = {
|
||||||
|
WM_LBUTTONDOWN: (DOWN, LEFT),
|
||||||
|
WM_LBUTTONUP: (UP, LEFT),
|
||||||
|
WM_LBUTTONDBLCLK: (DOUBLE, LEFT),
|
||||||
|
|
||||||
|
WM_RBUTTONDOWN: (DOWN, RIGHT),
|
||||||
|
WM_RBUTTONUP: (UP, RIGHT),
|
||||||
|
WM_RBUTTONDBLCLK: (DOUBLE, RIGHT),
|
||||||
|
|
||||||
|
WM_MBUTTONDOWN: (DOWN, MIDDLE),
|
||||||
|
WM_MBUTTONUP: (UP, MIDDLE),
|
||||||
|
WM_MBUTTONDBLCLK: (DOUBLE, MIDDLE),
|
||||||
|
|
||||||
|
WM_XBUTTONDOWN: (DOWN, X),
|
||||||
|
WM_XBUTTONUP: (UP, X),
|
||||||
|
WM_XBUTTONDBLCLK: (DOUBLE, X),
|
||||||
|
}
|
||||||
|
|
||||||
|
MOUSEEVENTF_ABSOLUTE = 0x8000
|
||||||
|
MOUSEEVENTF_MOVE = 0x1
|
||||||
|
MOUSEEVENTF_WHEEL = 0x800
|
||||||
|
MOUSEEVENTF_HWHEEL = 0x1000
|
||||||
|
MOUSEEVENTF_LEFTDOWN = 0x2
|
||||||
|
MOUSEEVENTF_LEFTUP = 0x4
|
||||||
|
MOUSEEVENTF_RIGHTDOWN = 0x8
|
||||||
|
MOUSEEVENTF_RIGHTUP = 0x10
|
||||||
|
MOUSEEVENTF_MIDDLEDOWN = 0x20
|
||||||
|
MOUSEEVENTF_MIDDLEUP = 0x40
|
||||||
|
MOUSEEVENTF_XDOWN = 0x0080
|
||||||
|
MOUSEEVENTF_XUP = 0x0100
|
||||||
|
|
||||||
|
simulated_mouse_codes = {
|
||||||
|
(WHEEL, HORIZONTAL): MOUSEEVENTF_HWHEEL,
|
||||||
|
(WHEEL, VERTICAL): MOUSEEVENTF_WHEEL,
|
||||||
|
|
||||||
|
(DOWN, LEFT): MOUSEEVENTF_LEFTDOWN,
|
||||||
|
(UP, LEFT): MOUSEEVENTF_LEFTUP,
|
||||||
|
|
||||||
|
(DOWN, RIGHT): MOUSEEVENTF_RIGHTDOWN,
|
||||||
|
(UP, RIGHT): MOUSEEVENTF_RIGHTUP,
|
||||||
|
|
||||||
|
(DOWN, MIDDLE): MOUSEEVENTF_MIDDLEDOWN,
|
||||||
|
(UP, MIDDLE): MOUSEEVENTF_MIDDLEUP,
|
||||||
|
|
||||||
|
(DOWN, X): MOUSEEVENTF_XDOWN,
|
||||||
|
(UP, X): MOUSEEVENTF_XUP,
|
||||||
|
}
|
||||||
|
|
||||||
|
NULL = c_int(0)
|
||||||
|
|
||||||
|
WHEEL_DELTA = 120
|
||||||
|
|
||||||
|
init = lambda: None
|
||||||
|
|
||||||
|
def listen(queue):
|
||||||
|
def low_level_mouse_handler(nCode, wParam, lParam):
|
||||||
|
struct = lParam.contents
|
||||||
|
# Can't use struct.time because it's usually zero.
|
||||||
|
t = time.time()
|
||||||
|
|
||||||
|
if wParam == WM_MOUSEMOVE:
|
||||||
|
event = MoveEvent(struct.x, struct.y, t)
|
||||||
|
elif wParam == WM_MOUSEWHEEL:
|
||||||
|
event = WheelEvent(struct.data / (WHEEL_DELTA * (2<<15)), t)
|
||||||
|
elif wParam in buttons_by_wm_code:
|
||||||
|
type, button = buttons_by_wm_code.get(wParam, ('?', '?'))
|
||||||
|
if wParam >= WM_XBUTTONDOWN:
|
||||||
|
button = {0x10000: X, 0x20000: X2}[struct.data]
|
||||||
|
event = ButtonEvent(type, button, t)
|
||||||
|
|
||||||
|
queue.put(event)
|
||||||
|
return CallNextHookEx(NULL, nCode, wParam, lParam)
|
||||||
|
|
||||||
|
WH_MOUSE_LL = c_int(14)
|
||||||
|
mouse_callback = LowLevelMouseProc(low_level_mouse_handler)
|
||||||
|
mouse_hook = SetWindowsHookEx(WH_MOUSE_LL, mouse_callback, NULL, NULL)
|
||||||
|
|
||||||
|
# Register to remove the hook when the interpreter exits. Unfortunately a
|
||||||
|
# try/finally block doesn't seem to work here.
|
||||||
|
atexit.register(UnhookWindowsHookEx, mouse_hook)
|
||||||
|
|
||||||
|
msg = LPMSG()
|
||||||
|
while not GetMessage(msg, NULL, NULL, NULL):
|
||||||
|
TranslateMessage(msg)
|
||||||
|
DispatchMessage(msg)
|
||||||
|
|
||||||
|
def _translate_button(button):
|
||||||
|
if button == X or button == X2:
|
||||||
|
return X, {X: 0x10000, X2: 0x20000}[button]
|
||||||
|
else:
|
||||||
|
return button, 0
|
||||||
|
|
||||||
|
def press(button=LEFT):
|
||||||
|
button, data = _translate_button(button)
|
||||||
|
code = simulated_mouse_codes[(DOWN, button)]
|
||||||
|
user32.mouse_event(code, 0, 0, data, 0)
|
||||||
|
|
||||||
|
def release(button=LEFT):
|
||||||
|
button, data = _translate_button(button)
|
||||||
|
code = simulated_mouse_codes[(UP, button)]
|
||||||
|
user32.mouse_event(code, 0, 0, data, 0)
|
||||||
|
|
||||||
|
def wheel(delta=1):
|
||||||
|
code = simulated_mouse_codes[(WHEEL, VERTICAL)]
|
||||||
|
user32.mouse_event(code, 0, 0, int(delta * WHEEL_DELTA), 0)
|
||||||
|
|
||||||
|
def move_to(x, y):
|
||||||
|
user32.SetCursorPos(int(x), int(y))
|
||||||
|
|
||||||
|
def move_relative(x, y):
|
||||||
|
user32.mouse_event(MOUSEEVENTF_MOVE, int(x), int(y), 0, 0)
|
||||||
|
|
||||||
|
class POINT(Structure):
|
||||||
|
_fields_ = [("x", c_long), ("y", c_long)]
|
||||||
|
|
||||||
|
def get_position():
|
||||||
|
point = POINT()
|
||||||
|
user32.GetCursorPos(byref(point))
|
||||||
|
return (point.x, point.y)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
def p(e):
|
||||||
|
print(e)
|
||||||
|
listen(p)
|
|
@ -0,0 +1,232 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import warnings
|
||||||
|
warnings.simplefilter('always', DeprecationWarning)
|
||||||
|
warnings.warn('The mouse sub-library is deprecated and will be removed in future versions. Please use the standalone package `mouse`.', DeprecationWarning, stacklevel=2)
|
||||||
|
|
||||||
|
import time as _time
|
||||||
|
|
||||||
|
import platform as _platform
|
||||||
|
if _platform.system() == 'Windows':
|
||||||
|
from. import _winmouse as _os_mouse
|
||||||
|
elif _platform.system() == 'Linux':
|
||||||
|
from. import _nixmouse as _os_mouse
|
||||||
|
elif _platform.system() == 'Darwin':
|
||||||
|
from. import _darwinmouse as _os_mouse
|
||||||
|
else:
|
||||||
|
raise OSError("Unsupported platform '{}'".format(_platform.system()))
|
||||||
|
|
||||||
|
from ._mouse_event import ButtonEvent, MoveEvent, WheelEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN, DOUBLE
|
||||||
|
from ._generic import GenericListener as _GenericListener
|
||||||
|
|
||||||
|
_pressed_events = set()
|
||||||
|
class _MouseListener(_GenericListener):
|
||||||
|
def init(self):
|
||||||
|
_os_mouse.init()
|
||||||
|
def pre_process_event(self, event):
|
||||||
|
if isinstance(event, ButtonEvent):
|
||||||
|
if event.event_type in (UP, DOUBLE):
|
||||||
|
_pressed_events.discard(event.button)
|
||||||
|
else:
|
||||||
|
_pressed_events.add(event.button)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def listen(self):
|
||||||
|
_os_mouse.listen(self.queue)
|
||||||
|
|
||||||
|
_listener = _MouseListener()
|
||||||
|
|
||||||
|
def is_pressed(button=LEFT):
|
||||||
|
""" Returns True if the given button is currently pressed. """
|
||||||
|
_listener.start_if_necessary()
|
||||||
|
return button in _pressed_events
|
||||||
|
|
||||||
|
def press(button=LEFT):
|
||||||
|
""" Presses the given button (but doesn't release). """
|
||||||
|
_os_mouse.press(button)
|
||||||
|
|
||||||
|
def release(button=LEFT):
|
||||||
|
""" Releases the given button. """
|
||||||
|
_os_mouse.release(button)
|
||||||
|
|
||||||
|
def click(button=LEFT):
|
||||||
|
""" Sends a click with the given button. """
|
||||||
|
_os_mouse.press(button)
|
||||||
|
_os_mouse.release(button)
|
||||||
|
|
||||||
|
def double_click(button=LEFT):
|
||||||
|
""" Sends a double click with the given button. """
|
||||||
|
click(button)
|
||||||
|
click(button)
|
||||||
|
|
||||||
|
def right_click():
|
||||||
|
""" Sends a right click with the given button. """
|
||||||
|
click(RIGHT)
|
||||||
|
|
||||||
|
def wheel(delta=1):
|
||||||
|
""" Scrolls the wheel `delta` clicks. Sign indicates direction. """
|
||||||
|
_os_mouse.wheel(delta)
|
||||||
|
|
||||||
|
def move(x, y, absolute=True, duration=0):
|
||||||
|
"""
|
||||||
|
Moves the mouse. If `absolute`, to position (x, y), otherwise move relative
|
||||||
|
to the current position. If `duration` is non-zero, animates the movement.
|
||||||
|
"""
|
||||||
|
x = int(x)
|
||||||
|
y = int(y)
|
||||||
|
|
||||||
|
# Requires an extra system call on Linux, but `move_relative` is measured
|
||||||
|
# in millimiters so we would lose precision.
|
||||||
|
position_x, position_y = get_position()
|
||||||
|
|
||||||
|
if not absolute:
|
||||||
|
x = position_x + x
|
||||||
|
y = position_y + y
|
||||||
|
|
||||||
|
if duration:
|
||||||
|
start_x = position_x
|
||||||
|
start_y = position_y
|
||||||
|
dx = x - start_x
|
||||||
|
dy = y - start_y
|
||||||
|
|
||||||
|
if dx == 0 and dy == 0:
|
||||||
|
_time.sleep(duration)
|
||||||
|
else:
|
||||||
|
# 120 movements per second.
|
||||||
|
# Round and keep float to ensure float division in Python 2
|
||||||
|
steps = max(1.0, float(int(duration * 120.0)))
|
||||||
|
for i in range(int(steps)+1):
|
||||||
|
move(start_x + dx*i/steps, start_y + dy*i/steps)
|
||||||
|
_time.sleep(duration/steps)
|
||||||
|
else:
|
||||||
|
_os_mouse.move_to(x, y)
|
||||||
|
|
||||||
|
def drag(start_x, start_y, end_x, end_y, absolute=True, duration=0):
|
||||||
|
"""
|
||||||
|
Holds the left mouse button, moving from start to end position, then
|
||||||
|
releases. `absolute` and `duration` are parameters regarding the mouse
|
||||||
|
movement.
|
||||||
|
"""
|
||||||
|
if is_pressed():
|
||||||
|
release()
|
||||||
|
move(start_x, start_y, absolute, 0)
|
||||||
|
press()
|
||||||
|
move(end_x, end_y, absolute, duration)
|
||||||
|
release()
|
||||||
|
|
||||||
|
def on_button(callback, args=(), buttons=(LEFT, MIDDLE, RIGHT, X, X2), types=(UP, DOWN, DOUBLE)):
|
||||||
|
""" Invokes `callback` with `args` when the specified event happens. """
|
||||||
|
if not isinstance(buttons, (tuple, list)):
|
||||||
|
buttons = (buttons,)
|
||||||
|
if not isinstance(types, (tuple, list)):
|
||||||
|
types = (types,)
|
||||||
|
|
||||||
|
def handler(event):
|
||||||
|
if isinstance(event, ButtonEvent):
|
||||||
|
if event.event_type in types and event.button in buttons:
|
||||||
|
callback(*args)
|
||||||
|
_listener.add_handler(handler)
|
||||||
|
return handler
|
||||||
|
|
||||||
|
def on_click(callback, args=()):
|
||||||
|
""" Invokes `callback` with `args` when the left button is clicked. """
|
||||||
|
return on_button(callback, args, [LEFT], [UP])
|
||||||
|
|
||||||
|
def on_double_click(callback, args=()):
|
||||||
|
"""
|
||||||
|
Invokes `callback` with `args` when the left button is double clicked.
|
||||||
|
"""
|
||||||
|
return on_button(callback, args, [LEFT], [DOUBLE])
|
||||||
|
|
||||||
|
def on_right_click(callback, args=()):
|
||||||
|
""" Invokes `callback` with `args` when the right button is clicked. """
|
||||||
|
return on_button(callback, args, [RIGHT], [UP])
|
||||||
|
|
||||||
|
def on_middle_click(callback, args=()):
|
||||||
|
""" Invokes `callback` with `args` when the middle button is clicked. """
|
||||||
|
return on_button(callback, args, [MIDDLE], [UP])
|
||||||
|
|
||||||
|
def wait(button=LEFT, target_types=(UP, DOWN, DOUBLE)):
|
||||||
|
"""
|
||||||
|
Blocks program execution until the given button performs an event.
|
||||||
|
"""
|
||||||
|
from threading import Lock
|
||||||
|
lock = Lock()
|
||||||
|
lock.acquire()
|
||||||
|
handler = on_button(lock.release, (), [button], target_types)
|
||||||
|
lock.acquire()
|
||||||
|
_listener.remove_handler(handler)
|
||||||
|
|
||||||
|
def get_position():
|
||||||
|
""" Returns the (x, y) mouse position. """
|
||||||
|
return _os_mouse.get_position()
|
||||||
|
|
||||||
|
def hook(callback):
|
||||||
|
"""
|
||||||
|
Installs a global listener on all available mouses, invoking `callback`
|
||||||
|
each time it is moved, a key status changes or the wheel is spun. A mouse
|
||||||
|
event is passed as argument, with type either `mouse.ButtonEvent`,
|
||||||
|
`mouse.WheelEvent` or `mouse.MoveEvent`.
|
||||||
|
|
||||||
|
Returns the given callback for easier development.
|
||||||
|
"""
|
||||||
|
_listener.add_handler(callback)
|
||||||
|
return callback
|
||||||
|
|
||||||
|
def unhook(callback):
|
||||||
|
"""
|
||||||
|
Removes a previously installed hook.
|
||||||
|
"""
|
||||||
|
_listener.remove_handler(callback)
|
||||||
|
|
||||||
|
def unhook_all():
|
||||||
|
"""
|
||||||
|
Removes all hooks registered by this application. Note this may include
|
||||||
|
hooks installed by high level functions, such as `record`.
|
||||||
|
"""
|
||||||
|
del _listener.handlers[:]
|
||||||
|
|
||||||
|
def record(button=RIGHT, target_types=(DOWN,)):
|
||||||
|
"""
|
||||||
|
Records all mouse events until the user presses the given button.
|
||||||
|
Then returns the list of events recorded. Pairs well with `play(events)`.
|
||||||
|
|
||||||
|
Note: this is a blocking function.
|
||||||
|
Note: for more details on the mouse hook and events see `hook`.
|
||||||
|
"""
|
||||||
|
recorded = []
|
||||||
|
hook(recorded.append)
|
||||||
|
wait(button=button, target_types=target_types)
|
||||||
|
unhook(recorded.append)
|
||||||
|
return recorded
|
||||||
|
|
||||||
|
def play(events, speed_factor=1.0, include_clicks=True, include_moves=True, include_wheel=True):
|
||||||
|
"""
|
||||||
|
Plays a sequence of recorded events, maintaining the relative time
|
||||||
|
intervals. If speed_factor is <= 0 then the actions are replayed as fast
|
||||||
|
as the OS allows. Pairs well with `record()`.
|
||||||
|
|
||||||
|
The parameters `include_*` define if events of that type should be inluded
|
||||||
|
in the replay or ignored.
|
||||||
|
"""
|
||||||
|
last_time = None
|
||||||
|
for event in events:
|
||||||
|
if speed_factor > 0 and last_time is not None:
|
||||||
|
_time.sleep((event.time - last_time) / speed_factor)
|
||||||
|
last_time = event.time
|
||||||
|
|
||||||
|
if isinstance(event, ButtonEvent) and include_clicks:
|
||||||
|
if event.event_type == UP:
|
||||||
|
_os_mouse.release(event.button)
|
||||||
|
else:
|
||||||
|
_os_mouse.press(event.button)
|
||||||
|
elif isinstance(event, MoveEvent) and include_moves:
|
||||||
|
_os_mouse.move_to(event.x, event.y)
|
||||||
|
elif isinstance(event, WheelEvent) and include_wheel:
|
||||||
|
_os_mouse.wheel(event.delta)
|
||||||
|
|
||||||
|
replay = play
|
||||||
|
hold = press
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print('Recording... Double click to stop and replay.')
|
||||||
|
play(record())
|
|
@ -0,0 +1,231 @@
|
||||||
|
from PyQt5.QtCore import *
|
||||||
|
from PyQt5.QtGui import *
|
||||||
|
from PyQt5.QtWidgets import *
|
||||||
|
from ui_mainwnd import Ui_MainWidget
|
||||||
|
from profileeditwnd import ProfileEditWnd
|
||||||
|
import json
|
||||||
|
from profileexecutor import ProfileExecutor
|
||||||
|
import sys
|
||||||
|
import pickle
|
||||||
|
import time
|
||||||
|
import keyboard
|
||||||
|
import threading
|
||||||
|
|
||||||
|
class MainWnd(QWidget):
|
||||||
|
def __init__(self, p_parent = None):
|
||||||
|
super().__init__(p_parent)
|
||||||
|
self.ui = Ui_MainWidget()
|
||||||
|
self.ui.setupUi(self)
|
||||||
|
self.m_profileExecutor = ProfileExecutor()
|
||||||
|
|
||||||
|
self.ui.profileCbx.currentIndexChanged.connect(self.slotProfileChanged)
|
||||||
|
self.ui.addBut.clicked.connect(self.slotAddNewProfile)
|
||||||
|
self.ui.editBut.clicked.connect(self.slotEditProfile)
|
||||||
|
self.ui.removeBut.clicked.connect(self.slotRemoveProfile)
|
||||||
|
self.ui.listeningChk.stateChanged.connect(self.slotListeningEnabled)
|
||||||
|
self.ui.ok.clicked.connect(self.slotOK)
|
||||||
|
self.ui.cancel.clicked.connect(self.slotCancel)
|
||||||
|
|
||||||
|
self.m_profileExecutor.start()
|
||||||
|
|
||||||
|
if self.loadFromDatabase() > 0 :
|
||||||
|
# if self.loadTestProfiles() > 0:
|
||||||
|
w_jsonProfile = self.ui.profileCbx.itemData(0)
|
||||||
|
if w_jsonProfile != None:
|
||||||
|
self.m_activeProfile = json.loads(w_jsonProfile)
|
||||||
|
self.m_profileExecutor.setProfile(self.m_activeProfile)
|
||||||
|
|
||||||
|
def saveToDatabase(self):
|
||||||
|
w_profiles = []
|
||||||
|
w_profileCnt = self.ui.profileCbx.count()
|
||||||
|
for w_idx in range(w_profileCnt):
|
||||||
|
w_jsonProfile = self.ui.profileCbx.itemData(w_idx)
|
||||||
|
if w_jsonProfile == None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
w_profile = json.loads(w_jsonProfile)
|
||||||
|
w_profiles.append(w_profile)
|
||||||
|
|
||||||
|
with open("profiles.dat", "wb") as f:
|
||||||
|
pickle.dump(w_profiles, f, pickle.HIGHEST_PROTOCOL)
|
||||||
|
|
||||||
|
def loadFromDatabase(self):
|
||||||
|
w_profiles = []
|
||||||
|
with open("profiles.dat", "rb") as f:
|
||||||
|
w_profiles = pickle.load(f)
|
||||||
|
|
||||||
|
for w_profile in w_profiles:
|
||||||
|
self.ui.profileCbx.addItem(w_profile['name'])
|
||||||
|
w_jsonProfile = json.dumps(w_profile)
|
||||||
|
self.ui.profileCbx.setItemData(self.ui.profileCbx.count() - 1, w_jsonProfile)
|
||||||
|
|
||||||
|
return len(w_profiles)
|
||||||
|
|
||||||
|
def loadTestProfiles(self):
|
||||||
|
|
||||||
|
w_carProfileDict = {
|
||||||
|
"name": "car game",
|
||||||
|
"commands": [
|
||||||
|
{'name': 'up',
|
||||||
|
'actions': [
|
||||||
|
{'name': 'key action', 'key': 'up', 'type': 1},
|
||||||
|
{'name': 'pause action', 'time': 0.03}
|
||||||
|
],
|
||||||
|
'repeat': 1,
|
||||||
|
'async': False
|
||||||
|
},
|
||||||
|
{'name': 'left',
|
||||||
|
'actions': [{'name': 'key action', 'key': 'right', 'type': 0},
|
||||||
|
{'name': 'pause action', 'time': 0.03},
|
||||||
|
{'name': 'key action', 'key': 'left', 'type': 1},
|
||||||
|
{'name': 'pause action', 'time': 0.03}
|
||||||
|
],
|
||||||
|
'repeat': 1,
|
||||||
|
'async': False
|
||||||
|
},
|
||||||
|
{'name': 'right',
|
||||||
|
'actions': [{'name': 'key action', 'key': 'left', 'type': 0},
|
||||||
|
{'name': 'pause action', 'time': 0.03},
|
||||||
|
{'name': 'key action', 'key': 'right', 'type': 1},
|
||||||
|
{'name': 'pause action', 'time': 0.03}
|
||||||
|
],
|
||||||
|
'repeat': 1,
|
||||||
|
'async': False
|
||||||
|
},
|
||||||
|
{'name': 'stop',
|
||||||
|
'actions': [
|
||||||
|
{'name': 'key action', 'key': 'left', 'type': 0},
|
||||||
|
{'name': 'pause action', 'time': 0.03},
|
||||||
|
{'name': 'key action', 'key': 'right', 'type': 0},
|
||||||
|
{'name': 'pause action', 'time': 0.03},
|
||||||
|
{'name': 'key action', 'key': 'up', 'type': 0},
|
||||||
|
{'name': 'pause action', 'time': 0.03}
|
||||||
|
],
|
||||||
|
'repeat': 1,
|
||||||
|
'async': False
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
w_airplaneProfileDict = {
|
||||||
|
"name": "airplane game",
|
||||||
|
"commands": [
|
||||||
|
{'name': 'up',
|
||||||
|
'actions': [
|
||||||
|
{'name': 'command stop action', 'command name': 'down'},
|
||||||
|
{'name': 'mouse move action', 'x': 0, 'y': -5, 'absolute': False},
|
||||||
|
{'name': 'pause action', 'time': 0.01}
|
||||||
|
],
|
||||||
|
'repeat': -1,
|
||||||
|
'async': True
|
||||||
|
},
|
||||||
|
{'name': 'left',
|
||||||
|
'actions': [
|
||||||
|
{'name': 'command stop action', 'command name': 'right'},
|
||||||
|
{'name': 'mouse move action', 'x': -5, 'y': 0, 'absolute': False},
|
||||||
|
{'name': 'pause action', 'time': 0.005}
|
||||||
|
],
|
||||||
|
'repeat': -1,
|
||||||
|
'async': True
|
||||||
|
},
|
||||||
|
{'name': 'right',
|
||||||
|
'actions': [
|
||||||
|
{'name': 'command stop action', 'command name': 'left'},
|
||||||
|
{'name': 'mouse move action', 'x': 5, 'y': 0, 'absolute': False},
|
||||||
|
{'name': 'pause action', 'time': 0.005}
|
||||||
|
],
|
||||||
|
'repeat': -1,
|
||||||
|
'async': True
|
||||||
|
},
|
||||||
|
{'name': 'down',
|
||||||
|
'actions': [
|
||||||
|
{'name': 'command stop action', 'command name': 'up'},
|
||||||
|
{'name': 'mouse move action', 'x': 0, 'y': 5, 'absolute': False},
|
||||||
|
{'name': 'pause action', 'time': 0.005}
|
||||||
|
],
|
||||||
|
'repeat': -1,
|
||||||
|
'async': True
|
||||||
|
},
|
||||||
|
{'name': 'shoot',
|
||||||
|
'actions': [
|
||||||
|
{'name': 'mouse click action', 'button': 'left', 'type': 1},
|
||||||
|
{'name': 'pause action', 'time': 0.03}
|
||||||
|
],
|
||||||
|
'repeat': 1,
|
||||||
|
'async': False
|
||||||
|
},
|
||||||
|
{'name': 'stop',
|
||||||
|
'actions': [
|
||||||
|
{'name': 'command stop action', 'command name': 'up'},
|
||||||
|
{'name': 'command stop action', 'command name': 'left'},
|
||||||
|
{'name': 'command stop action', 'command name': 'right'},
|
||||||
|
{'name': 'command stop action', 'command name': 'down'},
|
||||||
|
{'name': 'mouse click action', 'button': 'left', 'type': 0}
|
||||||
|
],
|
||||||
|
'repeat': 1,
|
||||||
|
'async': False
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
w_profiles = []
|
||||||
|
w_profiles.append(w_carProfileDict)
|
||||||
|
w_profiles.append(w_airplaneProfileDict)
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
for w_profile in w_profiles:
|
||||||
|
self.ui.profileCbx.addItem(w_profile['name'])
|
||||||
|
w_jsonProfile = json.dumps(w_profile)
|
||||||
|
self.ui.profileCbx.setItemData(i, w_jsonProfile)
|
||||||
|
i = i + 1
|
||||||
|
|
||||||
|
return i
|
||||||
|
|
||||||
|
def slotProfileChanged(self, p_idx):
|
||||||
|
w_jsonProfile = self.ui.profileCbx.itemData(p_idx)
|
||||||
|
if w_jsonProfile != None:
|
||||||
|
self.m_activeProfile = json.loads(w_jsonProfile)
|
||||||
|
self.m_profileExecutor.setProfile(self.m_activeProfile)
|
||||||
|
|
||||||
|
def slotAddNewProfile(self):
|
||||||
|
w_profileEditWnd = ProfileEditWnd(None, self)
|
||||||
|
if w_profileEditWnd.exec() == QDialog.Accepted:
|
||||||
|
w_profile = w_profileEditWnd.m_profile
|
||||||
|
self.ui.profileCbx.addItem(w_profile['name'])
|
||||||
|
w_jsonProfile = json.dumps(w_profile)
|
||||||
|
self.ui.profileCbx.setItemData(self.ui.profileCbx.count()-1, w_jsonProfile)
|
||||||
|
|
||||||
|
def slotEditProfile(self):
|
||||||
|
w_idx = self.ui.profileCbx.currentIndex()
|
||||||
|
w_jsonProfile = self.ui.profileCbx.itemData(w_idx)
|
||||||
|
w_profile = json.loads(w_jsonProfile)
|
||||||
|
w_profileEditWnd = ProfileEditWnd(w_profile, self)
|
||||||
|
if w_profileEditWnd.exec() == QDialog.Accepted:
|
||||||
|
w_profile = w_profileEditWnd.m_profile
|
||||||
|
self.ui.profileCbx.setItemText(w_idx, w_profile['name'])
|
||||||
|
w_jsonProfile = json.dumps(w_profile)
|
||||||
|
self.ui.profileCbx.setItemData(w_idx, w_jsonProfile)
|
||||||
|
|
||||||
|
def slotRemoveProfile(self):
|
||||||
|
w_curIdx = self.ui.profileCbx.currentIndex()
|
||||||
|
if w_curIdx >= 0:
|
||||||
|
self.ui.profileCbx.removeItem(w_curIdx)
|
||||||
|
|
||||||
|
def slotListeningEnabled(self, p_enabled):
|
||||||
|
if p_enabled:
|
||||||
|
self.m_profileExecutor.setEnableListening(True)
|
||||||
|
else:
|
||||||
|
self.m_profileExecutor.setEnableListening(False)
|
||||||
|
|
||||||
|
def slotOK(self):
|
||||||
|
self.saveToDatabase()
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def slotCancel(self):
|
||||||
|
self.close()
|
||||||
|
exit()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
mainWnd = MainWnd()
|
||||||
|
mainWnd.show()
|
||||||
|
sys.exit(app.exec_())
|
|
@ -0,0 +1,167 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>MainWidget</class>
|
||||||
|
<widget class="QWidget" name="MainWidget">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>820</width>
|
||||||
|
<height>166</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>10</pointsize>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>LinVAM</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_2">
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>20</number>
|
||||||
|
</property>
|
||||||
|
<property name="verticalSpacing">
|
||||||
|
<number>20</number>
|
||||||
|
</property>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>20</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="listeningChk">
|
||||||
|
<property name="text">
|
||||||
|
<string>Enable Listening</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="ok">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>130</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>OK</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="cancel">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>130</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Cancel</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>20</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>60</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Profile:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="profileCbx">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>250</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="addBut">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>130</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Add</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="editBut">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>130</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Edit</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="removeBut">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>130</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Remove</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_3">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
|
@ -0,0 +1,117 @@
|
||||||
|
from PyQt5.QtCore import *
|
||||||
|
from PyQt5.QtGui import *
|
||||||
|
from PyQt5.QtWidgets import *
|
||||||
|
from ui_mouseactioneditwnd import Ui_MouseActionEditDialog
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class MouseActionEditWnd(QDialog):
|
||||||
|
def __init__(self, p_mouseAction, p_parent = None):
|
||||||
|
super().__init__(p_parent)
|
||||||
|
self.ui = Ui_MouseActionEditDialog()
|
||||||
|
self.ui.setupUi(self)
|
||||||
|
|
||||||
|
self.ui.ok.clicked.connect(self.slotOK)
|
||||||
|
self.ui.cancel.clicked.connect(super().reject)
|
||||||
|
|
||||||
|
self.m_mouseAction = {}
|
||||||
|
|
||||||
|
if p_mouseAction == None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if p_mouseAction['name'] == 'mouse click action':
|
||||||
|
self.ui.mouseActionTabWidget.setCurrentIndex(0)
|
||||||
|
if p_mouseAction['button'] == 'left':
|
||||||
|
if p_mouseAction['type'] == 10: # click
|
||||||
|
self.ui.leftClick.setChecked(True)
|
||||||
|
elif p_mouseAction['type'] == 1: # down
|
||||||
|
self.ui.leftDown.setChecked(True)
|
||||||
|
elif p_mouseAction['type'] == 0: # up
|
||||||
|
self.ui.leftUp.setChecked(True)
|
||||||
|
elif p_mouseAction['type'] == 11: # double-click
|
||||||
|
self.ui.leftDclick.setChecked(True)
|
||||||
|
elif p_mouseAction['button'] == 'right':
|
||||||
|
if p_mouseAction['type'] == 10:
|
||||||
|
self.ui.rightClick.setChecked(True)
|
||||||
|
elif p_mouseAction['type'] == 1:
|
||||||
|
self.ui.rightDown.setChecked(True)
|
||||||
|
elif p_mouseAction['type'] == 0:
|
||||||
|
self.ui.rightUp.setChecked(True)
|
||||||
|
elif p_mouseAction['type'] == 11: # double-click
|
||||||
|
self.ui.rightDclick.setChecked(True)
|
||||||
|
elif p_mouseAction['button'] == 'middle':
|
||||||
|
if p_mouseAction['type'] == 10:
|
||||||
|
self.ui.middleClick.setChecked(True)
|
||||||
|
elif p_mouseAction['type'] == 1:
|
||||||
|
self.ui.middleDown.setChecked(True)
|
||||||
|
elif p_mouseAction['type'] == 0:
|
||||||
|
self.ui.middleUp.setChecked(True)
|
||||||
|
elif p_mouseAction['type'] == 11: # double-click
|
||||||
|
self.ui.middleDclick.setChecked(True)
|
||||||
|
elif p_mouseAction['name'] == 'mouse move action':
|
||||||
|
self.ui.mouseActionTabWidget.setCurrentIndex(1)
|
||||||
|
if p_mouseAction['absolute'] == True:
|
||||||
|
self.ui.moveTo.setChecked(True)
|
||||||
|
self.ui.xEdit.setText(str(p_mouseAction['x']))
|
||||||
|
self.ui.yEdit.setText(str(p_mouseAction['y']))
|
||||||
|
else:
|
||||||
|
self.ui.moveOffset.setChecked(True)
|
||||||
|
self.ui.xOffsetEdit.setText(str(p_mouseAction['x']))
|
||||||
|
self.ui.yOffsetEdit.setText(str(p_mouseAction['y']))
|
||||||
|
elif p_mouseAction['name'] == 'mouse scroll action':
|
||||||
|
self.ui.mouseActionTabWidget.setCurrentIndex(1)
|
||||||
|
if p_mouseAction['delta'] < 0:
|
||||||
|
self.ui.scrollUp.setChecked(True)
|
||||||
|
self.ui.scrollUpEdit.setText(str(-p_mouseAction['delta']))
|
||||||
|
else:
|
||||||
|
self.ui.scrollDown.setChecked(True)
|
||||||
|
self.ui.scrollDownEdit.setText(str(p_mouseAction['delta']))
|
||||||
|
|
||||||
|
def slotOK(self):
|
||||||
|
if self.ui.mouseActionTabWidget.currentIndex() == 0:
|
||||||
|
if self.ui.leftClick.isChecked():
|
||||||
|
self.m_mouseAction = {'name':'mouse click action', 'button':'left', 'type':10}
|
||||||
|
elif self.ui.rightClick.isChecked():
|
||||||
|
self.m_mouseAction = {'name': 'mouse click action', 'button': 'right', 'type': 10}
|
||||||
|
elif self.ui.middleClick.isChecked():
|
||||||
|
self.m_mouseAction = {'name': 'mouse click action', 'button': 'middle', 'type': 10}
|
||||||
|
|
||||||
|
elif self.ui.leftDclick.isChecked():
|
||||||
|
self.m_mouseAction = {'name':'mouse click action', 'button':'left', 'type':11}
|
||||||
|
elif self.ui.rightDclick.isChecked():
|
||||||
|
self.m_mouseAction = {'name': 'mouse click action', 'button': 'right', 'type': 11}
|
||||||
|
elif self.ui.middleDclick.isChecked():
|
||||||
|
self.m_mouseAction = {'name': 'mouse click action', 'button': 'middle', 'type': 11}
|
||||||
|
|
||||||
|
elif self.ui.leftDown.isChecked():
|
||||||
|
self.m_mouseAction = {'name': 'mouse click action', 'button': 'left', 'type': 1}
|
||||||
|
elif self.ui.rightDown.isChecked():
|
||||||
|
self.m_mouseAction = {'name': 'mouse click action', 'button': 'right', 'type': 1}
|
||||||
|
elif self.ui.middleDown.isChecked():
|
||||||
|
self.m_mouseAction = {'name': 'mouse click action', 'button': 'middle', 'type': 1}
|
||||||
|
|
||||||
|
elif self.ui.leftUp.isChecked():
|
||||||
|
self.m_mouseAction = {'name': 'mouse click action', 'button': 'left', 'type': 0}
|
||||||
|
elif self.ui.rightUp.isChecked():
|
||||||
|
self.m_mouseAction = {'name': 'mouse click action', 'button': 'right', 'type': 0}
|
||||||
|
elif self.ui.middleUp.isChecked():
|
||||||
|
self.m_mouseAction = {'name': 'mouse click action', 'button': 'middle', 'type': 0}
|
||||||
|
else:
|
||||||
|
if self.ui.moveTo.isChecked():
|
||||||
|
self.m_mouseAction['name'] = 'mouse move action'
|
||||||
|
self.m_mouseAction['x'] = int(self.ui.xEdit.text())
|
||||||
|
self.m_mouseAction['y'] = int(self.ui.yEdit.text())
|
||||||
|
self.m_mouseAction['absolute'] = True
|
||||||
|
elif self.ui.moveOffset.isChecked():
|
||||||
|
self.m_mouseAction['name'] = 'mouse move action'
|
||||||
|
self.m_mouseAction['x'] = int(self.ui.xOffsetEdit.text())
|
||||||
|
self.m_mouseAction['y'] = int(self.ui.yOffsetEdit.text())
|
||||||
|
self.m_mouseAction['absolute'] = False
|
||||||
|
elif self.ui.scrollUp.isChecked():
|
||||||
|
self.m_mouseAction['name'] = 'mouse scroll action'
|
||||||
|
self.m_mouseAction['delta'] = -(abs(int(self.ui.scrollUpEdit.text())))
|
||||||
|
elif self.ui.scrollDown.isChecked():
|
||||||
|
self.m_mouseAction['name'] = 'mouse scroll action'
|
||||||
|
self.m_mouseAction['delta'] = abs(int(self.ui.scrollDownEdit.text()))
|
||||||
|
|
||||||
|
super().accept()
|
|
@ -0,0 +1,402 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>MouseActionEditDialog</class>
|
||||||
|
<widget class="QDialog" name="MouseActionEditDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>651</width>
|
||||||
|
<height>268</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>10</pointsize>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Dialog</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<property name="verticalSpacing">
|
||||||
|
<number>20</number>
|
||||||
|
</property>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QTabWidget" name="mouseActionTabWidget">
|
||||||
|
<property name="currentIndex">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="clickActionTab">
|
||||||
|
<attribute name="title">
|
||||||
|
<string>Click</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_2">
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>20</number>
|
||||||
|
</property>
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>9</number>
|
||||||
|
</property>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QRadioButton" name="leftClick">
|
||||||
|
<property name="text">
|
||||||
|
<string>Click left button</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QRadioButton" name="rightClick">
|
||||||
|
<property name="text">
|
||||||
|
<string>Click right button</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="2">
|
||||||
|
<widget class="QRadioButton" name="middleClick">
|
||||||
|
<property name="text">
|
||||||
|
<string>Click middle button</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QRadioButton" name="leftDclick">
|
||||||
|
<property name="text">
|
||||||
|
<string>Double-click left button</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QRadioButton" name="rightDclick">
|
||||||
|
<property name="text">
|
||||||
|
<string>Double-click right button</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QRadioButton" name="middleDclick">
|
||||||
|
<property name="text">
|
||||||
|
<string>Double-click middle button</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QRadioButton" name="leftDown">
|
||||||
|
<property name="text">
|
||||||
|
<string>left button down</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QRadioButton" name="rightDown">
|
||||||
|
<property name="text">
|
||||||
|
<string>right button down</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="QRadioButton" name="middleDown">
|
||||||
|
<property name="text">
|
||||||
|
<string>middle button down</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QRadioButton" name="leftUp">
|
||||||
|
<property name="text">
|
||||||
|
<string>left button up</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="1">
|
||||||
|
<widget class="QRadioButton" name="rightUp">
|
||||||
|
<property name="text">
|
||||||
|
<string>right button up</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="2">
|
||||||
|
<widget class="QRadioButton" name="middleUp">
|
||||||
|
<property name="text">
|
||||||
|
<string>middle button up</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="moveActionTab">
|
||||||
|
<attribute name="title">
|
||||||
|
<string>Move</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_3">
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>20</number>
|
||||||
|
</property>
|
||||||
|
<item row="1" column="0" colspan="3">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<item>
|
||||||
|
<widget class="QRadioButton" name="moveTo">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>100</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Move to:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>25</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>X</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="xEdit"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_3">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>25</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Y</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="yEdit"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0" colspan="6">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
|
<item>
|
||||||
|
<widget class="QRadioButton" name="moveOffset">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>100</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Move offset</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoExclusive">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_5">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>25</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>X</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="xOffsetEdit"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_4">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>70</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>pixels</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_6">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>25</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Y</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="yOffsetEdit"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_7">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>70</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>pixels</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||||
|
<item>
|
||||||
|
<widget class="QRadioButton" name="scrollUp">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>100</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Scroll up</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoExclusive">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="scrollUpEdit"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_8">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>80</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>pixels</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QRadioButton" name="scrollDown">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>110</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Scroll down</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoExclusive">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="scrollDownEdit"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_9">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>80</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>pixels</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>20</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="ok">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>130</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>OK</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoDefault">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="cancel">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>130</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Cancel</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoDefault">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
|
@ -0,0 +1,27 @@
|
||||||
|
from PyQt5.QtCore import *
|
||||||
|
from PyQt5.QtGui import *
|
||||||
|
from PyQt5.QtWidgets import *
|
||||||
|
from ui_pauseactioneditwnd import Ui_PauseActionEditDialog
|
||||||
|
import json
|
||||||
|
|
||||||
|
class PauseActionEditWnd(QDialog):
|
||||||
|
def __init__(self, p_pauseAction, p_parent = None):
|
||||||
|
super().__init__(p_parent)
|
||||||
|
self.ui = Ui_PauseActionEditDialog()
|
||||||
|
self.ui.setupUi(self)
|
||||||
|
|
||||||
|
self.ui.ok.clicked.connect(self.slotOK)
|
||||||
|
self.ui.cancel.clicked.connect(super().reject)
|
||||||
|
|
||||||
|
self.m_pauseAction = {}
|
||||||
|
|
||||||
|
if p_pauseAction == None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.ui.secondEdit.setText(str(p_pauseAction['time']))
|
||||||
|
|
||||||
|
def slotOK(self):
|
||||||
|
self.m_pauseAction['name'] = 'pause action'
|
||||||
|
self.m_pauseAction['time'] = float(self.ui.secondEdit.text())
|
||||||
|
super().accept()
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>PauseActionEditDialog</class>
|
||||||
|
<widget class="QDialog" name="PauseActionEditDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>271</width>
|
||||||
|
<height>133</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>10</pointsize>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Pause Action Edit Dialog</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Pause for </string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="secondEdit"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>seconds</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>20</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="ok">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>100</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>OK</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="cancel">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>100</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Cancel</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
|
@ -0,0 +1,221 @@
|
||||||
|
from PyQt5.QtCore import *
|
||||||
|
from PyQt5.QtGui import *
|
||||||
|
from PyQt5.QtWidgets import *
|
||||||
|
from ui_profileeditwnd import Ui_ProfileEditDialog
|
||||||
|
from commandeditwnd import CommandEditWnd
|
||||||
|
import json
|
||||||
|
from profileexecutor import *
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class ProfileEditWnd(QDialog):
|
||||||
|
def __init__(self, p_profile, p_parent = None):
|
||||||
|
super().__init__(p_parent)
|
||||||
|
self.m_profiles = []
|
||||||
|
self.ui = Ui_ProfileEditDialog()
|
||||||
|
self.ui.setupUi(self)
|
||||||
|
self.m_profile = {}
|
||||||
|
|
||||||
|
self.ui.cmdTable.setHorizontalHeaderLabels(('Spoken command', 'Actions'))
|
||||||
|
self.ui.cmdTable.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
|
self.ui.cmdTable.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||||
|
|
||||||
|
self.ui.cmdTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Interactive)
|
||||||
|
self.ui.cmdTable.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
|
||||||
|
|
||||||
|
# define actions here
|
||||||
|
self.ui.newCmd.clicked.connect(self.slotNewCmd)
|
||||||
|
self.ui.editCmd.clicked.connect(self.slotEditCmd)
|
||||||
|
self.ui.cmdTable.doubleClicked.connect(self.slotEditCmd)
|
||||||
|
self.ui.deleteCmd.clicked.connect(self.slotDeleteCmd)
|
||||||
|
self.ui.ok.clicked.connect(self.slotOK)
|
||||||
|
self.ui.cancel.clicked.connect(self.slotCancel)
|
||||||
|
|
||||||
|
if p_profile == None or p_profile == {}:
|
||||||
|
self.ui.cmdTable.setRowCount(0)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.ui.profileNameEdit.setText(p_profile['name'])
|
||||||
|
w_commands = p_profile['commands']
|
||||||
|
self.ui.cmdTable.setRowCount(len(w_commands))
|
||||||
|
i = 0
|
||||||
|
for w_command in w_commands:
|
||||||
|
self.ui.cmdTable.setItem(i, 0, QTableWidgetItem(w_command['name']))
|
||||||
|
w_text = json.dumps(w_command)
|
||||||
|
w_item = QTableWidgetItem(w_text)
|
||||||
|
w_item.setData(Qt.UserRole, json.dumps(w_command))
|
||||||
|
self.ui.cmdTable.setItem(i, 1, w_item)
|
||||||
|
i = i + 1
|
||||||
|
|
||||||
|
QTimer.singleShot(100, self.ui.cmdTable.resizeRowsToContents)
|
||||||
|
|
||||||
|
# def activateProfile(self, p_profile):
|
||||||
|
# if p_profile == None or p_profile == {}:
|
||||||
|
# self.ui.cmdTable.setRowCount(0)
|
||||||
|
# return
|
||||||
|
#
|
||||||
|
# w_commands = p_profile['commands']
|
||||||
|
# self.ui.cmdTable.setRowCount(len(w_commands))
|
||||||
|
# i = 0
|
||||||
|
# for w_command in w_commands:
|
||||||
|
# self.ui.cmdTable.setItem(i, 0, QTableWidgetItem(w_command['name']))
|
||||||
|
# w_text = json.dumps(w_command)
|
||||||
|
# w_item = QTableWidgetItem(w_text)
|
||||||
|
# w_item.setData(Qt.UserRole, json.dumps(w_command))
|
||||||
|
# self.ui.cmdTable.setItem(i, 1, w_item)
|
||||||
|
# i = i + 1
|
||||||
|
# self.ui.cmdTable.resizeRowsToContents()
|
||||||
|
|
||||||
|
# def updateActiveProfile(self):
|
||||||
|
# w_idx = self.m_curProfileIdx
|
||||||
|
# if w_idx < 0:
|
||||||
|
# return
|
||||||
|
#
|
||||||
|
# w_profile = {}
|
||||||
|
# w_profile['name'] = self.ui.profileCbx.itemText(w_idx)
|
||||||
|
# w_commands = []
|
||||||
|
#
|
||||||
|
# w_commandCnt = self.ui.cmdTable.rowCount()
|
||||||
|
# for w_i in range(w_commandCnt):
|
||||||
|
# w_jsonCommand = self.ui.cmdTable.item(w_i, 1).data(Qt.UserRole)
|
||||||
|
# w_command = json.loads(w_jsonCommand)
|
||||||
|
# w_commands.append(w_command)
|
||||||
|
# w_profile['commands'] = w_commands
|
||||||
|
# w_jsonProfile = json.dumps(w_profile)
|
||||||
|
# self.ui.profileCbx.setItemData(w_idx, w_jsonProfile)
|
||||||
|
|
||||||
|
# def slotSelChanged(self, p_idx):
|
||||||
|
# self.updateActiveProfile()
|
||||||
|
# w_jsonProfile = self.ui.profileCbx.itemData(p_idx)
|
||||||
|
#
|
||||||
|
# w_profile = {}
|
||||||
|
# if w_jsonProfile != None:
|
||||||
|
# w_profile = json.loads(w_jsonProfile)
|
||||||
|
#
|
||||||
|
# self.activateProfile(w_profile)
|
||||||
|
#
|
||||||
|
# self.m_curProfileIdx = p_idx
|
||||||
|
|
||||||
|
# def importProfile(self, p_profile, p_update = True):
|
||||||
|
# if p_profile == None or p_profile == {}:
|
||||||
|
# return
|
||||||
|
#
|
||||||
|
# w_profileCnt = self.ui.profileCbx.count()
|
||||||
|
# for w_idx in range(w_profileCnt):
|
||||||
|
# w_jsonProfile = self.ui.profileCbx.itemData(w_idx)
|
||||||
|
# if w_jsonProfile == None:
|
||||||
|
# continue
|
||||||
|
#
|
||||||
|
# w_profile = json.loads(w_jsonProfile)
|
||||||
|
# if p_profile['name'] == w_profile['name']:
|
||||||
|
# if p_update:
|
||||||
|
# w_jsonProfile = json.dumps(p_profile)
|
||||||
|
# self.ui.profileCbx.setItemData(w_idx, w_jsonProfile)
|
||||||
|
# return True
|
||||||
|
# return False
|
||||||
|
#
|
||||||
|
# self.ui.profileCbx.addItem(p_profile['name'])
|
||||||
|
# w_jsonProfile = json.dumps(p_profile)
|
||||||
|
# self.ui.profileCbx.setItemData(w_profileCnt, w_jsonProfile)
|
||||||
|
# return True
|
||||||
|
|
||||||
|
# def slotAddNewProfile(self):
|
||||||
|
# text, okPressed = QInputDialog.getText(self, "Get Profile Name", "Profile name:", QLineEdit.Normal, "")
|
||||||
|
# if okPressed and text != '':
|
||||||
|
# w_profile = {}
|
||||||
|
# w_profile['name'] = text
|
||||||
|
# w_profile['commands'] = []
|
||||||
|
# if self.importProfile(w_profile, False) == False:
|
||||||
|
# QMessageBox.critical(None, 'Error', 'Adding a new profile was failed')
|
||||||
|
# return
|
||||||
|
# self.ui.profileCbx.setCurrentIndex(self.ui.profileCbx.count()-1)
|
||||||
|
|
||||||
|
# def slotRemoveProfile(self):
|
||||||
|
# w_curIdx = self.ui.profileCbx.currentIndex()
|
||||||
|
# if w_curIdx >= 0:
|
||||||
|
# self.ui.cmdTable.setRowCount(0)
|
||||||
|
# self.m_curProfileIdx = -1
|
||||||
|
# self.ui.profileCbx.removeItem(w_curIdx)
|
||||||
|
#
|
||||||
|
# def slotRenameProfile(self):
|
||||||
|
# w_curIdx = self.ui.profileCbx.currentIndex()
|
||||||
|
# if w_curIdx >= 0:
|
||||||
|
# text, okPressed = QInputDialog.getText(self, "Input Dialog", "Profile name:",
|
||||||
|
# QLineEdit.Normal, self.ui.profileCbx.itemText(w_curIdx))
|
||||||
|
# if okPressed and text != '':
|
||||||
|
# self.ui.profileCbx.setItemText(w_curIdx, text)
|
||||||
|
# w_jsonProfile = self.ui.profileCbx.itemData(w_curIdx)
|
||||||
|
# w_profile = json.loads(w_jsonProfile)
|
||||||
|
# w_profile['name'] = text
|
||||||
|
# w_jsonProfile = json.dumps(w_profile)
|
||||||
|
# self.ui.profileCbx.setItemData(w_curIdx, w_jsonProfile)
|
||||||
|
|
||||||
|
def importCommand(self, p_command, p_update):
|
||||||
|
w_commandCnt = self.ui.cmdTable.rowCount()
|
||||||
|
for w_i in range(w_commandCnt):
|
||||||
|
w_jsonCommand = self.ui.cmdTable.item(w_i, 1).data(Qt.UserRole)
|
||||||
|
w_command = json.loads(w_jsonCommand)
|
||||||
|
if w_command['name'] == p_command['name']:
|
||||||
|
if p_update:
|
||||||
|
w_text = json.dumps(p_command)
|
||||||
|
w_item = QTableWidgetItem(w_text)
|
||||||
|
w_item.setData(Qt.UserRole, json.dumps(p_command))
|
||||||
|
self.ui.cmdTable.setItem(w_i, 1, w_item)
|
||||||
|
self.ui.cmdTable.resizeRowsToContents()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
w_rowCnt = self.ui.cmdTable.rowCount()
|
||||||
|
self.ui.cmdTable.setRowCount(w_rowCnt + 1)
|
||||||
|
self.ui.cmdTable.setItem(w_rowCnt, 0, QTableWidgetItem(p_command['name']))
|
||||||
|
w_text = json.dumps(p_command)
|
||||||
|
w_item = QTableWidgetItem(w_text)
|
||||||
|
w_item.setData(Qt.UserRole, json.dumps(p_command))
|
||||||
|
self.ui.cmdTable.setItem(w_rowCnt, 1, w_item)
|
||||||
|
self.ui.cmdTable.resizeRowsToContents()
|
||||||
|
|
||||||
|
def slotNewCmd(self):
|
||||||
|
w_cmdEditWnd = CommandEditWnd(None, self)
|
||||||
|
if w_cmdEditWnd.exec_() == QDialog.Accepted:
|
||||||
|
if self.importCommand(w_cmdEditWnd.m_command, False) == False:
|
||||||
|
QMessageBox.critical(None, 'Error', 'Adding a new command was failed')
|
||||||
|
return
|
||||||
|
self.ui.cmdTable.selectRow(self.ui.cmdTable.rowCount() - 1)
|
||||||
|
self.ui.cmdTable.setFocus()
|
||||||
|
|
||||||
|
def slotEditCmd(self):
|
||||||
|
w_modelIdxs = self.ui.cmdTable.selectionModel().selectedRows()
|
||||||
|
if len(w_modelIdxs) == 0:
|
||||||
|
return
|
||||||
|
w_modelIdx = w_modelIdxs[0]
|
||||||
|
w_jsonCommand = self.ui.cmdTable.item(w_modelIdx.row(), 1).data(Qt.UserRole)
|
||||||
|
w_command = json.loads(w_jsonCommand)
|
||||||
|
|
||||||
|
w_cmdEditWnd = CommandEditWnd(w_command, self)
|
||||||
|
if w_cmdEditWnd.exec_() == QDialog.Accepted:
|
||||||
|
self.importCommand(w_cmdEditWnd.m_command, True)
|
||||||
|
self.ui.cmdTable.resizeRowsToContents()
|
||||||
|
|
||||||
|
def slotDeleteCmd(self):
|
||||||
|
w_modelIdxs = self.ui.cmdTable.selectionModel().selectedRows()
|
||||||
|
i = 0
|
||||||
|
for w_modelIdx in sorted(w_modelIdxs):
|
||||||
|
self.ui.cmdTable.removeRow(w_modelIdx.row() - i)
|
||||||
|
i = i + 1
|
||||||
|
self.ui.cmdTable.setFocus()
|
||||||
|
|
||||||
|
def slotOK(self):
|
||||||
|
self.m_profile = {}
|
||||||
|
self.m_profile['name'] = self.ui.profileNameEdit.text()
|
||||||
|
w_commands = []
|
||||||
|
|
||||||
|
w_commandCnt = self.ui.cmdTable.rowCount()
|
||||||
|
for w_i in range(w_commandCnt):
|
||||||
|
w_jsonCommand = self.ui.cmdTable.item(w_i, 1).data(Qt.UserRole)
|
||||||
|
w_command = json.loads(w_jsonCommand)
|
||||||
|
w_commands.append(w_command)
|
||||||
|
self.m_profile['commands'] = w_commands
|
||||||
|
|
||||||
|
super().accept()
|
||||||
|
|
||||||
|
def slotCancel(self):
|
||||||
|
super().reject()
|
|
@ -0,0 +1,219 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>ProfileEditDialog</class>
|
||||||
|
<widget class="QDialog" name="ProfileEditDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>1191</width>
|
||||||
|
<height>591</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Dialog</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_2">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>60</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Profile:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="profileNameEdit"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_3">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QGroupBox" name="groupBox">
|
||||||
|
<property name="title">
|
||||||
|
<string>Commands</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QTableWidget" name="cmdTable">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>0</width>
|
||||||
|
<height>200</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>11</pointsize>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="editTriggers">
|
||||||
|
<set>QAbstractItemView::NoEditTriggers</set>
|
||||||
|
</property>
|
||||||
|
<property name="dragDropOverwriteMode">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="selectionMode">
|
||||||
|
<enum>QAbstractItemView::SingleSelection</enum>
|
||||||
|
</property>
|
||||||
|
<property name="selectionBehavior">
|
||||||
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
|
</property>
|
||||||
|
<property name="columnCount">
|
||||||
|
<number>2</number>
|
||||||
|
</property>
|
||||||
|
<attribute name="horizontalHeaderDefaultSectionSize">
|
||||||
|
<number>160</number>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="horizontalHeaderMinimumSectionSize">
|
||||||
|
<number>160</number>
|
||||||
|
</attribute>
|
||||||
|
<column/>
|
||||||
|
<column/>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>20</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_4">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="newCmd">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>130</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>New</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="editCmd">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>130</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Edit</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="deleteCmd">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>130</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Delete</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>20</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="ok">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>130</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>OK</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="cancel">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>130</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Cancel</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
|
@ -0,0 +1,318 @@
|
||||||
|
import keyboard
|
||||||
|
from pynput.mouse import Button, Controller
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
|
||||||
|
class ProfileExecutor(threading.Thread):
|
||||||
|
mouse = Controller()
|
||||||
|
|
||||||
|
def __init__(self, p_profile = None):
|
||||||
|
threading.Thread.__init__(self)
|
||||||
|
self.m_profile = p_profile
|
||||||
|
self.m_stop = False
|
||||||
|
self.m_listening = True
|
||||||
|
self.m_cmdThreads = {}
|
||||||
|
|
||||||
|
def setProfile(self, p_profile):
|
||||||
|
self.m_profile = p_profile
|
||||||
|
|
||||||
|
def setEnableListening(self, p_enable):
|
||||||
|
self.m_listening = p_enable
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while self.m_stop != True:
|
||||||
|
keyboard.wait('ctrl+alt')
|
||||||
|
if self.m_listening != True:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for i in range(3):
|
||||||
|
self.doCommand('up')
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
self.doCommand('left')
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
self.doCommand('right')
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
self.doCommand('stop')
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.m_stop = True
|
||||||
|
|
||||||
|
def doAction(self, p_action):
|
||||||
|
# {'name': 'key action', 'key': 'left', 'type': 0}
|
||||||
|
# {'name': 'pause action', 'time': 0.03}
|
||||||
|
# {'name': 'command stop action', 'command name': 'down'}
|
||||||
|
# {'name': 'mouse move action', 'x':5, 'y':0, 'absolute': False}
|
||||||
|
# {'name': 'mouse click action', 'button': 'left', 'type': 0}
|
||||||
|
# {'name': 'mouse wheel action', 'delta':10}
|
||||||
|
w_actionName = p_action['name']
|
||||||
|
if w_actionName == 'key action':
|
||||||
|
w_key = p_action['key']
|
||||||
|
w_type = p_action['type']
|
||||||
|
if w_type == 1:
|
||||||
|
keyboard.press(w_key)
|
||||||
|
print("pressed key: ", w_key)
|
||||||
|
elif w_type == 0:
|
||||||
|
keyboard.release(w_key)
|
||||||
|
print("released key: ", w_key)
|
||||||
|
elif w_type == 10:
|
||||||
|
keyboard.press(w_key)
|
||||||
|
keyboard.release(w_key)
|
||||||
|
print("pressed and released key: ", w_key)
|
||||||
|
elif w_actionName == 'pause action':
|
||||||
|
time.sleep(p_action['time'])
|
||||||
|
elif w_actionName == 'command stop action':
|
||||||
|
self.stopCommand(p_action['command name'])
|
||||||
|
elif w_actionName == 'mouse move action':
|
||||||
|
if p_action['absolute']:
|
||||||
|
ProfileExecutor.mouse.position([p_action['x'], p_action['y']])
|
||||||
|
else:
|
||||||
|
ProfileExecutor.mouse.move(p_action['x'], p_action['y'])
|
||||||
|
elif w_actionName == 'mouse click action':
|
||||||
|
w_type = p_action['type']
|
||||||
|
w_button = p_action['button']
|
||||||
|
if w_type == 1:
|
||||||
|
if w_button == 'left':
|
||||||
|
ProfileExecutor.mouse.press(Button.left)
|
||||||
|
elif w_button == 'middle':
|
||||||
|
ProfileExecutor.mouse.press(Button.middle)
|
||||||
|
elif w_button == 'right':
|
||||||
|
ProfileExecutor.mouse.press(Button.right)
|
||||||
|
print("pressed mouse button: ", w_button)
|
||||||
|
elif w_type == 0:
|
||||||
|
if w_button == 'left':
|
||||||
|
ProfileExecutor.mouse.release(Button.left)
|
||||||
|
elif w_button == 'middle':
|
||||||
|
ProfileExecutor.mouse.release(Button.middle)
|
||||||
|
elif w_button == 'right':
|
||||||
|
ProfileExecutor.mouse.release(Button.right)
|
||||||
|
print("released mouse button: ", w_button)
|
||||||
|
elif w_type == 10:
|
||||||
|
if w_button == 'left':
|
||||||
|
ProfileExecutor.mouse.click(Button.left)
|
||||||
|
elif w_button == 'middle':
|
||||||
|
ProfileExecutor.mouse.click(Button.middle)
|
||||||
|
elif w_button == 'right':
|
||||||
|
ProfileExecutor.mouse.click(Button.right)
|
||||||
|
print("pressed and released mouse button: ", w_button)
|
||||||
|
elif w_actionName == 'mouse scroll action':
|
||||||
|
ProfileExecutor.mouse.scroll(0, p_action['delta'])
|
||||||
|
|
||||||
|
class CommandThread(threading.Thread):
|
||||||
|
def __init__(self, p_ProfileExecutor, p_actions, p_repeat):
|
||||||
|
threading.Thread.__init__(self)
|
||||||
|
self.ProfileExecutor = p_ProfileExecutor
|
||||||
|
self.m_actions = p_actions
|
||||||
|
self.m_repeat = p_repeat
|
||||||
|
self.m_stop = False
|
||||||
|
def run(self):
|
||||||
|
w_repeat = self.m_repeat
|
||||||
|
while self.m_stop != True:
|
||||||
|
for w_action in self.m_actions:
|
||||||
|
self.ProfileExecutor.doAction(w_action)
|
||||||
|
w_repeat = w_repeat - 1
|
||||||
|
if w_repeat == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.m_stop = True
|
||||||
|
threading.Thread.join(self)
|
||||||
|
|
||||||
|
def doCommand(self, p_cmdName):
|
||||||
|
if self.m_profile == None:
|
||||||
|
return
|
||||||
|
|
||||||
|
w_commands = self.m_profile['commands']
|
||||||
|
flag = False
|
||||||
|
for w_command in w_commands:
|
||||||
|
if w_command['name'] == p_cmdName:
|
||||||
|
flag = True
|
||||||
|
break
|
||||||
|
if flag == False:
|
||||||
|
return
|
||||||
|
|
||||||
|
w_actions = w_command['actions']
|
||||||
|
w_async = w_command['async']
|
||||||
|
|
||||||
|
if w_async == False:
|
||||||
|
w_repeat = w_command['repeat']
|
||||||
|
if w_repeat < 1:
|
||||||
|
w_repeat = 1
|
||||||
|
while True:
|
||||||
|
for w_action in w_command['actions']:
|
||||||
|
self.doAction(w_action)
|
||||||
|
w_repeat = w_repeat - 1
|
||||||
|
if w_repeat == 0:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
w_cmdThread = ProfileExecutor.CommandThread(self, w_actions, w_command['repeat'])
|
||||||
|
w_cmdThread.start()
|
||||||
|
self.m_cmdThreads[p_cmdName] = w_cmdThread
|
||||||
|
|
||||||
|
def stopCommand(self, p_cmdName):
|
||||||
|
if p_cmdName in self.m_cmdThreads.keys():
|
||||||
|
self.m_cmdThreads[p_cmdName].stop()
|
||||||
|
del self.m_cmdThreads[p_cmdName]
|
||||||
|
|
||||||
|
# def carGameTest():
|
||||||
|
# w_profileDict = {
|
||||||
|
# "name": "car game",
|
||||||
|
# "commands": [
|
||||||
|
# {'name': 'up',
|
||||||
|
# 'actions': [
|
||||||
|
# {'name': 'key action', 'key': 'up', 'type': 1},
|
||||||
|
# {'name': 'pause action', 'time': 0.03}
|
||||||
|
# ],
|
||||||
|
# 'repeat': 1,
|
||||||
|
# 'async': False
|
||||||
|
# },
|
||||||
|
# {'name': 'left',
|
||||||
|
# 'actions': [{'name': 'key action', 'key': 'right', 'type': 0},
|
||||||
|
# {'name': 'pause action', 'time': 0.03},
|
||||||
|
# {'name': 'key action', 'key': 'left', 'type': 1},
|
||||||
|
# {'name': 'pause action', 'time': 0.03}
|
||||||
|
# ],
|
||||||
|
# 'repeat': 1,
|
||||||
|
# 'async': False
|
||||||
|
# },
|
||||||
|
# {'name': 'right',
|
||||||
|
# 'actions': [{'name': 'key action', 'key': 'left', 'type': 0},
|
||||||
|
# {'name': 'pause action', 'time': 0.03},
|
||||||
|
# {'name': 'key action', 'key': 'right', 'type': 1},
|
||||||
|
# {'name': 'pause action', 'time': 0.03}
|
||||||
|
# ],
|
||||||
|
# 'repeat': 1,
|
||||||
|
# 'async': False
|
||||||
|
# },
|
||||||
|
# {'name': 'stop',
|
||||||
|
# 'actions': [
|
||||||
|
# {'name': 'key action', 'key': 'left', 'type': 0},
|
||||||
|
# {'name': 'pause action', 'time': 0.03},
|
||||||
|
# {'name': 'key action', 'key': 'right', 'type': 0},
|
||||||
|
# {'name': 'pause action', 'time': 0.03},
|
||||||
|
# {'name': 'key action', 'key': 'up', 'type': 0},
|
||||||
|
# {'name': 'pause action', 'time': 0.03}
|
||||||
|
# ],
|
||||||
|
# 'repeat': 1,
|
||||||
|
# 'async': False
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# w_ProfileExecutor = ProfileExecutor(w_profileDict)
|
||||||
|
#
|
||||||
|
# print("Move to the target window and press spacebar")
|
||||||
|
# keyboard.wait('space')
|
||||||
|
#
|
||||||
|
# print("Started !")
|
||||||
|
#
|
||||||
|
# for i in range(5):
|
||||||
|
# w_ProfileExecutor.doCommand('up')
|
||||||
|
# time.sleep(0.5)
|
||||||
|
#
|
||||||
|
# w_ProfileExecutor.doCommand('left')
|
||||||
|
# time.sleep(0.5)
|
||||||
|
#
|
||||||
|
# w_ProfileExecutor.doCommand('right')
|
||||||
|
# time.sleep(0.5)
|
||||||
|
#
|
||||||
|
# w_ProfileExecutor.doCommand('stop')
|
||||||
|
#
|
||||||
|
# def airplaneGameTest():
|
||||||
|
# w_profileDict = {
|
||||||
|
# "name": "airplane game",
|
||||||
|
# "commands": [
|
||||||
|
# {'name': 'up',
|
||||||
|
# 'actions': [
|
||||||
|
# {'name': 'command stop action', 'command name': 'down'},
|
||||||
|
# {'name': 'mouse move action', 'x':0, 'y':-5, 'absolute': False},
|
||||||
|
# {'name': 'pause action', 'time': 0.01}
|
||||||
|
# ],
|
||||||
|
# 'repeat': -1,
|
||||||
|
# 'async': True
|
||||||
|
# },
|
||||||
|
# {'name': 'left',
|
||||||
|
# 'actions': [
|
||||||
|
# {'name': 'command stop action', 'command name':'right'},
|
||||||
|
# {'name': 'mouse move action', 'x':-5, 'y':0, 'absolute': False},
|
||||||
|
# {'name': 'pause action', 'time': 0.005}
|
||||||
|
# ],
|
||||||
|
# 'repeat': -1,
|
||||||
|
# 'async': True
|
||||||
|
# },
|
||||||
|
# {'name': 'right',
|
||||||
|
# 'actions': [
|
||||||
|
# {'name': 'command stop action', 'command name': 'left'},
|
||||||
|
# {'name': 'mouse move action', 'x':5, 'y':0, 'absolute': False},
|
||||||
|
# {'name': 'pause action', 'time': 0.005}
|
||||||
|
# ],
|
||||||
|
# 'repeat': -1,
|
||||||
|
# 'async': True
|
||||||
|
# },
|
||||||
|
# {'name': 'down',
|
||||||
|
# 'actions': [
|
||||||
|
# {'name': 'command stop action', 'command name': 'up'},
|
||||||
|
# {'name': 'mouse move action', 'x':0, 'y':5, 'absolute': False},
|
||||||
|
# {'name': 'pause action', 'time': 0.005}
|
||||||
|
# ],
|
||||||
|
# 'repeat': -1,
|
||||||
|
# 'async': True
|
||||||
|
# },
|
||||||
|
# {'name': 'shoot',
|
||||||
|
# 'actions': [
|
||||||
|
# {'name': 'mouse click action', 'button':'left', 'type': 1},
|
||||||
|
# {'name': 'pause action', 'time': 0.03}
|
||||||
|
# ],
|
||||||
|
# 'repeat': 1,
|
||||||
|
# 'async': False
|
||||||
|
# },
|
||||||
|
# {'name': 'stop',
|
||||||
|
# 'actions': [
|
||||||
|
# {'name': 'command stop action', 'command name': 'up'},
|
||||||
|
# {'name': 'command stop action', 'command name': 'left'},
|
||||||
|
# {'name': 'command stop action', 'command name': 'right'},
|
||||||
|
# {'name': 'command stop action', 'command name': 'down'},
|
||||||
|
# {'name': 'mouse click action', 'button': 'left', 'type': 0}
|
||||||
|
# ],
|
||||||
|
# 'repeat': 1,
|
||||||
|
# 'async': False
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# w_ProfileExecutor = ProfileExecutor(w_profileDict)
|
||||||
|
#
|
||||||
|
# print("Move to the target window and press the spacebar to start")
|
||||||
|
# keyboard.wait('space')
|
||||||
|
#
|
||||||
|
# time.sleep(1)
|
||||||
|
#
|
||||||
|
# print("Started !")
|
||||||
|
#
|
||||||
|
# for i in range(3):
|
||||||
|
# w_ProfileExecutor.doCommand('shoot')
|
||||||
|
# time.sleep(1)
|
||||||
|
#
|
||||||
|
# w_ProfileExecutor.doCommand('up')
|
||||||
|
# time.sleep(1)
|
||||||
|
#
|
||||||
|
# w_ProfileExecutor.doCommand('down')
|
||||||
|
# time.sleep(0.5)
|
||||||
|
#
|
||||||
|
# w_ProfileExecutor.stopCommand('down')
|
||||||
|
#
|
||||||
|
# w_ProfileExecutor.doCommand('left')
|
||||||
|
# time.sleep(0.5)
|
||||||
|
#
|
||||||
|
# w_ProfileExecutor.doCommand('right')
|
||||||
|
# time.sleep(0.5)
|
||||||
|
#
|
||||||
|
# w_ProfileExecutor.doCommand('stop')
|
||||||
|
#
|
||||||
|
# if __name__ == "__main__":
|
||||||
|
# carGameTest()
|
||||||
|
# # airplaneGameTest()
|
Binary file not shown.
|
@ -0,0 +1,41 @@
|
||||||
|
# coding=utf-8
|
||||||
|
# pynput
|
||||||
|
# Copyright (C) 2015-2018 Moses Palmér
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
# later version.
|
||||||
|
#
|
||||||
|
# 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 Lesser General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
The main *pynput* module.
|
||||||
|
|
||||||
|
This module imports ``keyboard`` and ``mouse``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _logger(cls):
|
||||||
|
"""Creates a logger with a name suitable for a specific class.
|
||||||
|
|
||||||
|
This function takes into account that implementations for classes reside in
|
||||||
|
platform dependent modules, and thus removes the final part of the module
|
||||||
|
name.
|
||||||
|
|
||||||
|
:param type cls: The class for which to create a logger.
|
||||||
|
|
||||||
|
:return: a logger
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
return logging.getLogger('{}.{}'.format(
|
||||||
|
'.'.join(cls.__module__.split('.', 2)[:2]),
|
||||||
|
cls.__name__))
|
||||||
|
|
||||||
|
|
||||||
|
# from . import keyboard
|
||||||
|
from . import mouse
|
Binary file not shown.
|
@ -0,0 +1,19 @@
|
||||||
|
# coding=utf-8
|
||||||
|
# pystray
|
||||||
|
# Copyright (C) 2015-2018 Moses Palmér
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
# later version.
|
||||||
|
#
|
||||||
|
# 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 Lesser General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
__author__ = u'Moses Palmér'
|
||||||
|
__version__ = (1, 4, 2)
|
|
@ -0,0 +1,296 @@
|
||||||
|
# coding=utf-8
|
||||||
|
# pynput
|
||||||
|
# Copyright (C) 2015-2018 Moses Palmér
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
# later version.
|
||||||
|
#
|
||||||
|
# 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 Lesser General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
General utility functions and classes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=R0903
|
||||||
|
# We implement minimal mixins
|
||||||
|
|
||||||
|
# pylint: disable=W0212
|
||||||
|
# We implement an internal API
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import functools
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from six.moves import queue
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractListener(threading.Thread):
|
||||||
|
"""A class implementing the basic behaviour for event listeners.
|
||||||
|
|
||||||
|
Instances of this class can be used as context managers. This is equivalent
|
||||||
|
to the following code::
|
||||||
|
|
||||||
|
listener.start()
|
||||||
|
listener.wait()
|
||||||
|
try:
|
||||||
|
with_statements()
|
||||||
|
finally:
|
||||||
|
listener.stop()
|
||||||
|
|
||||||
|
Actual implementations of this class must set the attribute ``_log``, which
|
||||||
|
must be an instance of :class:`logging.Logger`.
|
||||||
|
|
||||||
|
:param bool suppress: Whether to suppress events. Setting this to ``True``
|
||||||
|
will prevent the input events from being passed to the rest of the
|
||||||
|
system.
|
||||||
|
|
||||||
|
:param kwargs: A mapping from callback attribute to callback handler. All
|
||||||
|
handlers will be wrapped in a function reading the return value of the
|
||||||
|
callback, and if it ``is False``, raising :class:`StopException`.
|
||||||
|
|
||||||
|
Any callback that is falsy will be ignored.
|
||||||
|
"""
|
||||||
|
class StopException(Exception):
|
||||||
|
"""If an event listener callback raises this exception, the current
|
||||||
|
listener is stopped.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
#: Exceptions that are handled outside of the emitter and should thus not be
|
||||||
|
#: passed through the queue
|
||||||
|
_HANDLED_EXCEPTIONS = tuple()
|
||||||
|
|
||||||
|
def __init__(self, suppress=False, **kwargs):
|
||||||
|
super(AbstractListener, self).__init__()
|
||||||
|
|
||||||
|
def wrapper(f):
|
||||||
|
def inner(*args):
|
||||||
|
if f(*args) is False:
|
||||||
|
raise self.StopException()
|
||||||
|
return inner
|
||||||
|
|
||||||
|
self._suppress = suppress
|
||||||
|
self._running = False
|
||||||
|
self._thread = threading.current_thread()
|
||||||
|
self._condition = threading.Condition()
|
||||||
|
self._ready = False
|
||||||
|
|
||||||
|
# Allow multiple calls to stop
|
||||||
|
self._queue = queue.Queue(10)
|
||||||
|
|
||||||
|
self.daemon = True
|
||||||
|
|
||||||
|
for name, callback in kwargs.items():
|
||||||
|
setattr(self, name, wrapper(callback or (lambda *a: None)))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def suppress(self):
|
||||||
|
"""Whether to suppress events.
|
||||||
|
"""
|
||||||
|
return self._suppress
|
||||||
|
|
||||||
|
@property
|
||||||
|
def running(self):
|
||||||
|
"""Whether the listener is currently running.
|
||||||
|
"""
|
||||||
|
return self._running
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stops listening for events.
|
||||||
|
|
||||||
|
When this method returns, no more events will be delivered.
|
||||||
|
"""
|
||||||
|
if self._running:
|
||||||
|
self._running = False
|
||||||
|
self._queue.put(None)
|
||||||
|
self._stop_platform()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.start()
|
||||||
|
self.wait()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, value, traceback):
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
"""Waits for this listener to become ready.
|
||||||
|
"""
|
||||||
|
self._condition.acquire()
|
||||||
|
while not self._ready:
|
||||||
|
self._condition.wait()
|
||||||
|
self._condition.release()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""The thread runner method.
|
||||||
|
"""
|
||||||
|
self._running = True
|
||||||
|
self._thread = threading.current_thread()
|
||||||
|
self._run()
|
||||||
|
|
||||||
|
# Make sure that the queue contains something
|
||||||
|
self._queue.put(None)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _emitter(cls, f):
|
||||||
|
"""A decorator to mark a method as the one emitting the callbacks.
|
||||||
|
|
||||||
|
This decorator will wrap the method and catch exception. If a
|
||||||
|
:class:`StopException` is caught, the listener will be stopped
|
||||||
|
gracefully. If any other exception is caught, it will be propagated to
|
||||||
|
the thread calling :meth:`join` and reraised there.
|
||||||
|
"""
|
||||||
|
@functools.wraps(f)
|
||||||
|
def inner(self, *args, **kwargs):
|
||||||
|
# pylint: disable=W0702; we want to catch all exception
|
||||||
|
try:
|
||||||
|
return f(self, *args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
if not isinstance(e, self._HANDLED_EXCEPTIONS):
|
||||||
|
if not isinstance(e, AbstractListener.StopException):
|
||||||
|
self._log.exception(
|
||||||
|
'Unhandled exception in listener callback')
|
||||||
|
self._queue.put(
|
||||||
|
None if isinstance(e, cls.StopException)
|
||||||
|
else sys.exc_info())
|
||||||
|
self.stop()
|
||||||
|
raise
|
||||||
|
# pylint: enable=W0702
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
def _mark_ready(self):
|
||||||
|
"""Marks this listener as ready to receive events.
|
||||||
|
|
||||||
|
This method must be called from :meth:`_run`. :meth:`wait` will block
|
||||||
|
until this method is called.
|
||||||
|
"""
|
||||||
|
self._condition.acquire()
|
||||||
|
self._ready = True
|
||||||
|
self._condition.notify()
|
||||||
|
self._condition.release()
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
"""The implementation of the :meth:`run` method.
|
||||||
|
|
||||||
|
This is a platform dependent implementation.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _stop_platform(self):
|
||||||
|
"""The implementation of the :meth:`stop` method.
|
||||||
|
|
||||||
|
This is a platform dependent implementation.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def join(self, *args):
|
||||||
|
super(AbstractListener, self).join(*args)
|
||||||
|
|
||||||
|
# Reraise any exceptions
|
||||||
|
try:
|
||||||
|
exc_type, exc_value, exc_traceback = self._queue.get()
|
||||||
|
except TypeError:
|
||||||
|
return
|
||||||
|
six.reraise(exc_type, exc_value, exc_traceback)
|
||||||
|
|
||||||
|
|
||||||
|
class NotifierMixin(object):
|
||||||
|
"""A mixin for notifiers of fake events.
|
||||||
|
|
||||||
|
This mixin can be used for controllers on platforms where sending fake
|
||||||
|
events does not cause a listener to receive a notification.
|
||||||
|
"""
|
||||||
|
def _emit(self, action, *args):
|
||||||
|
"""Sends a notification to all registered listeners.
|
||||||
|
|
||||||
|
This method will ensure that listeners that raise
|
||||||
|
:class:`StopException` are stopped.
|
||||||
|
|
||||||
|
:param str action: The name of the notification.
|
||||||
|
|
||||||
|
:param args: The arguments to pass.
|
||||||
|
"""
|
||||||
|
stopped = []
|
||||||
|
for listener in self._listeners():
|
||||||
|
try:
|
||||||
|
getattr(listener, action)(*args)
|
||||||
|
except listener.StopException:
|
||||||
|
stopped.append(listener)
|
||||||
|
for listener in stopped:
|
||||||
|
listener.stop()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _receiver(cls, listener_class):
|
||||||
|
"""A decorator to make a class able to receive fake events from a
|
||||||
|
controller.
|
||||||
|
|
||||||
|
This decorator will add the method ``_receive`` to the decorated class.
|
||||||
|
|
||||||
|
This method is a context manager which ensures that all calls to
|
||||||
|
:meth:`_emit` will invoke the named method in the listener instance
|
||||||
|
while the block is active.
|
||||||
|
"""
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def receive(self):
|
||||||
|
"""Executes a code block with this listener instance registered as
|
||||||
|
a receiver of fake input events.
|
||||||
|
"""
|
||||||
|
self._controller_class._add_listener(self)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
self._controller_class._remove_listener(self)
|
||||||
|
|
||||||
|
listener_class._receive = receive
|
||||||
|
listener_class._controller_class = cls
|
||||||
|
|
||||||
|
# Make sure this class has the necessary attributes
|
||||||
|
if not hasattr(cls, '_listener_cache'):
|
||||||
|
cls._listener_cache = set()
|
||||||
|
cls._listener_lock = threading.Lock()
|
||||||
|
|
||||||
|
return listener_class
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _listeners(cls):
|
||||||
|
"""Iterates over the set of running listeners.
|
||||||
|
|
||||||
|
This method will quit without acquiring the lock if the set is empty,
|
||||||
|
so there is potential for race conditions. This is an optimisation,
|
||||||
|
since :class:`Controller` will need to call this method for every
|
||||||
|
control event.
|
||||||
|
"""
|
||||||
|
if not cls._listener_cache:
|
||||||
|
return
|
||||||
|
with cls._listener_lock:
|
||||||
|
for listener in cls._listener_cache:
|
||||||
|
yield listener
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _add_listener(cls, listener):
|
||||||
|
"""Adds a listener to the set of running listeners.
|
||||||
|
|
||||||
|
:param listener: The listener for fake events.
|
||||||
|
"""
|
||||||
|
with cls._listener_lock:
|
||||||
|
cls._listener_cache.add(listener)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _remove_listener(cls, listener):
|
||||||
|
"""Removes this listener from the set of running listeners.
|
||||||
|
|
||||||
|
:param listener: The listener for fake events.
|
||||||
|
"""
|
||||||
|
with cls._listener_lock:
|
||||||
|
cls._listener_cache.remove(listener)
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,273 @@
|
||||||
|
# coding=utf-8
|
||||||
|
# pynput
|
||||||
|
# Copyright (C) 2015-2018 Moses Palmér
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
# later version.
|
||||||
|
#
|
||||||
|
# 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 Lesser General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
Utility functions and classes for the *Darwin* backend.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=C0103
|
||||||
|
# pylint: disable=R0903
|
||||||
|
# This module contains wrapper classes
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import ctypes
|
||||||
|
import ctypes.util
|
||||||
|
import six
|
||||||
|
|
||||||
|
import objc
|
||||||
|
import CoreFoundation
|
||||||
|
import Quartz
|
||||||
|
|
||||||
|
from . import AbstractListener
|
||||||
|
|
||||||
|
|
||||||
|
#: The objc module as a library handle
|
||||||
|
OBJC = ctypes.PyDLL(objc._objc.__file__)
|
||||||
|
|
||||||
|
OBJC.PyObjCObject_New.restype = ctypes.py_object
|
||||||
|
OBJC.PyObjCObject_New.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int]
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_value(value):
|
||||||
|
"""Converts a pointer to a *Python objc* value.
|
||||||
|
|
||||||
|
:param value: The pointer to convert.
|
||||||
|
|
||||||
|
:return: a wrapped value
|
||||||
|
"""
|
||||||
|
return OBJC.PyObjCObject_New(value, 0, 1)
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _wrapped(value):
|
||||||
|
"""A context manager that converts a raw pointer to a *Python objc* value.
|
||||||
|
|
||||||
|
When the block is exited, the value is released.
|
||||||
|
|
||||||
|
:param value: The raw value to wrap.
|
||||||
|
"""
|
||||||
|
wrapped_value = _wrap_value(value)
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield value
|
||||||
|
finally:
|
||||||
|
CoreFoundation.CFRelease(wrapped_value)
|
||||||
|
|
||||||
|
|
||||||
|
class CarbonExtra(object):
|
||||||
|
"""A class exposing some missing functionality from *Carbon* as class
|
||||||
|
attributes.
|
||||||
|
"""
|
||||||
|
_Carbon = ctypes.cdll.LoadLibrary(ctypes.util.find_library('Carbon'))
|
||||||
|
|
||||||
|
_Carbon.TISCopyCurrentKeyboardInputSource.argtypes = []
|
||||||
|
_Carbon.TISCopyCurrentKeyboardInputSource.restype = ctypes.c_void_p
|
||||||
|
|
||||||
|
_Carbon.TISCopyCurrentASCIICapableKeyboardLayoutInputSource.argtypes = []
|
||||||
|
_Carbon.TISCopyCurrentASCIICapableKeyboardLayoutInputSource.restype = \
|
||||||
|
ctypes.c_void_p
|
||||||
|
|
||||||
|
_Carbon.TISGetInputSourceProperty.argtypes = [
|
||||||
|
ctypes.c_void_p, ctypes.c_void_p]
|
||||||
|
_Carbon.TISGetInputSourceProperty.restype = ctypes.c_void_p
|
||||||
|
|
||||||
|
_Carbon.LMGetKbdType.argtypes = []
|
||||||
|
_Carbon.LMGetKbdType.restype = ctypes.c_uint32
|
||||||
|
|
||||||
|
_Carbon.UCKeyTranslate.argtypes = [
|
||||||
|
ctypes.c_void_p,
|
||||||
|
ctypes.c_uint16,
|
||||||
|
ctypes.c_uint16,
|
||||||
|
ctypes.c_uint32,
|
||||||
|
ctypes.c_uint32,
|
||||||
|
ctypes.c_uint32,
|
||||||
|
ctypes.POINTER(ctypes.c_uint32),
|
||||||
|
ctypes.c_uint8,
|
||||||
|
ctypes.POINTER(ctypes.c_uint8),
|
||||||
|
ctypes.c_uint16 * 4]
|
||||||
|
_Carbon.UCKeyTranslate.restype = ctypes.c_uint32
|
||||||
|
|
||||||
|
TISCopyCurrentKeyboardInputSource = \
|
||||||
|
_Carbon.TISCopyCurrentKeyboardInputSource
|
||||||
|
|
||||||
|
TISCopyCurrentASCIICapableKeyboardLayoutInputSource = \
|
||||||
|
_Carbon.TISCopyCurrentASCIICapableKeyboardLayoutInputSource
|
||||||
|
|
||||||
|
kTISPropertyUnicodeKeyLayoutData = ctypes.c_void_p.in_dll(
|
||||||
|
_Carbon, 'kTISPropertyUnicodeKeyLayoutData')
|
||||||
|
|
||||||
|
TISGetInputSourceProperty = \
|
||||||
|
_Carbon.TISGetInputSourceProperty
|
||||||
|
|
||||||
|
LMGetKbdType = \
|
||||||
|
_Carbon.LMGetKbdType
|
||||||
|
|
||||||
|
kUCKeyActionDisplay = 3
|
||||||
|
kUCKeyTranslateNoDeadKeysBit = 0
|
||||||
|
|
||||||
|
UCKeyTranslate = \
|
||||||
|
_Carbon.UCKeyTranslate
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def keycode_context():
|
||||||
|
"""Returns an opaque value representing a context for translating keycodes
|
||||||
|
to strings.
|
||||||
|
"""
|
||||||
|
keyboard_type, layout_data = None, None
|
||||||
|
for source in [
|
||||||
|
CarbonExtra.TISCopyCurrentKeyboardInputSource,
|
||||||
|
CarbonExtra.TISCopyCurrentASCIICapableKeyboardLayoutInputSource]:
|
||||||
|
with _wrapped(source()) as keyboard:
|
||||||
|
keyboard_type = CarbonExtra.LMGetKbdType()
|
||||||
|
layout = _wrap_value(CarbonExtra.TISGetInputSourceProperty(
|
||||||
|
keyboard,
|
||||||
|
CarbonExtra.kTISPropertyUnicodeKeyLayoutData))
|
||||||
|
layout_data = layout.bytes().tobytes() if layout else None
|
||||||
|
if keyboard is not None and layout_data is not None:
|
||||||
|
break
|
||||||
|
yield (keyboard_type, layout_data)
|
||||||
|
|
||||||
|
|
||||||
|
def keycode_to_string(context, keycode, modifier_state=0):
|
||||||
|
"""Converts a keycode to a string.
|
||||||
|
"""
|
||||||
|
LENGTH = 4
|
||||||
|
|
||||||
|
keyboard_type, layout_data = context
|
||||||
|
|
||||||
|
dead_key_state = ctypes.c_uint32()
|
||||||
|
length = ctypes.c_uint8()
|
||||||
|
unicode_string = (ctypes.c_uint16 * LENGTH)()
|
||||||
|
CarbonExtra.UCKeyTranslate(
|
||||||
|
layout_data,
|
||||||
|
keycode,
|
||||||
|
CarbonExtra.kUCKeyActionDisplay,
|
||||||
|
modifier_state,
|
||||||
|
keyboard_type,
|
||||||
|
CarbonExtra.kUCKeyTranslateNoDeadKeysBit,
|
||||||
|
ctypes.byref(dead_key_state),
|
||||||
|
LENGTH,
|
||||||
|
ctypes.byref(length),
|
||||||
|
unicode_string)
|
||||||
|
return u''.join(
|
||||||
|
six.unichr(unicode_string[i])
|
||||||
|
for i in range(length.value))
|
||||||
|
|
||||||
|
|
||||||
|
def get_unicode_to_keycode_map():
|
||||||
|
"""Returns a mapping from unicode strings to virtual key codes.
|
||||||
|
|
||||||
|
:return: a dict mapping key codes to strings
|
||||||
|
"""
|
||||||
|
with keycode_context() as context:
|
||||||
|
return {
|
||||||
|
keycode_to_string(context, keycode): keycode
|
||||||
|
for keycode in range(128)}
|
||||||
|
|
||||||
|
|
||||||
|
class ListenerMixin(object):
|
||||||
|
"""A mixin for *Quartz* event listeners.
|
||||||
|
|
||||||
|
Subclasses should set a value for :attr:`_EVENTS` and implement
|
||||||
|
:meth:`_handle`.
|
||||||
|
"""
|
||||||
|
#: The events that we listen to
|
||||||
|
_EVENTS = tuple()
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
self._loop = None
|
||||||
|
try:
|
||||||
|
tap = self._create_event_tap()
|
||||||
|
if tap is None:
|
||||||
|
self._mark_ready()
|
||||||
|
return
|
||||||
|
|
||||||
|
loop_source = Quartz.CFMachPortCreateRunLoopSource(
|
||||||
|
None, tap, 0)
|
||||||
|
self._loop = Quartz.CFRunLoopGetCurrent()
|
||||||
|
|
||||||
|
Quartz.CFRunLoopAddSource(
|
||||||
|
self._loop, loop_source, Quartz.kCFRunLoopDefaultMode)
|
||||||
|
Quartz.CGEventTapEnable(tap, True)
|
||||||
|
|
||||||
|
self._mark_ready()
|
||||||
|
|
||||||
|
# pylint: disable=W0702; we want to silence errors
|
||||||
|
try:
|
||||||
|
while self.running:
|
||||||
|
result = Quartz.CFRunLoopRunInMode(
|
||||||
|
Quartz.kCFRunLoopDefaultMode, 1, False)
|
||||||
|
try:
|
||||||
|
if result != Quartz.kCFRunLoopRunTimedOut:
|
||||||
|
break
|
||||||
|
except AttributeError:
|
||||||
|
# This happens during teardown of the virtual machine
|
||||||
|
break
|
||||||
|
|
||||||
|
except:
|
||||||
|
# This exception will have been passed to the main thread
|
||||||
|
pass
|
||||||
|
# pylint: enable=W0702
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self._loop = None
|
||||||
|
|
||||||
|
def _stop_platform(self):
|
||||||
|
# The base class sets the running flag to False; this will cause the
|
||||||
|
# loop around run loop invocations to terminate and set this event
|
||||||
|
try:
|
||||||
|
if self._loop is not None:
|
||||||
|
Quartz.CFRunLoopStop(self._loop)
|
||||||
|
except AttributeError:
|
||||||
|
# The loop may not have been created
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _create_event_tap(self):
|
||||||
|
"""Creates the event tap used by the listener.
|
||||||
|
|
||||||
|
:return: an event tap
|
||||||
|
"""
|
||||||
|
return Quartz.CGEventTapCreate(
|
||||||
|
Quartz.kCGSessionEventTap,
|
||||||
|
Quartz.kCGHeadInsertEventTap,
|
||||||
|
Quartz.kCGEventTapOptionListenOnly if (True
|
||||||
|
and not self.suppress
|
||||||
|
and self._intercept is None)
|
||||||
|
else Quartz.kCGEventTapOptionDefault,
|
||||||
|
self._EVENTS,
|
||||||
|
self._handler,
|
||||||
|
None)
|
||||||
|
|
||||||
|
@AbstractListener._emitter
|
||||||
|
def _handler(self, proxy, event_type, event, refcon):
|
||||||
|
"""The callback registered with *Mac OSX* for mouse events.
|
||||||
|
|
||||||
|
This method will call the callbacks registered on initialisation.
|
||||||
|
"""
|
||||||
|
self._handle(proxy, event_type, event, refcon)
|
||||||
|
if self._intercept is not None:
|
||||||
|
return self._intercept(event_type, event)
|
||||||
|
elif self.suppress:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _handle(self, proxy, event_type, event, refcon):
|
||||||
|
"""The device specific callback handler.
|
||||||
|
|
||||||
|
This method calls the appropriate callback registered when this
|
||||||
|
listener was created based on the event.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
|
@ -0,0 +1,641 @@
|
||||||
|
# coding=utf-8
|
||||||
|
# pynput
|
||||||
|
# Copyright (C) 2015-2018 Moses Palmér
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
# later version.
|
||||||
|
#
|
||||||
|
# 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 Lesser General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
Utility functions and classes for the *win32* backend.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=C0103
|
||||||
|
# We want to make it obvious how structs are related
|
||||||
|
|
||||||
|
# pylint: disable=R0903
|
||||||
|
# This module contains a number of structs
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import ctypes
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from ctypes import (
|
||||||
|
windll,
|
||||||
|
wintypes)
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from . import AbstractListener
|
||||||
|
|
||||||
|
|
||||||
|
# LPDWORD is not in ctypes.wintypes on Python 2
|
||||||
|
if not hasattr(wintypes, 'LPDWORD'):
|
||||||
|
wintypes.LPDWORD = ctypes.POINTER(wintypes.DWORD)
|
||||||
|
|
||||||
|
|
||||||
|
class MOUSEINPUT(ctypes.Structure):
|
||||||
|
"""Contains information about a simulated mouse event.
|
||||||
|
"""
|
||||||
|
MOVE = 0x0001
|
||||||
|
LEFTDOWN = 0x0002
|
||||||
|
LEFTUP = 0x0004
|
||||||
|
RIGHTDOWN = 0x0008
|
||||||
|
RIGHTUP = 0x0010
|
||||||
|
MIDDLEDOWN = 0x0020
|
||||||
|
MIDDLEUP = 0x0040
|
||||||
|
XDOWN = 0x0080
|
||||||
|
XUP = 0x0100
|
||||||
|
WHEEL = 0x0800
|
||||||
|
HWHEEL = 0x1000
|
||||||
|
ABSOLUTE = 0x8000
|
||||||
|
|
||||||
|
XBUTTON1 = 0x0001
|
||||||
|
XBUTTON2 = 0x0002
|
||||||
|
|
||||||
|
_fields_ = [
|
||||||
|
('dx', wintypes.LONG),
|
||||||
|
('dy', wintypes.LONG),
|
||||||
|
('mouseData', wintypes.DWORD),
|
||||||
|
('dwFlags', wintypes.DWORD),
|
||||||
|
('time', wintypes.DWORD),
|
||||||
|
('dwExtraInfo', ctypes.c_void_p)]
|
||||||
|
|
||||||
|
|
||||||
|
class KEYBDINPUT(ctypes.Structure):
|
||||||
|
"""Contains information about a simulated keyboard event.
|
||||||
|
"""
|
||||||
|
EXTENDEDKEY = 0x0001
|
||||||
|
KEYUP = 0x0002
|
||||||
|
SCANCODE = 0x0008
|
||||||
|
UNICODE = 0x0004
|
||||||
|
|
||||||
|
_fields_ = [
|
||||||
|
('wVk', wintypes.WORD),
|
||||||
|
('wScan', wintypes.WORD),
|
||||||
|
('dwFlags', wintypes.DWORD),
|
||||||
|
('time', wintypes.DWORD),
|
||||||
|
('dwExtraInfo', ctypes.c_void_p)]
|
||||||
|
|
||||||
|
|
||||||
|
class HARDWAREINPUT(ctypes.Structure):
|
||||||
|
"""Contains information about a simulated message generated by an input
|
||||||
|
device other than a keyboard or mouse.
|
||||||
|
"""
|
||||||
|
_fields_ = [
|
||||||
|
('uMsg', wintypes.DWORD),
|
||||||
|
('wParamL', wintypes.WORD),
|
||||||
|
('wParamH', wintypes.WORD)]
|
||||||
|
|
||||||
|
|
||||||
|
class INPUT_union(ctypes.Union):
|
||||||
|
"""Represents the union of input types in :class:`INPUT`.
|
||||||
|
"""
|
||||||
|
_fields_ = [
|
||||||
|
('mi', MOUSEINPUT),
|
||||||
|
('ki', KEYBDINPUT),
|
||||||
|
('hi', HARDWAREINPUT)]
|
||||||
|
|
||||||
|
|
||||||
|
class INPUT(ctypes.Structure):
|
||||||
|
"""Used by :attr:`SendInput` to store information for synthesizing input
|
||||||
|
events such as keystrokes, mouse movement, and mouse clicks.
|
||||||
|
"""
|
||||||
|
MOUSE = 0
|
||||||
|
KEYBOARD = 1
|
||||||
|
HARDWARE = 2
|
||||||
|
|
||||||
|
_fields_ = [
|
||||||
|
('type', wintypes.DWORD),
|
||||||
|
('value', INPUT_union)]
|
||||||
|
|
||||||
|
|
||||||
|
LPINPUT = ctypes.POINTER(INPUT)
|
||||||
|
|
||||||
|
VkKeyScan = windll.user32.VkKeyScanW
|
||||||
|
VkKeyScan.argtypes = (
|
||||||
|
wintypes.WCHAR,)
|
||||||
|
|
||||||
|
SendInput = windll.user32.SendInput
|
||||||
|
SendInput.argtypes = (
|
||||||
|
wintypes.UINT,
|
||||||
|
LPINPUT,
|
||||||
|
ctypes.c_int)
|
||||||
|
|
||||||
|
GetCurrentThreadId = windll.kernel32.GetCurrentThreadId
|
||||||
|
GetCurrentThreadId.restype = wintypes.DWORD
|
||||||
|
|
||||||
|
|
||||||
|
class MessageLoop(object):
|
||||||
|
"""A class representing a message loop.
|
||||||
|
"""
|
||||||
|
#: The message that signals this loop to terminate
|
||||||
|
WM_STOP = 0x0401
|
||||||
|
|
||||||
|
_LPMSG = ctypes.POINTER(wintypes.MSG)
|
||||||
|
|
||||||
|
_GetMessage = windll.user32.GetMessageW
|
||||||
|
_GetMessage.argtypes = (
|
||||||
|
_LPMSG,
|
||||||
|
wintypes.HWND,
|
||||||
|
wintypes.UINT,
|
||||||
|
wintypes.UINT)
|
||||||
|
_PeekMessage = windll.user32.PeekMessageW
|
||||||
|
_PeekMessage.argtypes = (
|
||||||
|
_LPMSG,
|
||||||
|
wintypes.HWND,
|
||||||
|
wintypes.UINT,
|
||||||
|
wintypes.UINT,
|
||||||
|
wintypes.UINT)
|
||||||
|
_PostThreadMessage = windll.user32.PostThreadMessageW
|
||||||
|
_PostThreadMessage.argtypes = (
|
||||||
|
wintypes.DWORD,
|
||||||
|
wintypes.UINT,
|
||||||
|
wintypes.WPARAM,
|
||||||
|
wintypes.LPARAM)
|
||||||
|
|
||||||
|
PM_NOREMOVE = 0
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._threadid = None
|
||||||
|
self._event = threading.Event()
|
||||||
|
self.thread = None
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""Initialises the message loop and yields all messages until
|
||||||
|
:meth:`stop` is called.
|
||||||
|
|
||||||
|
:raises AssertionError: if :meth:`start` has not been called
|
||||||
|
"""
|
||||||
|
assert self._threadid is not None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Pump messages until WM_STOP
|
||||||
|
while True:
|
||||||
|
msg = wintypes.MSG()
|
||||||
|
lpmsg = ctypes.byref(msg)
|
||||||
|
r = self._GetMessage(lpmsg, None, 0, 0)
|
||||||
|
if r <= 0 or msg.message == self.WM_STOP:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
yield msg
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self._threadid = None
|
||||||
|
self.thread = None
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Starts the message loop.
|
||||||
|
|
||||||
|
This method must be called before iterating over messages, and it must
|
||||||
|
be called from the same thread.
|
||||||
|
"""
|
||||||
|
self._threadid = GetCurrentThreadId()
|
||||||
|
self.thread = threading.current_thread()
|
||||||
|
|
||||||
|
# Create the message loop
|
||||||
|
msg = wintypes.MSG()
|
||||||
|
lpmsg = ctypes.byref(msg)
|
||||||
|
self._PeekMessage(lpmsg, None, 0x0400, 0x0400, self.PM_NOREMOVE)
|
||||||
|
|
||||||
|
# Set the event to signal to other threads that the loop is created
|
||||||
|
self._event.set()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stops the message loop.
|
||||||
|
"""
|
||||||
|
self._event.wait()
|
||||||
|
if self._threadid:
|
||||||
|
self.post(self.WM_STOP, 0, 0)
|
||||||
|
|
||||||
|
def post(self, msg, wparam, lparam):
|
||||||
|
"""Posts a message to this message loop.
|
||||||
|
|
||||||
|
:param ctypes.wintypes.UINT msg: The message.
|
||||||
|
|
||||||
|
:param ctypes.wintypes.WPARAM wparam: The value of ``wParam``.
|
||||||
|
|
||||||
|
:param ctypes.wintypes.LPARAM lparam: The value of ``lParam``.
|
||||||
|
"""
|
||||||
|
self._PostThreadMessage(self._threadid, msg, wparam, lparam)
|
||||||
|
|
||||||
|
|
||||||
|
class SystemHook(object):
|
||||||
|
"""A class to handle Windows hooks.
|
||||||
|
"""
|
||||||
|
#: The hook action value for actions we should check
|
||||||
|
HC_ACTION = 0
|
||||||
|
|
||||||
|
_HOOKPROC = ctypes.WINFUNCTYPE(
|
||||||
|
wintypes.LPARAM,
|
||||||
|
ctypes.c_int32, wintypes.WPARAM, wintypes.LPARAM)
|
||||||
|
|
||||||
|
_SetWindowsHookEx = windll.user32.SetWindowsHookExW
|
||||||
|
_SetWindowsHookEx.argtypes = (
|
||||||
|
ctypes.c_int,
|
||||||
|
_HOOKPROC,
|
||||||
|
wintypes.HINSTANCE,
|
||||||
|
wintypes.DWORD)
|
||||||
|
_UnhookWindowsHookEx = windll.user32.UnhookWindowsHookEx
|
||||||
|
_UnhookWindowsHookEx.argtypes = (
|
||||||
|
wintypes.HHOOK,)
|
||||||
|
_CallNextHookEx = windll.user32.CallNextHookEx
|
||||||
|
_CallNextHookEx.argtypes = (
|
||||||
|
wintypes.HHOOK,
|
||||||
|
ctypes.c_int,
|
||||||
|
wintypes.WPARAM,
|
||||||
|
wintypes.LPARAM)
|
||||||
|
|
||||||
|
#: The registered hook procedures
|
||||||
|
_HOOKS = {}
|
||||||
|
|
||||||
|
class SuppressException(Exception):
|
||||||
|
"""An exception raised by a hook callback to suppress further
|
||||||
|
propagation of events.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __init__(self, hook_id, on_hook=lambda code, msg, lpdata: None):
|
||||||
|
self.hook_id = hook_id
|
||||||
|
self.on_hook = on_hook
|
||||||
|
self._hook = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
key = threading.current_thread().ident
|
||||||
|
assert key not in self._HOOKS
|
||||||
|
|
||||||
|
# Add ourself to lookup table and install the hook
|
||||||
|
self._HOOKS[key] = self
|
||||||
|
self._hook = self._SetWindowsHookEx(
|
||||||
|
self.hook_id,
|
||||||
|
self._handler,
|
||||||
|
None,
|
||||||
|
0)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, value, traceback):
|
||||||
|
key = threading.current_thread().ident
|
||||||
|
assert key in self._HOOKS
|
||||||
|
|
||||||
|
if self._hook is not None:
|
||||||
|
# Uninstall the hook and remove ourself from lookup table
|
||||||
|
self._UnhookWindowsHookEx(self._hook)
|
||||||
|
del self._HOOKS[key]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@_HOOKPROC
|
||||||
|
def _handler(code, msg, lpdata):
|
||||||
|
key = threading.current_thread().ident
|
||||||
|
self = SystemHook._HOOKS.get(key, None)
|
||||||
|
if self:
|
||||||
|
# pylint: disable=W0702; we want to silence errors
|
||||||
|
try:
|
||||||
|
self.on_hook(code, msg, lpdata)
|
||||||
|
except self.SuppressException:
|
||||||
|
# Return non-zero to stop event propagation
|
||||||
|
return 1
|
||||||
|
except:
|
||||||
|
# Ignore any errors
|
||||||
|
pass
|
||||||
|
# pylint: enable=W0702
|
||||||
|
return SystemHook._CallNextHookEx(0, code, msg, lpdata)
|
||||||
|
|
||||||
|
|
||||||
|
class ListenerMixin(object):
|
||||||
|
"""A mixin for *win32* event listeners.
|
||||||
|
|
||||||
|
Subclasses should set a value for :attr:`_EVENTS` and implement
|
||||||
|
:meth:`_handle`.
|
||||||
|
|
||||||
|
Subclasses must also be decorated with a decorator compatible with
|
||||||
|
:meth:`pynput._util.NotifierMixin._receiver` or implement the method
|
||||||
|
``_receive()``.
|
||||||
|
"""
|
||||||
|
#: The Windows hook ID for the events to capture
|
||||||
|
_EVENTS = None
|
||||||
|
|
||||||
|
#: The window message used to signal that an even should be handled
|
||||||
|
_WM_PROCESS = 0x410
|
||||||
|
|
||||||
|
def suppress_event(self):
|
||||||
|
"""Causes the currently filtered event to be suppressed.
|
||||||
|
|
||||||
|
This has a system wide effect and will generally result in no
|
||||||
|
applications receiving the event.
|
||||||
|
|
||||||
|
This method will raise an undefined exception.
|
||||||
|
"""
|
||||||
|
raise SystemHook.SuppressException()
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
self._message_loop = MessageLoop()
|
||||||
|
with self._receive():
|
||||||
|
self._mark_ready()
|
||||||
|
self._message_loop.start()
|
||||||
|
|
||||||
|
# pylint: disable=W0702; we want to silence errors
|
||||||
|
try:
|
||||||
|
with SystemHook(self._EVENTS, self._handler):
|
||||||
|
# Just pump messages
|
||||||
|
for msg in self._message_loop:
|
||||||
|
if not self.running:
|
||||||
|
break
|
||||||
|
if msg.message == self._WM_PROCESS:
|
||||||
|
self._process(msg.wParam, msg.lParam)
|
||||||
|
except:
|
||||||
|
# This exception will have been passed to the main thread
|
||||||
|
pass
|
||||||
|
# pylint: enable=W0702
|
||||||
|
|
||||||
|
def _stop_platform(self):
|
||||||
|
try:
|
||||||
|
self._message_loop.stop()
|
||||||
|
except AttributeError:
|
||||||
|
# The loop may not have been created
|
||||||
|
pass
|
||||||
|
|
||||||
|
@AbstractListener._emitter
|
||||||
|
def _handler(self, code, msg, lpdata):
|
||||||
|
"""The callback registered with *Windows* for events.
|
||||||
|
|
||||||
|
This method will post the message :attr:`_WM_HANDLE` to the message loop
|
||||||
|
started with this listener using :meth:`MessageLoop.post`. The
|
||||||
|
parameters are retrieved with a call to :meth:`_handle`.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
converted = self._convert(code, msg, lpdata)
|
||||||
|
if converted is not None:
|
||||||
|
self._message_loop.post(self._WM_PROCESS, *converted)
|
||||||
|
except NotImplementedError:
|
||||||
|
self._handle(code, msg, lpdata)
|
||||||
|
|
||||||
|
if self.suppress:
|
||||||
|
self.suppress_event()
|
||||||
|
|
||||||
|
def _convert(self, code, msg, lpdata):
|
||||||
|
"""The device specific callback handler.
|
||||||
|
|
||||||
|
This method converts a low-level message and data to a
|
||||||
|
``WPARAM`` / ``LPARAM`` pair.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _process(self, wparam, lparam):
|
||||||
|
"""The device specific callback handler.
|
||||||
|
|
||||||
|
This method performs the actual dispatching of events.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _handle(self, code, msg, lpdata):
|
||||||
|
"""The device specific callback handler.
|
||||||
|
|
||||||
|
This method calls the appropriate callback registered when this
|
||||||
|
listener was created based on the event.
|
||||||
|
|
||||||
|
This method is only called if :meth:`_convert` is not implemented.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class KeyTranslator(object):
|
||||||
|
"""A class to translate virtual key codes to characters.
|
||||||
|
"""
|
||||||
|
_AttachThreadInput = ctypes.windll.user32.AttachThreadInput
|
||||||
|
_AttachThreadInput.argtypes = (
|
||||||
|
wintypes.DWORD,
|
||||||
|
wintypes.DWORD,
|
||||||
|
wintypes.BOOL)
|
||||||
|
_GetForegroundWindow = ctypes.windll.user32.GetForegroundWindow
|
||||||
|
_GetKeyboardLayout = ctypes.windll.user32.GetKeyboardLayout
|
||||||
|
_GetKeyboardLayout.argtypes = (
|
||||||
|
wintypes.DWORD,)
|
||||||
|
_GetKeyboardState = ctypes.windll.user32.GetKeyboardState
|
||||||
|
_GetKeyboardState.argtypes = (
|
||||||
|
ctypes.c_voidp,)
|
||||||
|
_GetWindowThreadProcessId = ctypes.windll.user32.GetWindowThreadProcessId
|
||||||
|
_GetWindowThreadProcessId.argtypes = (
|
||||||
|
wintypes.HWND,
|
||||||
|
wintypes.LPDWORD)
|
||||||
|
_MapVirtualKeyEx = ctypes.windll.user32.MapVirtualKeyExW
|
||||||
|
_MapVirtualKeyEx.argtypes = (
|
||||||
|
wintypes.UINT,
|
||||||
|
wintypes.UINT,
|
||||||
|
wintypes.HKL)
|
||||||
|
_ToUnicodeEx = ctypes.windll.user32.ToUnicodeEx
|
||||||
|
_ToUnicodeEx.argtypes = (
|
||||||
|
wintypes.UINT,
|
||||||
|
wintypes.UINT,
|
||||||
|
ctypes.c_voidp,
|
||||||
|
ctypes.c_voidp,
|
||||||
|
ctypes.c_int,
|
||||||
|
wintypes.UINT,
|
||||||
|
wintypes.HKL)
|
||||||
|
|
||||||
|
_MAPVK_VK_TO_VSC = 0
|
||||||
|
_MAPVK_VK_TO_CHAR = 2
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.__state = (ctypes.c_ubyte * 255)()
|
||||||
|
self._cache = {}
|
||||||
|
self._reinject_arguments = None
|
||||||
|
|
||||||
|
def __call__(self, vk, is_press):
|
||||||
|
"""Converts a virtual key code to a string.
|
||||||
|
|
||||||
|
:param int vk: The virtual key code.
|
||||||
|
|
||||||
|
:param bool is_press: Whether this is a press. Because the *win32*
|
||||||
|
functions used to translate the key modifies internal kernel state,
|
||||||
|
some cleanup must be performed for key presses.
|
||||||
|
|
||||||
|
:return: parameters suitable for the :class:`pynput.keyboard.KeyCode`
|
||||||
|
constructor
|
||||||
|
|
||||||
|
:raises OSError: if a call to any *win32* function fails
|
||||||
|
"""
|
||||||
|
# Get the keyboard state and layout
|
||||||
|
state, layout = self._get_state_and_layout()
|
||||||
|
|
||||||
|
# Get the scan code for the virtual key
|
||||||
|
scan = self._to_scan(vk, layout)
|
||||||
|
|
||||||
|
# Try to reuse the previous key in the cache
|
||||||
|
try:
|
||||||
|
if is_press:
|
||||||
|
return self._cache[vk]
|
||||||
|
else:
|
||||||
|
return self._cache.pop(vk)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Get a string representation of the key
|
||||||
|
char, is_dead = self._to_char(vk, layout)
|
||||||
|
modified_char = self._to_char_with_modifiers(vk, layout, scan, state)
|
||||||
|
|
||||||
|
# Clear the keyboard state if the key was a dead key
|
||||||
|
if is_dead:
|
||||||
|
self._reset_state(vk, layout, scan)
|
||||||
|
|
||||||
|
# If the previous key handled was a dead key, we reinject it
|
||||||
|
if self._reinject_arguments:
|
||||||
|
self._reinject(*self._reinject_arguments)
|
||||||
|
self._reinject_arguments = None
|
||||||
|
|
||||||
|
# If the current key is a dead key, we store the current state to be
|
||||||
|
# able to reinject later
|
||||||
|
elif is_dead:
|
||||||
|
self._reinject_arguments = (
|
||||||
|
vk,
|
||||||
|
layout,
|
||||||
|
scan,
|
||||||
|
(ctypes.c_ubyte * 255)(*state))
|
||||||
|
|
||||||
|
# Otherwise we just clear any previous dead key state
|
||||||
|
else:
|
||||||
|
self._reinject_arguments = None
|
||||||
|
|
||||||
|
# Update the cache
|
||||||
|
self._cache[vk] = {
|
||||||
|
'char': modified_char or char,
|
||||||
|
'is_dead': is_dead,
|
||||||
|
'vk': vk}
|
||||||
|
|
||||||
|
return self._cache[vk]
|
||||||
|
|
||||||
|
def _get_state_and_layout(self):
|
||||||
|
"""Returns the keyboard state and layout.
|
||||||
|
|
||||||
|
The state is read from the currently active window if possible. It is
|
||||||
|
kept in a cache, so any call to this method will invalidate return
|
||||||
|
values from previous invocations.
|
||||||
|
|
||||||
|
:return: the tuple ``(state, layout)``
|
||||||
|
"""
|
||||||
|
# Get the state of the keyboard attached to the active window
|
||||||
|
with self._thread_input() as active_thread:
|
||||||
|
if not self._GetKeyboardState(ctypes.byref(self.__state)):
|
||||||
|
raise OSError(
|
||||||
|
'GetKeyboardState failed: %d',
|
||||||
|
ctypes.wintypes.get_last_error())
|
||||||
|
|
||||||
|
# Get the keyboard layout for the thread for which we retrieved the
|
||||||
|
# state
|
||||||
|
layout = self._GetKeyboardLayout(active_thread)
|
||||||
|
|
||||||
|
return (self.__state, layout)
|
||||||
|
|
||||||
|
def _to_scan(self, vk, layout):
|
||||||
|
"""Retrieves the scan code for a virtual key code.
|
||||||
|
|
||||||
|
:param int vk: The virtual key code.
|
||||||
|
|
||||||
|
:param layout: The keyboard layout.
|
||||||
|
|
||||||
|
:return: the scan code
|
||||||
|
"""
|
||||||
|
return self._MapVirtualKeyEx(
|
||||||
|
vk, self._MAPVK_VK_TO_VSC, layout)
|
||||||
|
|
||||||
|
def _to_char(self, vk, layout):
|
||||||
|
"""Converts a virtual key by simply mapping it through the keyboard
|
||||||
|
layout.
|
||||||
|
|
||||||
|
This method is stateless, so any active shift state or dead keys are
|
||||||
|
ignored.
|
||||||
|
|
||||||
|
:param int vk: The virtual key code.
|
||||||
|
|
||||||
|
:param layout: The keyboard layout.
|
||||||
|
|
||||||
|
:return: the string representation of the key, or ``None``, and whether
|
||||||
|
was dead as the tuple ``(char, is_dead)``
|
||||||
|
"""
|
||||||
|
# MapVirtualKeyEx will yield a string representation for dead keys
|
||||||
|
flags_and_codepoint = self._MapVirtualKeyEx(
|
||||||
|
vk, self._MAPVK_VK_TO_CHAR, layout)
|
||||||
|
if flags_and_codepoint:
|
||||||
|
return (
|
||||||
|
six.unichr(flags_and_codepoint & 0xFFFF),
|
||||||
|
bool(flags_and_codepoint & (1 << 31)))
|
||||||
|
else:
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
|
def _to_char_with_modifiers(self, vk, layout, scan, state):
|
||||||
|
"""Converts a virtual key by mapping it through the keyboard layout and
|
||||||
|
internal kernel keyboard state.
|
||||||
|
|
||||||
|
This method is stateful, so any active shift state and dead keys are
|
||||||
|
applied. Currently active dead keys will be removed from the internal
|
||||||
|
kernel keyboard state.
|
||||||
|
|
||||||
|
:param int vk: The virtual key code.
|
||||||
|
|
||||||
|
:param layout: The keyboard layout.
|
||||||
|
|
||||||
|
:param int scan: The scan code of the key.
|
||||||
|
|
||||||
|
:param state: The keyboard state.
|
||||||
|
|
||||||
|
:return: the string representation of the key, or ``None``
|
||||||
|
"""
|
||||||
|
# This will apply any dead keys and modify the internal kernel keyboard
|
||||||
|
# state
|
||||||
|
out = (ctypes.wintypes.WCHAR * 5)()
|
||||||
|
count = self._ToUnicodeEx(
|
||||||
|
vk, scan, ctypes.byref(state), ctypes.byref(out),
|
||||||
|
len(out), 0, layout)
|
||||||
|
|
||||||
|
return out[0] if count > 0 else None
|
||||||
|
|
||||||
|
def _reset_state(self, vk, layout, scan):
|
||||||
|
"""Clears the internal kernel keyboard state.
|
||||||
|
|
||||||
|
This method will remove all dead keys from the internal state.
|
||||||
|
|
||||||
|
:param int vk: The virtual key code.
|
||||||
|
|
||||||
|
:param layout: The keyboard layout.
|
||||||
|
|
||||||
|
:param int scan: The scan code of the key.
|
||||||
|
"""
|
||||||
|
state = (ctypes.c_byte * 255)()
|
||||||
|
out = (ctypes.wintypes.WCHAR * 5)()
|
||||||
|
while self._ToUnicodeEx(
|
||||||
|
vk, scan, ctypes.byref(state), ctypes.byref(out),
|
||||||
|
len(out), 0, layout) < 0:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _reinject(self, vk, layout, scan, state):
|
||||||
|
"""Reinjects the previous dead key.
|
||||||
|
|
||||||
|
This must be called if ``ToUnicodeEx`` has been called, and the
|
||||||
|
previous key was a dead one.
|
||||||
|
|
||||||
|
:param int vk: The virtual key code.
|
||||||
|
|
||||||
|
:param layout: The keyboard layout.
|
||||||
|
|
||||||
|
:param int scan: The scan code of the key.
|
||||||
|
|
||||||
|
:param state: The keyboard state.
|
||||||
|
"""
|
||||||
|
out = (ctypes.wintypes.WCHAR * 5)()
|
||||||
|
self._ToUnicodeEx(
|
||||||
|
vk, scan, ctypes.byref(state), ctypes.byref(out),
|
||||||
|
len(out), 0, layout)
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _thread_input(self):
|
||||||
|
"""Yields the current thread ID.
|
||||||
|
"""
|
||||||
|
yield GetCurrentThreadId()
|
|
@ -0,0 +1,179 @@
|
||||||
|
# coding: utf-8
|
||||||
|
# pynput
|
||||||
|
# Copyright (C) 2015-2018 Moses Palmér
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
# later version.
|
||||||
|
#
|
||||||
|
# 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 Lesser General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# pylint: disable=C0111,C0302
|
||||||
|
|
||||||
|
LBUTTON = 1
|
||||||
|
RBUTTON = 2
|
||||||
|
CANCEL = 3
|
||||||
|
MBUTTON = 4
|
||||||
|
XBUTTON1 = 5
|
||||||
|
XBUTTON2 = 6
|
||||||
|
BACK = 8
|
||||||
|
TAB = 9
|
||||||
|
CLEAR = 12
|
||||||
|
RETURN = 13
|
||||||
|
SHIFT = 16
|
||||||
|
CONTROL = 17
|
||||||
|
MENU = 18
|
||||||
|
PAUSE = 19
|
||||||
|
CAPITAL = 20
|
||||||
|
KANA = 21
|
||||||
|
HANGEUL = 21
|
||||||
|
HANGUL = 21
|
||||||
|
JUNJA = 23
|
||||||
|
FINAL = 24
|
||||||
|
HANJA = 25
|
||||||
|
KANJI = 25
|
||||||
|
ESCAPE = 27
|
||||||
|
CONVERT = 28
|
||||||
|
NONCONVERT = 29
|
||||||
|
ACCEPT = 30
|
||||||
|
MODECHANGE = 31
|
||||||
|
SPACE = 32
|
||||||
|
PRIOR = 33
|
||||||
|
NEXT = 34
|
||||||
|
END = 35
|
||||||
|
HOME = 36
|
||||||
|
LEFT = 37
|
||||||
|
UP = 38
|
||||||
|
RIGHT = 39
|
||||||
|
DOWN = 40
|
||||||
|
SELECT = 41
|
||||||
|
PRINT = 42
|
||||||
|
EXECUTE = 43
|
||||||
|
SNAPSHOT = 44
|
||||||
|
INSERT = 45
|
||||||
|
DELETE = 46
|
||||||
|
HELP = 47
|
||||||
|
LWIN = 91
|
||||||
|
RWIN = 92
|
||||||
|
APPS = 93
|
||||||
|
SLEEP = 95
|
||||||
|
NUMPAD0 = 96
|
||||||
|
NUMPAD1 = 97
|
||||||
|
NUMPAD2 = 98
|
||||||
|
NUMPAD3 = 99
|
||||||
|
NUMPAD4 = 100
|
||||||
|
NUMPAD5 = 101
|
||||||
|
NUMPAD6 = 102
|
||||||
|
NUMPAD7 = 103
|
||||||
|
NUMPAD8 = 104
|
||||||
|
NUMPAD9 = 105
|
||||||
|
MULTIPLY = 106
|
||||||
|
ADD = 107
|
||||||
|
SEPARATOR = 108
|
||||||
|
SUBTRACT = 109
|
||||||
|
DECIMAL = 110
|
||||||
|
DIVIDE = 111
|
||||||
|
F1 = 112
|
||||||
|
F2 = 113
|
||||||
|
F3 = 114
|
||||||
|
F4 = 115
|
||||||
|
F5 = 116
|
||||||
|
F6 = 117
|
||||||
|
F7 = 118
|
||||||
|
F8 = 119
|
||||||
|
F9 = 120
|
||||||
|
F10 = 121
|
||||||
|
F11 = 122
|
||||||
|
F12 = 123
|
||||||
|
F13 = 124
|
||||||
|
F14 = 125
|
||||||
|
F15 = 126
|
||||||
|
F16 = 127
|
||||||
|
F17 = 128
|
||||||
|
F18 = 129
|
||||||
|
F19 = 130
|
||||||
|
F20 = 131
|
||||||
|
F21 = 132
|
||||||
|
F22 = 133
|
||||||
|
F23 = 134
|
||||||
|
F24 = 135
|
||||||
|
NUMLOCK = 144
|
||||||
|
SCROLL = 145
|
||||||
|
OEM_NEC_EQUAL = 146
|
||||||
|
OEM_FJ_JISHO = 146
|
||||||
|
OEM_FJ_MASSHOU = 147
|
||||||
|
OEM_FJ_TOUROKU = 148
|
||||||
|
OEM_FJ_LOYA = 149
|
||||||
|
OEM_FJ_ROYA = 150
|
||||||
|
LSHIFT = 160
|
||||||
|
RSHIFT = 161
|
||||||
|
LCONTROL = 162
|
||||||
|
RCONTROL = 163
|
||||||
|
LMENU = 164
|
||||||
|
RMENU = 165
|
||||||
|
BROWSER_BACK = 166
|
||||||
|
BROWSER_FORWARD = 167
|
||||||
|
BROWSER_REFRESH = 168
|
||||||
|
BROWSER_STOP = 169
|
||||||
|
BROWSER_SEARCH = 170
|
||||||
|
BROWSER_FAVORITES = 171
|
||||||
|
BROWSER_HOME = 172
|
||||||
|
VOLUME_MUTE = 173
|
||||||
|
VOLUME_DOWN = 174
|
||||||
|
VOLUME_UP = 175
|
||||||
|
MEDIA_NEXT_TRACK = 176
|
||||||
|
MEDIA_PREV_TRACK = 177
|
||||||
|
MEDIA_STOP = 178
|
||||||
|
MEDIA_PLAY_PAUSE = 179
|
||||||
|
LAUNCH_MAIL = 180
|
||||||
|
LAUNCH_MEDIA_SELECT = 181
|
||||||
|
LAUNCH_APP1 = 182
|
||||||
|
LAUNCH_APP2 = 183
|
||||||
|
OEM_1 = 186
|
||||||
|
OEM_PLUS = 187
|
||||||
|
OEM_COMMA = 188
|
||||||
|
OEM_MINUS = 189
|
||||||
|
OEM_PERIOD = 190
|
||||||
|
OEM_2 = 191
|
||||||
|
OEM_3 = 192
|
||||||
|
OEM_4 = 219
|
||||||
|
OEM_5 = 220
|
||||||
|
OEM_6 = 221
|
||||||
|
OEM_7 = 222
|
||||||
|
OEM_8 = 223
|
||||||
|
OEM_AX = 225
|
||||||
|
OEM_102 = 226
|
||||||
|
ICO_HELP = 227
|
||||||
|
ICO_00 = 228
|
||||||
|
PROCESSKEY = 229
|
||||||
|
ICO_CLEAR = 230
|
||||||
|
PACKET = 231
|
||||||
|
OEM_RESET = 233
|
||||||
|
OEM_JUMP = 234
|
||||||
|
OEM_PA1 = 235
|
||||||
|
OEM_PA2 = 236
|
||||||
|
OEM_PA3 = 237
|
||||||
|
OEM_WSCTRL = 238
|
||||||
|
OEM_CUSEL = 239
|
||||||
|
OEM_ATTN = 240
|
||||||
|
OEM_FINISH = 241
|
||||||
|
OEM_COPY = 242
|
||||||
|
OEM_AUTO = 243
|
||||||
|
OEM_ENLW = 244
|
||||||
|
OEM_BACKTAB = 245
|
||||||
|
ATTN = 246
|
||||||
|
CRSEL = 247
|
||||||
|
EXSEL = 248
|
||||||
|
EREOF = 249
|
||||||
|
PLAY = 250
|
||||||
|
ZOOM = 251
|
||||||
|
NONAME = 252
|
||||||
|
PA1 = 253
|
||||||
|
OEM_CLEAR = 254
|
|
@ -0,0 +1,480 @@
|
||||||
|
# coding=utf-8
|
||||||
|
# pynput
|
||||||
|
# Copyright (C) 2015-2018 Moses Palmér
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
# later version.
|
||||||
|
#
|
||||||
|
# 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 Lesser General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
Utility functions and classes for the *Xorg* backend.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=R0903
|
||||||
|
# We implement stubs
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import functools
|
||||||
|
import itertools
|
||||||
|
import operator
|
||||||
|
import Xlib.display
|
||||||
|
import Xlib.threaded
|
||||||
|
import Xlib.XK
|
||||||
|
|
||||||
|
from . import AbstractListener
|
||||||
|
from .xorg_keysyms import SYMBOLS
|
||||||
|
|
||||||
|
|
||||||
|
# Create a display to verify that we have an X connection
|
||||||
|
def _check():
|
||||||
|
display = Xlib.display.Display()
|
||||||
|
display.close()
|
||||||
|
_check()
|
||||||
|
del _check
|
||||||
|
|
||||||
|
|
||||||
|
class X11Error(Exception):
|
||||||
|
"""An error that is thrown at the end of a code block managed by a
|
||||||
|
:func:`display_manager` if an *X11* error occurred.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def display_manager(display):
|
||||||
|
"""Traps *X* errors and raises an :class:``X11Error`` at the end if any
|
||||||
|
error occurred.
|
||||||
|
|
||||||
|
This handler also ensures that the :class:`Xlib.display.Display` being
|
||||||
|
managed is sync'd.
|
||||||
|
|
||||||
|
:param Xlib.display.Display display: The *X* display.
|
||||||
|
|
||||||
|
:return: the display
|
||||||
|
:rtype: Xlib.display.Display
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
def handler(*args):
|
||||||
|
"""The *Xlib* error handler.
|
||||||
|
"""
|
||||||
|
errors.append(args)
|
||||||
|
|
||||||
|
old_handler = display.set_error_handler(handler)
|
||||||
|
try:
|
||||||
|
yield display
|
||||||
|
display.sync()
|
||||||
|
finally:
|
||||||
|
display.set_error_handler(old_handler)
|
||||||
|
if errors:
|
||||||
|
raise X11Error(errors)
|
||||||
|
|
||||||
|
|
||||||
|
def _find_mask(display, symbol):
|
||||||
|
"""Returns the mode flags to use for a modifier symbol.
|
||||||
|
|
||||||
|
:param Xlib.display.Display display: The *X* display.
|
||||||
|
|
||||||
|
:param str symbol: The name of the symbol.
|
||||||
|
|
||||||
|
:return: the modifier mask
|
||||||
|
"""
|
||||||
|
# Get the key code for the symbol
|
||||||
|
modifier_keycode = display.keysym_to_keycode(
|
||||||
|
Xlib.XK.string_to_keysym(symbol))
|
||||||
|
|
||||||
|
for index, keycodes in enumerate(display.get_modifier_mapping()):
|
||||||
|
for keycode in keycodes:
|
||||||
|
if keycode == modifier_keycode:
|
||||||
|
return 1 << index
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def alt_mask(display):
|
||||||
|
"""Returns the *alt* mask flags.
|
||||||
|
|
||||||
|
The first time this function is called for a display, the value is cached.
|
||||||
|
Subsequent calls will return the cached value.
|
||||||
|
|
||||||
|
:param Xlib.display.Display display: The *X* display.
|
||||||
|
|
||||||
|
:return: the modifier mask
|
||||||
|
"""
|
||||||
|
if not hasattr(display, '__alt_mask'):
|
||||||
|
display.__alt_mask = _find_mask(display, 'Alt_L')
|
||||||
|
return display.__alt_mask
|
||||||
|
|
||||||
|
|
||||||
|
def alt_gr_mask(display):
|
||||||
|
"""Returns the *alt* mask flags.
|
||||||
|
|
||||||
|
The first time this function is called for a display, the value is cached.
|
||||||
|
Subsequent calls will return the cached value.
|
||||||
|
|
||||||
|
:param Xlib.display.Display display: The *X* display.
|
||||||
|
|
||||||
|
:return: the modifier mask
|
||||||
|
"""
|
||||||
|
if not hasattr(display, '__altgr_mask'):
|
||||||
|
display.__altgr_mask = _find_mask(display, 'Mode_switch')
|
||||||
|
return display.__altgr_mask
|
||||||
|
|
||||||
|
|
||||||
|
def numlock_mask(display):
|
||||||
|
"""Returns the *numlock* mask flags.
|
||||||
|
|
||||||
|
The first time this function is called for a display, the value is cached.
|
||||||
|
Subsequent calls will return the cached value.
|
||||||
|
|
||||||
|
:param Xlib.display.Display display: The *X* display.
|
||||||
|
|
||||||
|
:return: the modifier mask
|
||||||
|
"""
|
||||||
|
if not hasattr(display, '__numlock_mask'):
|
||||||
|
display.__numlock_mask = _find_mask(display, 'Num_Lock')
|
||||||
|
return display.__numlock_mask
|
||||||
|
|
||||||
|
|
||||||
|
def keysym_is_latin_upper(keysym):
|
||||||
|
"""Determines whether a *keysym* is an upper case *latin* character.
|
||||||
|
|
||||||
|
This is true only if ``XK_A`` <= ``keysym`` <= ` XK_Z``.
|
||||||
|
|
||||||
|
:param in keysym: The *keysym* to check.
|
||||||
|
"""
|
||||||
|
return Xlib.XK.XK_A <= keysym <= Xlib.XK.XK_Z
|
||||||
|
|
||||||
|
|
||||||
|
def keysym_is_latin_lower(keysym):
|
||||||
|
"""Determines whether a *keysym* is a lower case *latin* character.
|
||||||
|
|
||||||
|
This is true only if ``XK_a`` <= ``keysym`` <= ` XK_z``.
|
||||||
|
|
||||||
|
:param in keysym: The *keysym* to check.
|
||||||
|
"""
|
||||||
|
return Xlib.XK.XK_a <= keysym <= Xlib.XK.XK_z
|
||||||
|
|
||||||
|
|
||||||
|
def keysym_group(ks1, ks2):
|
||||||
|
"""Generates a group from two *keysyms*.
|
||||||
|
|
||||||
|
The implementation of this function comes from:
|
||||||
|
|
||||||
|
Within each group, if the second element of the group is ``NoSymbol``,
|
||||||
|
then the group should be treated as if the second element were the same
|
||||||
|
as the first element, except when the first element is an alphabetic
|
||||||
|
*KeySym* ``K`` for which both lowercase and uppercase forms are
|
||||||
|
defined.
|
||||||
|
|
||||||
|
In that case, the group should be treated as if the first element were
|
||||||
|
the lowercase form of ``K`` and the second element were the uppercase
|
||||||
|
form of ``K``.
|
||||||
|
|
||||||
|
This function assumes that *alphabetic* means *latin*; this assumption
|
||||||
|
appears to be consistent with observations of the return values from
|
||||||
|
``XGetKeyboardMapping``.
|
||||||
|
|
||||||
|
:param ks1: The first *keysym*.
|
||||||
|
|
||||||
|
:param ks2: The second *keysym*.
|
||||||
|
|
||||||
|
:return: a tuple conforming to the description above
|
||||||
|
"""
|
||||||
|
if ks2 == Xlib.XK.NoSymbol:
|
||||||
|
if keysym_is_latin_upper(ks1):
|
||||||
|
return (Xlib.XK.XK_a + ks1 - Xlib.XK.XK_A, ks1)
|
||||||
|
elif keysym_is_latin_lower(ks1):
|
||||||
|
return (ks1, Xlib.XK.XK_A + ks1 - Xlib.XK.XK_a)
|
||||||
|
else:
|
||||||
|
return (ks1, ks1)
|
||||||
|
else:
|
||||||
|
return (ks1, ks2)
|
||||||
|
|
||||||
|
|
||||||
|
def keysym_normalize(keysym):
|
||||||
|
"""Normalises a list of *keysyms*.
|
||||||
|
|
||||||
|
The implementation of this function comes from:
|
||||||
|
|
||||||
|
If the list (ignoring trailing ``NoSymbol`` entries) is a single
|
||||||
|
*KeySym* ``K``, then the list is treated as if it were the list
|
||||||
|
``K NoSymbol K NoSymbol``.
|
||||||
|
|
||||||
|
If the list (ignoring trailing ``NoSymbol`` entries) is a pair of
|
||||||
|
*KeySyms* ``K1 K2``, then the list is treated as if it were the list
|
||||||
|
``K1 K2 K1 K2``.
|
||||||
|
|
||||||
|
If the list (ignoring trailing ``NoSymbol`` entries) is a triple of
|
||||||
|
*KeySyms* ``K1 K2 K3``, then the list is treated as if it were the list
|
||||||
|
``K1 K2 K3 NoSymbol``.
|
||||||
|
|
||||||
|
This function will also group the *keysyms* using :func:`keysym_group`.
|
||||||
|
|
||||||
|
:param keysyms: A list of keysyms.
|
||||||
|
|
||||||
|
:return: the tuple ``(group_1, group_2)`` or ``None``
|
||||||
|
"""
|
||||||
|
# Remove trailing NoSymbol
|
||||||
|
stripped = list(reversed(list(
|
||||||
|
itertools.dropwhile(
|
||||||
|
lambda n: n == Xlib.XK.NoSymbol,
|
||||||
|
reversed(keysym)))))
|
||||||
|
|
||||||
|
if not stripped:
|
||||||
|
return
|
||||||
|
|
||||||
|
elif len(stripped) == 1:
|
||||||
|
return (
|
||||||
|
keysym_group(stripped[0], Xlib.XK.NoSymbol),
|
||||||
|
keysym_group(stripped[0], Xlib.XK.NoSymbol))
|
||||||
|
|
||||||
|
elif len(stripped) == 2:
|
||||||
|
return (
|
||||||
|
keysym_group(stripped[0], stripped[1]),
|
||||||
|
keysym_group(stripped[0], stripped[1]))
|
||||||
|
|
||||||
|
elif len(stripped) == 3:
|
||||||
|
return (
|
||||||
|
keysym_group(stripped[0], stripped[1]),
|
||||||
|
keysym_group(stripped[2], Xlib.XK.NoSymbol))
|
||||||
|
|
||||||
|
elif len(stripped) >= 6:
|
||||||
|
# TODO: Find out why this is necessary; using only the documented
|
||||||
|
# behaviour may lead to only a US layout being used?
|
||||||
|
return (
|
||||||
|
keysym_group(stripped[0], stripped[1]),
|
||||||
|
keysym_group(stripped[4], stripped[5]))
|
||||||
|
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
keysym_group(stripped[0], stripped[1]),
|
||||||
|
keysym_group(stripped[2], stripped[3]))
|
||||||
|
|
||||||
|
|
||||||
|
def index_to_shift(display, index):
|
||||||
|
"""Converts an index in a *key code* list to the corresponding shift state.
|
||||||
|
|
||||||
|
:param Xlib.display.Display display: The display for which to retrieve the
|
||||||
|
shift mask.
|
||||||
|
|
||||||
|
:param int index: The keyboard mapping *key code* index.
|
||||||
|
|
||||||
|
:return: a shift mask
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
(1 << 0 if index & 1 else 0) |
|
||||||
|
(alt_gr_mask(display) if index & 2 else 0))
|
||||||
|
|
||||||
|
|
||||||
|
def shift_to_index(display, shift):
|
||||||
|
"""Converts an index in a *key code* list to the corresponding shift state.
|
||||||
|
|
||||||
|
:param Xlib.display.Display display: The display for which to retrieve the
|
||||||
|
shift mask.
|
||||||
|
|
||||||
|
:param int index: The keyboard mapping *key code* index.
|
||||||
|
|
||||||
|
:retur: a shift mask
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
(1 if shift & 1 else 0) +
|
||||||
|
(2 if shift & alt_gr_mask(display) else 0))
|
||||||
|
|
||||||
|
|
||||||
|
def keyboard_mapping(display):
|
||||||
|
"""Generates a mapping from *keysyms* to *key codes* and required
|
||||||
|
modifier shift states.
|
||||||
|
|
||||||
|
:param Xlib.display.Display display: The display for which to retrieve the
|
||||||
|
keyboard mapping.
|
||||||
|
|
||||||
|
:return: the keyboard mapping
|
||||||
|
"""
|
||||||
|
mapping = {}
|
||||||
|
|
||||||
|
shift_mask = 1 << 0
|
||||||
|
group_mask = alt_gr_mask(display)
|
||||||
|
|
||||||
|
# Iterate over all keysym lists in the keyboard mapping
|
||||||
|
min_keycode = display.display.info.min_keycode
|
||||||
|
keycode_count = display.display.info.max_keycode - min_keycode + 1
|
||||||
|
for index, keysyms in enumerate(display.get_keyboard_mapping(
|
||||||
|
min_keycode, keycode_count)):
|
||||||
|
key_code = index + min_keycode
|
||||||
|
|
||||||
|
# Normalise the keysym list to yield a tuple containing the two groups
|
||||||
|
normalized = keysym_normalize(keysyms)
|
||||||
|
if not normalized:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Iterate over the groups to extract the shift and modifier state
|
||||||
|
for groups, group in zip(normalized, (False, True)):
|
||||||
|
for keysym, shift in zip(groups, (False, True)):
|
||||||
|
if not keysym:
|
||||||
|
continue
|
||||||
|
shift_state = 0 \
|
||||||
|
| (shift_mask if shift else 0) \
|
||||||
|
| (group_mask if group else 0)
|
||||||
|
|
||||||
|
# Prefer already known lesser shift states
|
||||||
|
if keysym in mapping and mapping[keysym][1] < shift_state:
|
||||||
|
continue
|
||||||
|
mapping[keysym] = (key_code, shift_state)
|
||||||
|
|
||||||
|
return mapping
|
||||||
|
|
||||||
|
|
||||||
|
def symbol_to_keysym(symbol):
|
||||||
|
"""Converts a symbol name to a *keysym*.
|
||||||
|
|
||||||
|
:param str symbol: The name of the symbol.
|
||||||
|
|
||||||
|
:return: the corresponding *keysym*, or ``0`` if it cannot be found
|
||||||
|
"""
|
||||||
|
# First try simple translation
|
||||||
|
keysym = Xlib.XK.string_to_keysym(symbol)
|
||||||
|
if keysym:
|
||||||
|
return keysym
|
||||||
|
|
||||||
|
# If that fails, try checking a module attribute of Xlib.keysymdef.xkb
|
||||||
|
if not keysym:
|
||||||
|
try:
|
||||||
|
return getattr(Xlib.keysymdef.xkb, 'XK_' + symbol, 0)
|
||||||
|
except AttributeError:
|
||||||
|
return SYMBOLS.get(symbol, (0,))[0]
|
||||||
|
|
||||||
|
|
||||||
|
class ListenerMixin(object):
|
||||||
|
"""A mixin for *X* event listeners.
|
||||||
|
|
||||||
|
Subclasses should set a value for :attr:`_EVENTS` and implement
|
||||||
|
:meth:`_handle`.
|
||||||
|
"""
|
||||||
|
#: The events for which to listen
|
||||||
|
_EVENTS = tuple()
|
||||||
|
|
||||||
|
#: We use this instance for parsing the binary data
|
||||||
|
_EVENT_PARSER = Xlib.protocol.rq.EventField(None)
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
self._display_stop = Xlib.display.Display()
|
||||||
|
self._display_record = Xlib.display.Display()
|
||||||
|
with display_manager(self._display_stop) as dm:
|
||||||
|
self._context = dm.record_create_context(
|
||||||
|
0,
|
||||||
|
[Xlib.ext.record.AllClients],
|
||||||
|
[{
|
||||||
|
'core_requests': (0, 0),
|
||||||
|
'core_replies': (0, 0),
|
||||||
|
'ext_requests': (0, 0, 0, 0),
|
||||||
|
'ext_replies': (0, 0, 0, 0),
|
||||||
|
'delivered_events': (0, 0),
|
||||||
|
'device_events': self._EVENTS,
|
||||||
|
'errors': (0, 0),
|
||||||
|
'client_started': False,
|
||||||
|
'client_died': False}])
|
||||||
|
|
||||||
|
# pylint: disable=W0702; we want to silence errors
|
||||||
|
try:
|
||||||
|
self._initialize(self._display_stop)
|
||||||
|
self._mark_ready()
|
||||||
|
if self.suppress:
|
||||||
|
with display_manager(self._display_record) as dm:
|
||||||
|
self._suppress_start(dm)
|
||||||
|
self._display_record.record_enable_context(
|
||||||
|
self._context, self._handler)
|
||||||
|
except:
|
||||||
|
# This exception will have been passed to the main thread
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
if self.suppress:
|
||||||
|
with display_manager(self._display_stop) as dm:
|
||||||
|
self._suppress_stop(dm)
|
||||||
|
self._display_record.record_free_context(self._context)
|
||||||
|
self._display_stop.close()
|
||||||
|
self._display_record.close()
|
||||||
|
# pylint: enable=W0702
|
||||||
|
|
||||||
|
def _stop_platform(self):
|
||||||
|
if not hasattr(self, '_context'):
|
||||||
|
self.wait()
|
||||||
|
# pylint: disable=W0702; we must ignore errors
|
||||||
|
try:
|
||||||
|
with display_manager(self._display_stop) as dm:
|
||||||
|
dm.record_disable_context(self._context)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
# pylint: enable=W0702
|
||||||
|
|
||||||
|
def _suppress_start(self, display):
|
||||||
|
"""Starts suppressing events.
|
||||||
|
|
||||||
|
:param Xlib.display.Display display: The display for which to suppress
|
||||||
|
events.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _suppress_stop(self, display):
|
||||||
|
"""Starts suppressing events.
|
||||||
|
|
||||||
|
:param Xlib.display.Display display: The display for which to suppress
|
||||||
|
events.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _event_mask(self):
|
||||||
|
"""The event mask.
|
||||||
|
"""
|
||||||
|
return functools.reduce(operator.__or__, self._EVENTS, 0)
|
||||||
|
|
||||||
|
@AbstractListener._emitter
|
||||||
|
def _handler(self, events):
|
||||||
|
"""The callback registered with *X* for mouse events.
|
||||||
|
|
||||||
|
This method will parse the response and call the callbacks registered
|
||||||
|
on initialisation.
|
||||||
|
|
||||||
|
:param events: The events passed by *X*. This is a binary block
|
||||||
|
parsable by :attr:`_EVENT_PARSER`.
|
||||||
|
"""
|
||||||
|
if not self.running:
|
||||||
|
raise self.StopException()
|
||||||
|
|
||||||
|
data = events.data
|
||||||
|
|
||||||
|
while data and len(data):
|
||||||
|
event, data = self._EVENT_PARSER.parse_binary_value(
|
||||||
|
data, self._display_record.display, None, None)
|
||||||
|
self._handle(self._display_stop, event)
|
||||||
|
|
||||||
|
def _initialize(self, display):
|
||||||
|
"""Initialises this listener.
|
||||||
|
|
||||||
|
This method is called immediately before the event loop, from the
|
||||||
|
handler thread.
|
||||||
|
|
||||||
|
:param display: The display being used.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _handle(self, display, event):
|
||||||
|
"""The device specific callback handler.
|
||||||
|
|
||||||
|
This method calls the appropriate callback registered when this
|
||||||
|
listener was created based on the event.
|
||||||
|
|
||||||
|
:param display: The display being used.
|
||||||
|
|
||||||
|
:param event: The event.
|
||||||
|
"""
|
||||||
|
pass
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,56 @@
|
||||||
|
# coding=utf-8
|
||||||
|
# pynput
|
||||||
|
# Copyright (C) 2015-2018 Moses Palmér
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
# later version.
|
||||||
|
#
|
||||||
|
# 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 Lesser General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
The module containing mouse classes.
|
||||||
|
|
||||||
|
See the documentation for more information.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=C0103
|
||||||
|
# Button, Controller and Listener are not constants
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if os.environ.get('__PYNPUT_GENERATE_DOCUMENTATION') == 'yes':
|
||||||
|
from ._base import Button, Controller, Listener
|
||||||
|
else:
|
||||||
|
Button = None
|
||||||
|
Controller = None
|
||||||
|
Listener = None
|
||||||
|
|
||||||
|
|
||||||
|
if sys.platform == 'darwin':
|
||||||
|
if not Button and not Controller and not Listener:
|
||||||
|
from ._darwin import Button, Controller, Listener
|
||||||
|
|
||||||
|
elif sys.platform == 'win32':
|
||||||
|
if not Button and not Controller and not Listener:
|
||||||
|
from ._win32 import Button, Controller, Listener
|
||||||
|
|
||||||
|
else:
|
||||||
|
if not Button and not Controller and not Listener:
|
||||||
|
try:
|
||||||
|
from ._xorg import Button, Controller, Listener
|
||||||
|
except ImportError:
|
||||||
|
# For now, since we only support Xlib anyway, we re-raise these
|
||||||
|
# errors to allow users to determine the cause of failures to import
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
if not Button or not Controller or not Listener:
|
||||||
|
raise ImportError('this platform is not supported')
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,263 @@
|
||||||
|
# coding=utf-8
|
||||||
|
# pynput
|
||||||
|
# Copyright (C) 2015-2018 Moses Palmér
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
# later version.
|
||||||
|
#
|
||||||
|
# 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 Lesser General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
This module contains the base implementation.
|
||||||
|
|
||||||
|
The actual interface to mouse classes is defined here, but the implementation
|
||||||
|
is located in a platform dependent module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=R0903
|
||||||
|
# We implement stubs
|
||||||
|
|
||||||
|
import enum
|
||||||
|
|
||||||
|
from pynput._util import AbstractListener
|
||||||
|
from pynput import _logger
|
||||||
|
|
||||||
|
|
||||||
|
class Button(enum.Enum):
|
||||||
|
"""The various buttons.
|
||||||
|
|
||||||
|
The actual values for these items differ between platforms. Some
|
||||||
|
platforms may have additional buttons, but these are guaranteed to be
|
||||||
|
present everywhere.
|
||||||
|
"""
|
||||||
|
#: An unknown button was pressed
|
||||||
|
unknown = 0
|
||||||
|
|
||||||
|
#: The left button
|
||||||
|
left = 1
|
||||||
|
|
||||||
|
#: The middle button
|
||||||
|
middle = 2
|
||||||
|
|
||||||
|
#: The right button
|
||||||
|
right = 3
|
||||||
|
|
||||||
|
|
||||||
|
class Controller(object):
|
||||||
|
"""A controller for sending virtual mouse events to the system.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self._log = _logger(self.__class__)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def position(self):
|
||||||
|
"""The current position of the mouse pointer.
|
||||||
|
|
||||||
|
This is the tuple ``(x, y)``, and setting it will move the pointer.
|
||||||
|
"""
|
||||||
|
return self._position_get()
|
||||||
|
|
||||||
|
@position.setter
|
||||||
|
def position(self, pos):
|
||||||
|
self._position_set(pos)
|
||||||
|
|
||||||
|
def scroll(self, dx, dy):
|
||||||
|
"""Sends scroll events.
|
||||||
|
|
||||||
|
:param int dx: The horizontal scroll. The units of scrolling is
|
||||||
|
undefined.
|
||||||
|
|
||||||
|
:param int dy: The vertical scroll. The units of scrolling is
|
||||||
|
undefined.
|
||||||
|
|
||||||
|
:raises ValueError: if the values are invalid, for example out of
|
||||||
|
bounds
|
||||||
|
"""
|
||||||
|
self._scroll(dx, dy)
|
||||||
|
|
||||||
|
def press(self, button):
|
||||||
|
"""Emits a button press event at the current position.
|
||||||
|
|
||||||
|
:param Button button: The button to press.
|
||||||
|
"""
|
||||||
|
self._press(button)
|
||||||
|
|
||||||
|
def release(self, button):
|
||||||
|
"""Emits a button release event at the current position.
|
||||||
|
|
||||||
|
:param Button button: The button to release.
|
||||||
|
"""
|
||||||
|
self._release(button)
|
||||||
|
|
||||||
|
def move(self, dx, dy):
|
||||||
|
"""Moves the mouse pointer a number of pixels from its current
|
||||||
|
position.
|
||||||
|
|
||||||
|
:param int x: The horizontal offset.
|
||||||
|
|
||||||
|
:param int dy: The vertical offset.
|
||||||
|
|
||||||
|
:raises ValueError: if the values are invalid, for example out of
|
||||||
|
bounds
|
||||||
|
"""
|
||||||
|
self.position = tuple(sum(i) for i in zip(self.position, (dx, dy)))
|
||||||
|
|
||||||
|
def click(self, button, count=1):
|
||||||
|
"""Emits a button click event at the current position.
|
||||||
|
|
||||||
|
The default implementation sends a series of press and release events.
|
||||||
|
|
||||||
|
:param Button button: The button to click.
|
||||||
|
|
||||||
|
:param int count: The number of clicks to send.
|
||||||
|
"""
|
||||||
|
with self as controller:
|
||||||
|
for _ in range(count):
|
||||||
|
controller.press(button)
|
||||||
|
controller.release(button)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""Begins a series of clicks.
|
||||||
|
|
||||||
|
In the default :meth:`click` implementation, the return value of this
|
||||||
|
method is used for the calls to :meth:`press` and :meth:`release`
|
||||||
|
instead of ``self``.
|
||||||
|
|
||||||
|
The default implementation is a no-op.
|
||||||
|
"""
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, value, traceback):
|
||||||
|
"""Ends a series of clicks.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _position_get(self):
|
||||||
|
"""The implementation of the getter for :attr:`position`.
|
||||||
|
|
||||||
|
This is a platform dependent implementation.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _position_set(self, pos):
|
||||||
|
"""The implementation of the setter for :attr:`position`.
|
||||||
|
|
||||||
|
This is a platform dependent implementation.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _scroll(self, dx, dy):
|
||||||
|
"""The implementation of the :meth:`scroll` method.
|
||||||
|
|
||||||
|
This is a platform dependent implementation.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _press(self, button):
|
||||||
|
"""The implementation of the :meth:`press` method.
|
||||||
|
|
||||||
|
This is a platform dependent implementation.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _release(self, button):
|
||||||
|
"""The implementation of the :meth:`release` method.
|
||||||
|
|
||||||
|
This is a platform dependent implementation.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=W0223; This is also an abstract class
|
||||||
|
class Listener(AbstractListener):
|
||||||
|
"""A listener for mouse events.
|
||||||
|
|
||||||
|
Instances of this class can be used as context managers. This is equivalent
|
||||||
|
to the following code::
|
||||||
|
|
||||||
|
listener.start()
|
||||||
|
try:
|
||||||
|
listener.wait()
|
||||||
|
with_statements()
|
||||||
|
finally:
|
||||||
|
listener.stop()
|
||||||
|
|
||||||
|
This class inherits from :class:`threading.Thread` and supports all its
|
||||||
|
methods. It will set :attr:`daemon` to ``True`` when created.
|
||||||
|
|
||||||
|
:param callable on_move: The callback to call when mouse move events occur.
|
||||||
|
|
||||||
|
It will be called with the arguments ``(x, y)``, which is the new
|
||||||
|
pointer position. If this callback raises :class:`StopException` or
|
||||||
|
returns ``False``, the listener is stopped.
|
||||||
|
|
||||||
|
:param callable on_click: The callback to call when a mouse button is
|
||||||
|
clicked.
|
||||||
|
|
||||||
|
It will be called with the arguments ``(x, y, button, pressed)``,
|
||||||
|
where ``(x, y)`` is the new pointer position, ``button`` is one of the
|
||||||
|
:class:`Button` values and ``pressed`` is whether the button was
|
||||||
|
pressed.
|
||||||
|
|
||||||
|
If this callback raises :class:`StopException` or returns ``False``,
|
||||||
|
the listener is stopped.
|
||||||
|
|
||||||
|
:param callable on_scroll: The callback to call when mouse scroll
|
||||||
|
events occur.
|
||||||
|
|
||||||
|
It will be called with the arguments ``(x, y, dx, dy)``, where
|
||||||
|
``(x, y)`` is the new pointer position, and ``(dx, dy)`` is the scroll
|
||||||
|
vector.
|
||||||
|
|
||||||
|
If this callback raises :class:`StopException` or returns ``False``,
|
||||||
|
the listener is stopped.
|
||||||
|
|
||||||
|
:param bool suppress: Whether to suppress events. Setting this to ``True``
|
||||||
|
will prevent the input events from being passed to the rest of the
|
||||||
|
system.
|
||||||
|
|
||||||
|
:param kwargs: Any non-standard platform dependent options. These should be
|
||||||
|
prefixed with the platform name thus: ``darwin_``, ``xorg_`` or
|
||||||
|
``win32_``.
|
||||||
|
|
||||||
|
Supported values are:
|
||||||
|
|
||||||
|
``darwin_intercept``
|
||||||
|
A callable taking the arguments ``(event_type, event)``, where
|
||||||
|
``event_type`` is any mouse related event type constant, and
|
||||||
|
``event`` is a ``CGEventRef``.
|
||||||
|
|
||||||
|
This callable can freely modify the event using functions like
|
||||||
|
``Quartz.CGEventSetIntegerValueField``. If this callable does not
|
||||||
|
return the event, the event is suppressed system wide.
|
||||||
|
|
||||||
|
``win32_event_filter``
|
||||||
|
A callable taking the arguments ``(msg, data)``, where ``msg`` is
|
||||||
|
the current message, and ``data`` associated data as a
|
||||||
|
`MSLLHOOKSTRUCT <https://msdn.microsoft.com/en-us/library/windows/desktop/ms644970(v=vs.85).aspx>`_.
|
||||||
|
|
||||||
|
If this callback returns ``False``, the event will not
|
||||||
|
be propagated to the listener callback.
|
||||||
|
|
||||||
|
If ``self.suppress_event()`` is called, the event is suppressed
|
||||||
|
system wide.
|
||||||
|
"""
|
||||||
|
def __init__(self, on_move=None, on_click=None, on_scroll=None,
|
||||||
|
suppress=False, **kwargs):
|
||||||
|
self._log = _logger(self.__class__)
|
||||||
|
prefix = self.__class__.__module__.rsplit('.', 1)[-1][1:] + '_'
|
||||||
|
self._options = {
|
||||||
|
key[len(prefix):]: value
|
||||||
|
for key, value in kwargs.items()
|
||||||
|
if key.startswith(prefix)}
|
||||||
|
super(Listener, self).__init__(
|
||||||
|
on_move=on_move, on_click=on_click, on_scroll=on_scroll,
|
||||||
|
suppress=suppress)
|
||||||
|
# pylint: enable=W0223
|
|
@ -0,0 +1,215 @@
|
||||||
|
# coding=utf-8
|
||||||
|
# pynput
|
||||||
|
# Copyright (C) 2015-2018 Moses Palmér
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
# later version.
|
||||||
|
#
|
||||||
|
# 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 Lesser General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
The mouse implementation for *OSX*.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=C0111
|
||||||
|
# The documentation is extracted from the base classes
|
||||||
|
|
||||||
|
# pylint: disable=R0903
|
||||||
|
# We implement stubs
|
||||||
|
|
||||||
|
import enum
|
||||||
|
import Quartz
|
||||||
|
|
||||||
|
from AppKit import NSEvent
|
||||||
|
|
||||||
|
from pynput._util.darwin import (
|
||||||
|
ListenerMixin)
|
||||||
|
from . import _base
|
||||||
|
|
||||||
|
|
||||||
|
def _button_value(base_name, mouse_button):
|
||||||
|
"""Generates the value tuple for a :class:`Button` value.
|
||||||
|
|
||||||
|
:param str base_name: The base name for the button. This shuld be a string
|
||||||
|
like ``'kCGEventLeftMouse'``.
|
||||||
|
|
||||||
|
:param int mouse_button: The mouse button ID.
|
||||||
|
|
||||||
|
:return: a value tuple
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
tuple(
|
||||||
|
getattr(Quartz, '%sMouse%s' % (base_name, name))
|
||||||
|
for name in ('Down', 'Up', 'Dragged')),
|
||||||
|
mouse_button)
|
||||||
|
|
||||||
|
|
||||||
|
class Button(enum.Enum):
|
||||||
|
"""The various buttons.
|
||||||
|
"""
|
||||||
|
unknown = None
|
||||||
|
left = _button_value('kCGEventLeft', 0)
|
||||||
|
middle = _button_value('kCGEventOther', 2)
|
||||||
|
right = _button_value('kCGEventRight', 1)
|
||||||
|
|
||||||
|
|
||||||
|
class Controller(_base.Controller):
|
||||||
|
#: The scroll speed
|
||||||
|
_SCROLL_SPEED = 5
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(Controller, self).__init__(*args, **kwargs)
|
||||||
|
self._click = None
|
||||||
|
self._drag_button = None
|
||||||
|
|
||||||
|
def _position_get(self):
|
||||||
|
pos = NSEvent.mouseLocation()
|
||||||
|
|
||||||
|
return pos.x, Quartz.CGDisplayPixelsHigh(0) - pos.y
|
||||||
|
|
||||||
|
def _position_set(self, pos):
|
||||||
|
try:
|
||||||
|
(_, _, mouse_type), mouse_button = self._drag_button.value
|
||||||
|
except AttributeError:
|
||||||
|
mouse_type = Quartz.kCGEventMouseMoved
|
||||||
|
mouse_button = 0
|
||||||
|
|
||||||
|
Quartz.CGEventPost(
|
||||||
|
Quartz.kCGHIDEventTap,
|
||||||
|
Quartz.CGEventCreateMouseEvent(
|
||||||
|
None,
|
||||||
|
mouse_type,
|
||||||
|
pos,
|
||||||
|
mouse_button))
|
||||||
|
|
||||||
|
def _scroll(self, dx, dy):
|
||||||
|
while dx != 0 or dy != 0:
|
||||||
|
xval = 1 if dx > 0 else -1 if dx < 0 else 0
|
||||||
|
dx -= xval
|
||||||
|
yval = 1 if dy > 0 else -1 if dy < 0 else 0
|
||||||
|
dy -= yval
|
||||||
|
|
||||||
|
Quartz.CGEventPost(
|
||||||
|
Quartz.kCGHIDEventTap,
|
||||||
|
Quartz.CGEventCreateScrollWheelEvent(
|
||||||
|
None,
|
||||||
|
Quartz.kCGScrollEventUnitPixel,
|
||||||
|
2,
|
||||||
|
yval * self._SCROLL_SPEED,
|
||||||
|
xval * self._SCROLL_SPEED))
|
||||||
|
|
||||||
|
def _press(self, button):
|
||||||
|
(press, _, _), mouse_button = button.value
|
||||||
|
event = Quartz.CGEventCreateMouseEvent(
|
||||||
|
None,
|
||||||
|
press,
|
||||||
|
self.position,
|
||||||
|
mouse_button)
|
||||||
|
|
||||||
|
# If we are performing a click, we need to set this state flag
|
||||||
|
if self._click is not None:
|
||||||
|
self._click += 1
|
||||||
|
Quartz.CGEventSetIntegerValueField(
|
||||||
|
event,
|
||||||
|
Quartz.kCGMouseEventClickState,
|
||||||
|
self._click)
|
||||||
|
|
||||||
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, event)
|
||||||
|
|
||||||
|
# Store the button to enable dragging
|
||||||
|
self._drag_button = button
|
||||||
|
|
||||||
|
def _release(self, button):
|
||||||
|
(_, release, _), mouse_button = button.value
|
||||||
|
event = Quartz.CGEventCreateMouseEvent(
|
||||||
|
None,
|
||||||
|
release,
|
||||||
|
self.position,
|
||||||
|
mouse_button)
|
||||||
|
|
||||||
|
# If we are performing a click, we need to set this state flag
|
||||||
|
if self._click is not None:
|
||||||
|
Quartz.CGEventSetIntegerValueField(
|
||||||
|
event,
|
||||||
|
Quartz.kCGMouseEventClickState,
|
||||||
|
self._click)
|
||||||
|
|
||||||
|
Quartz.CGEventPost(Quartz.kCGHIDEventTap, event)
|
||||||
|
|
||||||
|
if button == self._drag_button:
|
||||||
|
self._drag_button = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self._click = 0
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, value, traceback):
|
||||||
|
self._click = None
|
||||||
|
|
||||||
|
|
||||||
|
class Listener(ListenerMixin, _base.Listener):
|
||||||
|
#: The events that we listen to
|
||||||
|
_EVENTS = (
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventMouseMoved) |
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseDown) |
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseUp) |
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseDragged) |
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseDown) |
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseUp) |
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseDragged) |
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseDown) |
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseUp) |
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseDragged) |
|
||||||
|
Quartz.CGEventMaskBit(Quartz.kCGEventScrollWheel))
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(Listener, self).__init__(*args, **kwargs)
|
||||||
|
self._intercept = self._options.get(
|
||||||
|
'intercept',
|
||||||
|
None)
|
||||||
|
|
||||||
|
def _handle(self, dummy_proxy, event_type, event, dummy_refcon):
|
||||||
|
"""The callback registered with *Mac OSX* for mouse events.
|
||||||
|
|
||||||
|
This method will call the callbacks registered on initialisation.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
(px, py) = Quartz.CGEventGetLocation(event)
|
||||||
|
except AttributeError:
|
||||||
|
# This happens during teardown of the virtual machine
|
||||||
|
return
|
||||||
|
|
||||||
|
# Quickly detect the most common event type
|
||||||
|
if event_type == Quartz.kCGEventMouseMoved:
|
||||||
|
self.on_move(px, py)
|
||||||
|
|
||||||
|
elif event_type == Quartz.kCGEventScrollWheel:
|
||||||
|
dx = Quartz.CGEventGetIntegerValueField(
|
||||||
|
event,
|
||||||
|
Quartz.kCGScrollWheelEventDeltaAxis2)
|
||||||
|
dy = Quartz.CGEventGetIntegerValueField(
|
||||||
|
event,
|
||||||
|
Quartz.kCGScrollWheelEventDeltaAxis1)
|
||||||
|
self.on_scroll(px, py, dx, dy)
|
||||||
|
|
||||||
|
else:
|
||||||
|
for button in Button:
|
||||||
|
try:
|
||||||
|
(press, release, drag), _ = button.value
|
||||||
|
except TypeError:
|
||||||
|
# Button.unknown cannot be enumerated
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Press and release generate click events, and drag
|
||||||
|
# generates move events
|
||||||
|
if event_type in (press, release):
|
||||||
|
self.on_click(px, py, button, event_type == press)
|
||||||
|
elif event_type == drag:
|
||||||
|
self.on_move(px, py)
|
|
@ -0,0 +1,195 @@
|
||||||
|
# coding=utf-8
|
||||||
|
# pynput
|
||||||
|
# Copyright (C) 2015-2018 Moses Palmér
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
# later version.
|
||||||
|
#
|
||||||
|
# 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 Lesser General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
The mouse implementation for *Windows*.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=C0111
|
||||||
|
# The documentation is extracted from the base classes
|
||||||
|
|
||||||
|
# pylint: disable=R0903
|
||||||
|
# We implement stubs
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
import enum
|
||||||
|
|
||||||
|
from ctypes import (
|
||||||
|
windll,
|
||||||
|
wintypes)
|
||||||
|
|
||||||
|
from pynput._util import NotifierMixin
|
||||||
|
from pynput._util.win32 import (
|
||||||
|
INPUT,
|
||||||
|
INPUT_union,
|
||||||
|
ListenerMixin,
|
||||||
|
MOUSEINPUT,
|
||||||
|
SendInput,
|
||||||
|
SystemHook)
|
||||||
|
from . import _base
|
||||||
|
|
||||||
|
|
||||||
|
class Button(enum.Enum):
|
||||||
|
"""The various buttons.
|
||||||
|
"""
|
||||||
|
unknown = None
|
||||||
|
left = (MOUSEINPUT.LEFTUP, MOUSEINPUT.LEFTDOWN)
|
||||||
|
middle = (MOUSEINPUT.MIDDLEUP, MOUSEINPUT.MIDDLEDOWN)
|
||||||
|
right = (MOUSEINPUT.RIGHTUP, MOUSEINPUT.RIGHTDOWN)
|
||||||
|
|
||||||
|
|
||||||
|
class Controller(NotifierMixin, _base.Controller):
|
||||||
|
__GetCursorPos = windll.user32.GetCursorPos
|
||||||
|
__SetCursorPos = windll.user32.SetCursorPos
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(Controller, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def _position_get(self):
|
||||||
|
point = wintypes.POINT()
|
||||||
|
if self.__GetCursorPos(ctypes.byref(point)):
|
||||||
|
return (point.x, point.y)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _position_set(self, pos):
|
||||||
|
pos = int(pos[0]), int(pos[1])
|
||||||
|
self.__SetCursorPos(*pos)
|
||||||
|
self._emit('on_move', *pos)
|
||||||
|
|
||||||
|
def _scroll(self, dx, dy):
|
||||||
|
if dy:
|
||||||
|
SendInput(
|
||||||
|
1,
|
||||||
|
ctypes.byref(INPUT(
|
||||||
|
type=INPUT.MOUSE,
|
||||||
|
value=INPUT_union(
|
||||||
|
mi=MOUSEINPUT(
|
||||||
|
dwFlags=MOUSEINPUT.WHEEL,
|
||||||
|
mouseData=int(dy))))),
|
||||||
|
ctypes.sizeof(INPUT))
|
||||||
|
|
||||||
|
if dx:
|
||||||
|
SendInput(
|
||||||
|
1,
|
||||||
|
ctypes.byref(INPUT(
|
||||||
|
type=INPUT.MOUSE,
|
||||||
|
value=INPUT_union(
|
||||||
|
mi=MOUSEINPUT(
|
||||||
|
dwFlags=MOUSEINPUT.HWHEEL,
|
||||||
|
mouseData=int(dx))))),
|
||||||
|
ctypes.sizeof(INPUT))
|
||||||
|
|
||||||
|
if dx or dy:
|
||||||
|
px, py = self._position_get()
|
||||||
|
self._emit('on_scroll', px, py, dx, dy)
|
||||||
|
|
||||||
|
def _press(self, button):
|
||||||
|
SendInput(
|
||||||
|
1,
|
||||||
|
ctypes.byref(INPUT(
|
||||||
|
type=INPUT.MOUSE,
|
||||||
|
value=INPUT_union(
|
||||||
|
mi=MOUSEINPUT(
|
||||||
|
dwFlags=button.value[1])))),
|
||||||
|
ctypes.sizeof(INPUT))
|
||||||
|
|
||||||
|
def _release(self, button):
|
||||||
|
SendInput(
|
||||||
|
1,
|
||||||
|
ctypes.byref(INPUT(
|
||||||
|
type=INPUT.MOUSE,
|
||||||
|
value=INPUT_union(
|
||||||
|
mi=MOUSEINPUT(
|
||||||
|
dwFlags=button.value[0])))),
|
||||||
|
ctypes.sizeof(INPUT))
|
||||||
|
|
||||||
|
|
||||||
|
@Controller._receiver
|
||||||
|
class Listener(ListenerMixin, _base.Listener):
|
||||||
|
#: The Windows hook ID for low level mouse events, ``WH_MOUSE_LL``
|
||||||
|
_EVENTS = 14
|
||||||
|
|
||||||
|
WM_LBUTTONDOWN = 0x0201
|
||||||
|
WM_LBUTTONUP = 0x0202
|
||||||
|
WM_MBUTTONDOWN = 0x0207
|
||||||
|
WM_MBUTTONUP = 0x0208
|
||||||
|
WM_MOUSEMOVE = 0x0200
|
||||||
|
WM_MOUSEWHEEL = 0x020A
|
||||||
|
WM_MOUSEHWHEEL = 0x020E
|
||||||
|
WM_RBUTTONDOWN = 0x0204
|
||||||
|
WM_RBUTTONUP = 0x0205
|
||||||
|
|
||||||
|
_WHEEL_DELTA = 120
|
||||||
|
|
||||||
|
#: A mapping from messages to button events
|
||||||
|
CLICK_BUTTONS = {
|
||||||
|
WM_LBUTTONDOWN: (Button.left, True),
|
||||||
|
WM_LBUTTONUP: (Button.left, False),
|
||||||
|
WM_MBUTTONDOWN: (Button.middle, True),
|
||||||
|
WM_MBUTTONUP: (Button.middle, False),
|
||||||
|
WM_RBUTTONDOWN: (Button.right, True),
|
||||||
|
WM_RBUTTONUP: (Button.right, False)}
|
||||||
|
|
||||||
|
#: A mapping from messages to scroll vectors
|
||||||
|
SCROLL_BUTTONS = {
|
||||||
|
WM_MOUSEWHEEL: (0, 1),
|
||||||
|
WM_MOUSEHWHEEL: (1, 0)}
|
||||||
|
|
||||||
|
_HANDLED_EXCEPTIONS = (
|
||||||
|
SystemHook.SuppressException,)
|
||||||
|
|
||||||
|
class _MSLLHOOKSTRUCT(ctypes.Structure):
|
||||||
|
"""Contains information about a mouse event passed to a ``WH_MOUSE_LL``
|
||||||
|
hook procedure, ``MouseProc``.
|
||||||
|
"""
|
||||||
|
_fields_ = [
|
||||||
|
('pt', wintypes.POINT),
|
||||||
|
('mouseData', wintypes.DWORD),
|
||||||
|
('flags', wintypes.DWORD),
|
||||||
|
('time', wintypes.DWORD),
|
||||||
|
('dwExtraInfo', ctypes.c_void_p)]
|
||||||
|
|
||||||
|
#: A pointer to a :class:`_MSLLHOOKSTRUCT`
|
||||||
|
_LPMSLLHOOKSTRUCT = ctypes.POINTER(_MSLLHOOKSTRUCT)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(Listener, self).__init__(*args, **kwargs)
|
||||||
|
self._event_filter = self._options.get(
|
||||||
|
'event_filter',
|
||||||
|
lambda msg, data: True)
|
||||||
|
|
||||||
|
def _handle(self, code, msg, lpdata):
|
||||||
|
if code != SystemHook.HC_ACTION:
|
||||||
|
return
|
||||||
|
|
||||||
|
data = ctypes.cast(lpdata, self._LPMSLLHOOKSTRUCT).contents
|
||||||
|
|
||||||
|
# Suppress further propagation of the event if it is filtered
|
||||||
|
if self._event_filter(msg, data) is False:
|
||||||
|
return
|
||||||
|
|
||||||
|
if msg == self.WM_MOUSEMOVE:
|
||||||
|
self.on_move(data.pt.x, data.pt.y)
|
||||||
|
|
||||||
|
elif msg in self.CLICK_BUTTONS:
|
||||||
|
button, pressed = self.CLICK_BUTTONS[msg]
|
||||||
|
self.on_click(data.pt.x, data.pt.y, button, pressed)
|
||||||
|
|
||||||
|
elif msg in self.SCROLL_BUTTONS:
|
||||||
|
mx, my = self.SCROLL_BUTTONS[msg]
|
||||||
|
dd = wintypes.SHORT(data.mouseData >> 16).value // self._WHEEL_DELTA
|
||||||
|
self.on_scroll(data.pt.x, data.pt.y, dd * mx, dd * my)
|
|
@ -0,0 +1,174 @@
|
||||||
|
# coding=utf-8
|
||||||
|
# pynput
|
||||||
|
# Copyright (C) 2015-2018 Moses Palmér
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
# later version.
|
||||||
|
#
|
||||||
|
# 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 Lesser General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
The keyboard implementation for *Xorg*.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=C0111
|
||||||
|
# The documentation is extracted from the base classes
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=E1101,E1102
|
||||||
|
# We dynamically generate the Button class
|
||||||
|
|
||||||
|
# pylint: disable=R0903
|
||||||
|
# We implement stubs
|
||||||
|
|
||||||
|
import enum
|
||||||
|
import Xlib.display
|
||||||
|
import Xlib.ext
|
||||||
|
import Xlib.ext.xtest
|
||||||
|
import Xlib.X
|
||||||
|
import Xlib.protocol
|
||||||
|
|
||||||
|
from pynput._util.xorg import (
|
||||||
|
display_manager,
|
||||||
|
ListenerMixin)
|
||||||
|
from . import _base
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=C0103
|
||||||
|
Button = enum.Enum(
|
||||||
|
'Button',
|
||||||
|
module=__name__,
|
||||||
|
names=[
|
||||||
|
('unknown', None),
|
||||||
|
('left', 1),
|
||||||
|
('middle', 2),
|
||||||
|
('right', 3),
|
||||||
|
('scroll_up', 4),
|
||||||
|
('scroll_down', 5),
|
||||||
|
('scroll_left', 6),
|
||||||
|
('scroll_right', 7)] + [
|
||||||
|
('button%d' % i, i)
|
||||||
|
for i in range(8, 31)])
|
||||||
|
# pylint: enable=C0103
|
||||||
|
|
||||||
|
|
||||||
|
class Controller(_base.Controller):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(Controller, self).__init__(*args, **kwargs)
|
||||||
|
self._display = Xlib.display.Display()
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if hasattr(self, '_display'):
|
||||||
|
self._display.close()
|
||||||
|
|
||||||
|
def _position_get(self):
|
||||||
|
with display_manager(self._display) as dm:
|
||||||
|
qp = dm.screen().root.query_pointer()
|
||||||
|
return (qp.root_x, qp.root_y)
|
||||||
|
|
||||||
|
def _position_set(self, pos):
|
||||||
|
px, py = self._check_bounds(*pos)
|
||||||
|
with display_manager(self._display) as dm:
|
||||||
|
Xlib.ext.xtest.fake_input(dm, Xlib.X.MotionNotify, x=px, y=py)
|
||||||
|
|
||||||
|
def _scroll(self, dx, dy):
|
||||||
|
dx, dy = self._check_bounds(dx, dy)
|
||||||
|
if dy:
|
||||||
|
self.click(
|
||||||
|
button=Button.scroll_up if dy > 0 else Button.scroll_down,
|
||||||
|
count=abs(dy))
|
||||||
|
|
||||||
|
if dx:
|
||||||
|
self.click(
|
||||||
|
button=Button.scroll_right if dx > 0 else Button.scroll_left,
|
||||||
|
count=abs(dx))
|
||||||
|
|
||||||
|
def _press(self, button):
|
||||||
|
with display_manager(self._display) as dm:
|
||||||
|
Xlib.ext.xtest.fake_input(dm, Xlib.X.ButtonPress, button.value)
|
||||||
|
|
||||||
|
def _release(self, button):
|
||||||
|
with display_manager(self._display) as dm:
|
||||||
|
Xlib.ext.xtest.fake_input(dm, Xlib.X.ButtonRelease, button.value)
|
||||||
|
|
||||||
|
def _check_bounds(self, *args):
|
||||||
|
"""Checks the arguments and makes sure they are within the bounds of a
|
||||||
|
short integer.
|
||||||
|
|
||||||
|
:param args: The values to verify.
|
||||||
|
"""
|
||||||
|
if not all(
|
||||||
|
(-0x7fff - 1) <= number <= 0x7fff
|
||||||
|
for number in args):
|
||||||
|
raise ValueError(args)
|
||||||
|
else:
|
||||||
|
return tuple(int(p) for p in args)
|
||||||
|
|
||||||
|
|
||||||
|
class Listener(ListenerMixin, _base.Listener):
|
||||||
|
#: A mapping from button values to scroll directions
|
||||||
|
_SCROLL_BUTTONS = {
|
||||||
|
Button.scroll_up.value: (0, 1),
|
||||||
|
Button.scroll_down.value: (0, -1),
|
||||||
|
Button.scroll_right.value: (1, 0),
|
||||||
|
Button.scroll_left.value: (-1, 0)}
|
||||||
|
|
||||||
|
_EVENTS = (
|
||||||
|
Xlib.X.ButtonPressMask,
|
||||||
|
Xlib.X.ButtonReleaseMask)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(Listener, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def _handle(self, dummy_display, event):
|
||||||
|
px = event.root_x
|
||||||
|
py = event.root_y
|
||||||
|
|
||||||
|
if event.type == Xlib.X.ButtonPress:
|
||||||
|
# Scroll events are sent as button presses with the scroll
|
||||||
|
# button codes
|
||||||
|
scroll = self._SCROLL_BUTTONS.get(event.detail, None)
|
||||||
|
if scroll:
|
||||||
|
self.on_scroll(px, py, *scroll)
|
||||||
|
else:
|
||||||
|
self.on_click(px, py, self._button(event.detail), True)
|
||||||
|
|
||||||
|
elif event.type == Xlib.X.ButtonRelease:
|
||||||
|
# Send an event only if this was not a scroll event
|
||||||
|
if event.detail not in self._SCROLL_BUTTONS:
|
||||||
|
self.on_click(px, py, self._button(event.detail), False)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.on_move(px, py)
|
||||||
|
|
||||||
|
|
||||||
|
def _suppress_start(self, display):
|
||||||
|
display.screen().root.grab_pointer(
|
||||||
|
True, self._event_mask, Xlib.X.GrabModeAsync, Xlib.X.GrabModeAsync,
|
||||||
|
0, 0, Xlib.X.CurrentTime)
|
||||||
|
|
||||||
|
def _suppress_stop(self, display):
|
||||||
|
display.ungrab_pointer(Xlib.X.CurrentTime)
|
||||||
|
|
||||||
|
# pylint: disable=R0201
|
||||||
|
def _button(self, detail):
|
||||||
|
"""Creates a mouse button from an event detail.
|
||||||
|
|
||||||
|
If the button is unknown, :attr:`Button.unknown` is returned.
|
||||||
|
|
||||||
|
:param detail: The event detail.
|
||||||
|
|
||||||
|
:return: a button
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return Button(detail)
|
||||||
|
except ValueError:
|
||||||
|
return Button.unknown
|
||||||
|
# pylint: enable=R0201
|
|
@ -0,0 +1,186 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Form implementation generated from reading ui file 'commandeditwnd.ui'
|
||||||
|
#
|
||||||
|
# Created by: PyQt5 UI code generator 5.12.1
|
||||||
|
#
|
||||||
|
# WARNING! All changes made in this file will be lost!
|
||||||
|
|
||||||
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
|
|
||||||
|
class Ui_CommandEditDialog(object):
|
||||||
|
def setupUi(self, CommandEditDialog):
|
||||||
|
CommandEditDialog.setObjectName("CommandEditDialog")
|
||||||
|
CommandEditDialog.resize(927, 419)
|
||||||
|
font = QtGui.QFont()
|
||||||
|
font.setPointSize(10)
|
||||||
|
CommandEditDialog.setFont(font)
|
||||||
|
self.gridLayout = QtWidgets.QGridLayout(CommandEditDialog)
|
||||||
|
self.gridLayout.setContentsMargins(25, 20, 20, 15)
|
||||||
|
self.gridLayout.setObjectName("gridLayout")
|
||||||
|
spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||||
|
self.gridLayout.addItem(spacerItem, 4, 0, 1, 2)
|
||||||
|
self.horizontalLayout_6 = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout_6.setObjectName("horizontalLayout_6")
|
||||||
|
self.label_2 = QtWidgets.QLabel(CommandEditDialog)
|
||||||
|
self.label_2.setObjectName("label_2")
|
||||||
|
self.horizontalLayout_6.addWidget(self.label_2)
|
||||||
|
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||||
|
self.horizontalLayout_6.addItem(spacerItem1)
|
||||||
|
self.gridLayout.addLayout(self.horizontalLayout_6, 2, 0, 1, 2)
|
||||||
|
spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||||
|
self.gridLayout.addItem(spacerItem2, 1, 0, 1, 1)
|
||||||
|
self.horizontalLayout = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||||
|
self.label = QtWidgets.QLabel(CommandEditDialog)
|
||||||
|
self.label.setObjectName("label")
|
||||||
|
self.horizontalLayout.addWidget(self.label)
|
||||||
|
self.say = QtWidgets.QLineEdit(CommandEditDialog)
|
||||||
|
self.say.setObjectName("say")
|
||||||
|
self.horizontalLayout.addWidget(self.say)
|
||||||
|
spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||||
|
self.horizontalLayout.addItem(spacerItem3)
|
||||||
|
self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 8)
|
||||||
|
self.horizontalLayout_9 = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout_9.setObjectName("horizontalLayout_9")
|
||||||
|
self.oneExe = QtWidgets.QRadioButton(CommandEditDialog)
|
||||||
|
self.oneExe.setAutoExclusive(True)
|
||||||
|
self.oneExe.setObjectName("oneExe")
|
||||||
|
self.horizontalLayout_9.addWidget(self.oneExe)
|
||||||
|
spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||||
|
self.horizontalLayout_9.addItem(spacerItem4)
|
||||||
|
self.gridLayout.addLayout(self.horizontalLayout_9, 6, 0, 1, 1)
|
||||||
|
self.horizontalLayout_10 = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout_10.setObjectName("horizontalLayout_10")
|
||||||
|
self.continueExe = QtWidgets.QRadioButton(CommandEditDialog)
|
||||||
|
self.continueExe.setAutoExclusive(True)
|
||||||
|
self.continueExe.setObjectName("continueExe")
|
||||||
|
self.horizontalLayout_10.addWidget(self.continueExe)
|
||||||
|
spacerItem5 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||||
|
self.horizontalLayout_10.addItem(spacerItem5)
|
||||||
|
self.gridLayout.addLayout(self.horizontalLayout_10, 7, 0, 1, 1)
|
||||||
|
self.horizontalLayout_11 = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout_11.setObjectName("horizontalLayout_11")
|
||||||
|
self.repeatExe = QtWidgets.QRadioButton(CommandEditDialog)
|
||||||
|
self.repeatExe.setAutoExclusive(True)
|
||||||
|
self.repeatExe.setObjectName("repeatExe")
|
||||||
|
self.horizontalLayout_11.addWidget(self.repeatExe)
|
||||||
|
self.repeatCnt = QtWidgets.QSpinBox(CommandEditDialog)
|
||||||
|
self.repeatCnt.setMinimum(2)
|
||||||
|
self.repeatCnt.setObjectName("repeatCnt")
|
||||||
|
self.horizontalLayout_11.addWidget(self.repeatCnt)
|
||||||
|
self.label_3 = QtWidgets.QLabel(CommandEditDialog)
|
||||||
|
self.label_3.setObjectName("label_3")
|
||||||
|
self.horizontalLayout_11.addWidget(self.label_3)
|
||||||
|
spacerItem6 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||||
|
self.horizontalLayout_11.addItem(spacerItem6)
|
||||||
|
self.gridLayout.addLayout(self.horizontalLayout_11, 8, 0, 1, 1)
|
||||||
|
self.horizontalLayout_7 = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout_7.setObjectName("horizontalLayout_7")
|
||||||
|
self.verticalLayout_5 = QtWidgets.QVBoxLayout()
|
||||||
|
self.verticalLayout_5.setObjectName("verticalLayout_5")
|
||||||
|
self.keyBut = QtWidgets.QPushButton(CommandEditDialog)
|
||||||
|
self.keyBut.setAutoDefault(False)
|
||||||
|
self.keyBut.setObjectName("keyBut")
|
||||||
|
self.verticalLayout_5.addWidget(self.keyBut)
|
||||||
|
self.mouseBut = QtWidgets.QPushButton(CommandEditDialog)
|
||||||
|
self.mouseBut.setAutoDefault(False)
|
||||||
|
self.mouseBut.setObjectName("mouseBut")
|
||||||
|
self.verticalLayout_5.addWidget(self.mouseBut)
|
||||||
|
self.pauseBut = QtWidgets.QPushButton(CommandEditDialog)
|
||||||
|
self.pauseBut.setAutoDefault(False)
|
||||||
|
self.pauseBut.setObjectName("pauseBut")
|
||||||
|
self.verticalLayout_5.addWidget(self.pauseBut)
|
||||||
|
self.otherBut = QtWidgets.QPushButton(CommandEditDialog)
|
||||||
|
self.otherBut.setAutoDefault(False)
|
||||||
|
self.otherBut.setObjectName("otherBut")
|
||||||
|
self.verticalLayout_5.addWidget(self.otherBut)
|
||||||
|
self.horizontalLayout_7.addLayout(self.verticalLayout_5)
|
||||||
|
self.actionsListWidget = QtWidgets.QListWidget(CommandEditDialog)
|
||||||
|
font = QtGui.QFont()
|
||||||
|
font.setPointSize(11)
|
||||||
|
self.actionsListWidget.setFont(font)
|
||||||
|
self.actionsListWidget.setObjectName("actionsListWidget")
|
||||||
|
self.horizontalLayout_7.addWidget(self.actionsListWidget)
|
||||||
|
self.verticalLayout_6 = QtWidgets.QVBoxLayout()
|
||||||
|
self.verticalLayout_6.setObjectName("verticalLayout_6")
|
||||||
|
self.upBut = QtWidgets.QPushButton(CommandEditDialog)
|
||||||
|
self.upBut.setAutoDefault(False)
|
||||||
|
self.upBut.setObjectName("upBut")
|
||||||
|
self.verticalLayout_6.addWidget(self.upBut)
|
||||||
|
self.downBut = QtWidgets.QPushButton(CommandEditDialog)
|
||||||
|
self.downBut.setAutoDefault(False)
|
||||||
|
self.downBut.setObjectName("downBut")
|
||||||
|
self.verticalLayout_6.addWidget(self.downBut)
|
||||||
|
self.editBut = QtWidgets.QPushButton(CommandEditDialog)
|
||||||
|
self.editBut.setAutoDefault(False)
|
||||||
|
self.editBut.setObjectName("editBut")
|
||||||
|
self.verticalLayout_6.addWidget(self.editBut)
|
||||||
|
self.deleteBut = QtWidgets.QPushButton(CommandEditDialog)
|
||||||
|
self.deleteBut.setAutoDefault(False)
|
||||||
|
self.deleteBut.setObjectName("deleteBut")
|
||||||
|
self.verticalLayout_6.addWidget(self.deleteBut)
|
||||||
|
self.horizontalLayout_7.addLayout(self.verticalLayout_6)
|
||||||
|
self.gridLayout.addLayout(self.horizontalLayout_7, 3, 0, 1, 7)
|
||||||
|
self.horizontalLayout_8 = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout_8.setObjectName("horizontalLayout_8")
|
||||||
|
self.asyncChk = QtWidgets.QCheckBox(CommandEditDialog)
|
||||||
|
self.asyncChk.setChecked(True)
|
||||||
|
self.asyncChk.setObjectName("asyncChk")
|
||||||
|
self.horizontalLayout_8.addWidget(self.asyncChk)
|
||||||
|
spacerItem7 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||||
|
self.horizontalLayout_8.addItem(spacerItem7)
|
||||||
|
self.gridLayout.addLayout(self.horizontalLayout_8, 5, 0, 1, 3)
|
||||||
|
self.horizontalLayout_12 = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout_12.setSpacing(20)
|
||||||
|
self.horizontalLayout_12.setObjectName("horizontalLayout_12")
|
||||||
|
spacerItem8 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||||
|
self.horizontalLayout_12.addItem(spacerItem8)
|
||||||
|
self.ok = QtWidgets.QPushButton(CommandEditDialog)
|
||||||
|
self.ok.setMinimumSize(QtCore.QSize(130, 0))
|
||||||
|
self.ok.setAutoDefault(False)
|
||||||
|
self.ok.setObjectName("ok")
|
||||||
|
self.horizontalLayout_12.addWidget(self.ok)
|
||||||
|
self.cancel = QtWidgets.QPushButton(CommandEditDialog)
|
||||||
|
self.cancel.setMinimumSize(QtCore.QSize(130, 0))
|
||||||
|
self.cancel.setAutoDefault(False)
|
||||||
|
self.cancel.setObjectName("cancel")
|
||||||
|
self.horizontalLayout_12.addWidget(self.cancel)
|
||||||
|
self.gridLayout.addLayout(self.horizontalLayout_12, 12, 0, 1, 3)
|
||||||
|
|
||||||
|
self.retranslateUi(CommandEditDialog)
|
||||||
|
QtCore.QMetaObject.connectSlotsByName(CommandEditDialog)
|
||||||
|
|
||||||
|
def retranslateUi(self, CommandEditDialog):
|
||||||
|
_translate = QtCore.QCoreApplication.translate
|
||||||
|
CommandEditDialog.setWindowTitle(_translate("CommandEditDialog", "Command Edit Dialog"))
|
||||||
|
self.label_2.setText(_translate("CommandEditDialog", "When this command excutes, do the following sequence:"))
|
||||||
|
self.label.setText(_translate("CommandEditDialog", "When I say :"))
|
||||||
|
self.oneExe.setText(_translate("CommandEditDialog", "This command executes once"))
|
||||||
|
self.continueExe.setText(_translate("CommandEditDialog", "This command repeats continuously"))
|
||||||
|
self.repeatExe.setText(_translate("CommandEditDialog", "This command repeats"))
|
||||||
|
self.label_3.setText(_translate("CommandEditDialog", "times"))
|
||||||
|
self.keyBut.setText(_translate("CommandEditDialog", "Key Press"))
|
||||||
|
self.mouseBut.setText(_translate("CommandEditDialog", "Mouse"))
|
||||||
|
self.pauseBut.setText(_translate("CommandEditDialog", "Pause"))
|
||||||
|
self.otherBut.setText(_translate("CommandEditDialog", "Other"))
|
||||||
|
self.upBut.setText(_translate("CommandEditDialog", "Up"))
|
||||||
|
self.downBut.setText(_translate("CommandEditDialog", "Down"))
|
||||||
|
self.editBut.setText(_translate("CommandEditDialog", "Edit"))
|
||||||
|
self.deleteBut.setText(_translate("CommandEditDialog", "Delete"))
|
||||||
|
self.asyncChk.setText(_translate("CommandEditDialog", "Allow other commands to execute while this one is running"))
|
||||||
|
self.ok.setText(_translate("CommandEditDialog", "OK"))
|
||||||
|
self.cancel.setText(_translate("CommandEditDialog", "Cancel"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
app = QtWidgets.QApplication(sys.argv)
|
||||||
|
CommandEditDialog = QtWidgets.QDialog()
|
||||||
|
ui = Ui_CommandEditDialog()
|
||||||
|
ui.setupUi(CommandEditDialog)
|
||||||
|
CommandEditDialog.show()
|
||||||
|
sys.exit(app.exec_())
|
|
@ -0,0 +1,107 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Form implementation generated from reading ui file '.\keyactioneditwnd.ui'
|
||||||
|
#
|
||||||
|
# Created by: PyQt5 UI code generator 5.12.1
|
||||||
|
#
|
||||||
|
# WARNING! All changes made in this file will be lost!
|
||||||
|
|
||||||
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
|
|
||||||
|
class Ui_KeyActionEditDialog(object):
|
||||||
|
def setupUi(self, KeyActionEditDialog):
|
||||||
|
KeyActionEditDialog.setObjectName("KeyActionEditDialog")
|
||||||
|
KeyActionEditDialog.resize(471, 164)
|
||||||
|
self.gridLayout = QtWidgets.QGridLayout(KeyActionEditDialog)
|
||||||
|
self.gridLayout.setObjectName("gridLayout")
|
||||||
|
self.label = QtWidgets.QLabel(KeyActionEditDialog)
|
||||||
|
self.label.setObjectName("label")
|
||||||
|
self.gridLayout.addWidget(self.label, 0, 0, 1, 1)
|
||||||
|
spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||||
|
self.gridLayout.addItem(spacerItem, 2, 0, 1, 1)
|
||||||
|
spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||||
|
self.gridLayout.addItem(spacerItem1, 4, 0, 1, 1)
|
||||||
|
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout_2.setSpacing(20)
|
||||||
|
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
|
||||||
|
self.press_releaseKey = QtWidgets.QRadioButton(KeyActionEditDialog)
|
||||||
|
self.press_releaseKey.setObjectName("press_releaseKey")
|
||||||
|
self.horizontalLayout_2.addWidget(self.press_releaseKey)
|
||||||
|
self.pressKey = QtWidgets.QRadioButton(KeyActionEditDialog)
|
||||||
|
self.pressKey.setObjectName("pressKey")
|
||||||
|
self.horizontalLayout_2.addWidget(self.pressKey)
|
||||||
|
self.releaseKey = QtWidgets.QRadioButton(KeyActionEditDialog)
|
||||||
|
self.releaseKey.setObjectName("releaseKey")
|
||||||
|
self.horizontalLayout_2.addWidget(self.releaseKey)
|
||||||
|
self.gridLayout.addLayout(self.horizontalLayout_2, 3, 0, 1, 1)
|
||||||
|
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
|
||||||
|
spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||||
|
self.horizontalLayout_3.addItem(spacerItem2)
|
||||||
|
self.ok = QtWidgets.QPushButton(KeyActionEditDialog)
|
||||||
|
self.ok.setAutoDefault(False)
|
||||||
|
self.ok.setObjectName("ok")
|
||||||
|
self.horizontalLayout_3.addWidget(self.ok)
|
||||||
|
self.cancel = QtWidgets.QPushButton(KeyActionEditDialog)
|
||||||
|
self.cancel.setAutoDefault(False)
|
||||||
|
self.cancel.setObjectName("cancel")
|
||||||
|
self.horizontalLayout_3.addWidget(self.cancel)
|
||||||
|
self.gridLayout.addLayout(self.horizontalLayout_3, 5, 0, 1, 1)
|
||||||
|
self.horizontalLayout = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||||
|
self.ctrlBut = QtWidgets.QPushButton(KeyActionEditDialog)
|
||||||
|
font = QtGui.QFont()
|
||||||
|
font.setBold(False)
|
||||||
|
font.setWeight(50)
|
||||||
|
font.setStrikeOut(False)
|
||||||
|
self.ctrlBut.setFont(font)
|
||||||
|
self.ctrlBut.setAutoDefault(False)
|
||||||
|
self.ctrlBut.setObjectName("ctrlBut")
|
||||||
|
self.horizontalLayout.addWidget(self.ctrlBut)
|
||||||
|
self.altBut = QtWidgets.QPushButton(KeyActionEditDialog)
|
||||||
|
self.altBut.setAutoDefault(False)
|
||||||
|
self.altBut.setObjectName("altBut")
|
||||||
|
self.horizontalLayout.addWidget(self.altBut)
|
||||||
|
self.shiftBut = QtWidgets.QPushButton(KeyActionEditDialog)
|
||||||
|
self.shiftBut.setAutoDefault(False)
|
||||||
|
self.shiftBut.setObjectName("shiftBut")
|
||||||
|
self.horizontalLayout.addWidget(self.shiftBut)
|
||||||
|
self.winBut = QtWidgets.QPushButton(KeyActionEditDialog)
|
||||||
|
self.winBut.setAutoDefault(False)
|
||||||
|
self.winBut.setObjectName("winBut")
|
||||||
|
self.horizontalLayout.addWidget(self.winBut)
|
||||||
|
self.keyEdit = QtWidgets.QLineEdit(KeyActionEditDialog)
|
||||||
|
self.keyEdit.setAlignment(QtCore.Qt.AlignCenter)
|
||||||
|
self.keyEdit.setObjectName("keyEdit")
|
||||||
|
self.horizontalLayout.addWidget(self.keyEdit)
|
||||||
|
self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 1)
|
||||||
|
|
||||||
|
self.retranslateUi(KeyActionEditDialog)
|
||||||
|
QtCore.QMetaObject.connectSlotsByName(KeyActionEditDialog)
|
||||||
|
|
||||||
|
def retranslateUi(self, KeyActionEditDialog):
|
||||||
|
_translate = QtCore.QCoreApplication.translate
|
||||||
|
KeyActionEditDialog.setWindowTitle(_translate("KeyActionEditDialog", "Key Action Dialog"))
|
||||||
|
self.label.setText(_translate("KeyActionEditDialog", "Key (Combination):"))
|
||||||
|
self.press_releaseKey.setText(_translate("KeyActionEditDialog", "Press and Release Key(s)"))
|
||||||
|
self.pressKey.setText(_translate("KeyActionEditDialog", "Press Key(s)"))
|
||||||
|
self.releaseKey.setText(_translate("KeyActionEditDialog", "Release Key(s)"))
|
||||||
|
self.ok.setText(_translate("KeyActionEditDialog", "OK"))
|
||||||
|
self.cancel.setText(_translate("KeyActionEditDialog", "Cancel"))
|
||||||
|
self.ctrlBut.setText(_translate("KeyActionEditDialog", "Ctrl"))
|
||||||
|
self.altBut.setText(_translate("KeyActionEditDialog", "Alt"))
|
||||||
|
self.shiftBut.setText(_translate("KeyActionEditDialog", "Shift"))
|
||||||
|
self.winBut.setText(_translate("KeyActionEditDialog", "Win"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
app = QtWidgets.QApplication(sys.argv)
|
||||||
|
KeyActionEditDialog = QtWidgets.QDialog()
|
||||||
|
ui = Ui_KeyActionEditDialog()
|
||||||
|
ui.setupUi(KeyActionEditDialog)
|
||||||
|
KeyActionEditDialog.show()
|
||||||
|
sys.exit(app.exec_())
|
|
@ -0,0 +1,91 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Form implementation generated from reading ui file 'mainwnd.ui'
|
||||||
|
#
|
||||||
|
# Created by: PyQt5 UI code generator 5.12.1
|
||||||
|
#
|
||||||
|
# WARNING! All changes made in this file will be lost!
|
||||||
|
|
||||||
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
|
|
||||||
|
class Ui_MainWidget(object):
|
||||||
|
def setupUi(self, MainWidget):
|
||||||
|
MainWidget.setObjectName("MainWidget")
|
||||||
|
MainWidget.resize(820, 166)
|
||||||
|
font = QtGui.QFont()
|
||||||
|
font.setPointSize(10)
|
||||||
|
MainWidget.setFont(font)
|
||||||
|
self.gridLayout_2 = QtWidgets.QGridLayout(MainWidget)
|
||||||
|
self.gridLayout_2.setContentsMargins(-1, 20, -1, -1)
|
||||||
|
self.gridLayout_2.setVerticalSpacing(20)
|
||||||
|
self.gridLayout_2.setObjectName("gridLayout_2")
|
||||||
|
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout_2.setSpacing(20)
|
||||||
|
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
|
||||||
|
self.listeningChk = QtWidgets.QCheckBox(MainWidget)
|
||||||
|
self.listeningChk.setObjectName("listeningChk")
|
||||||
|
self.horizontalLayout_2.addWidget(self.listeningChk)
|
||||||
|
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||||
|
self.horizontalLayout_2.addItem(spacerItem)
|
||||||
|
self.ok = QtWidgets.QPushButton(MainWidget)
|
||||||
|
self.ok.setMinimumSize(QtCore.QSize(130, 0))
|
||||||
|
self.ok.setObjectName("ok")
|
||||||
|
self.horizontalLayout_2.addWidget(self.ok)
|
||||||
|
self.cancel = QtWidgets.QPushButton(MainWidget)
|
||||||
|
self.cancel.setMinimumSize(QtCore.QSize(130, 0))
|
||||||
|
self.cancel.setObjectName("cancel")
|
||||||
|
self.horizontalLayout_2.addWidget(self.cancel)
|
||||||
|
self.gridLayout_2.addLayout(self.horizontalLayout_2, 1, 0, 1, 1)
|
||||||
|
self.horizontalLayout = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout.setSpacing(20)
|
||||||
|
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||||
|
self.label = QtWidgets.QLabel(MainWidget)
|
||||||
|
self.label.setMinimumSize(QtCore.QSize(60, 0))
|
||||||
|
self.label.setObjectName("label")
|
||||||
|
self.horizontalLayout.addWidget(self.label)
|
||||||
|
self.profileCbx = QtWidgets.QComboBox(MainWidget)
|
||||||
|
self.profileCbx.setMinimumSize(QtCore.QSize(250, 0))
|
||||||
|
self.profileCbx.setObjectName("profileCbx")
|
||||||
|
self.horizontalLayout.addWidget(self.profileCbx)
|
||||||
|
self.addBut = QtWidgets.QPushButton(MainWidget)
|
||||||
|
self.addBut.setMinimumSize(QtCore.QSize(130, 0))
|
||||||
|
self.addBut.setObjectName("addBut")
|
||||||
|
self.horizontalLayout.addWidget(self.addBut)
|
||||||
|
self.editBut = QtWidgets.QPushButton(MainWidget)
|
||||||
|
self.editBut.setMinimumSize(QtCore.QSize(130, 0))
|
||||||
|
self.editBut.setObjectName("editBut")
|
||||||
|
self.horizontalLayout.addWidget(self.editBut)
|
||||||
|
self.removeBut = QtWidgets.QPushButton(MainWidget)
|
||||||
|
self.removeBut.setMinimumSize(QtCore.QSize(130, 0))
|
||||||
|
self.removeBut.setObjectName("removeBut")
|
||||||
|
self.horizontalLayout.addWidget(self.removeBut)
|
||||||
|
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||||
|
self.horizontalLayout.addItem(spacerItem1)
|
||||||
|
self.gridLayout_2.addLayout(self.horizontalLayout, 0, 0, 1, 1)
|
||||||
|
|
||||||
|
self.retranslateUi(MainWidget)
|
||||||
|
QtCore.QMetaObject.connectSlotsByName(MainWidget)
|
||||||
|
|
||||||
|
def retranslateUi(self, MainWidget):
|
||||||
|
_translate = QtCore.QCoreApplication.translate
|
||||||
|
MainWidget.setWindowTitle(_translate("MainWidget", "LinVAM"))
|
||||||
|
self.listeningChk.setText(_translate("MainWidget", "Enable Listening"))
|
||||||
|
self.ok.setText(_translate("MainWidget", "OK"))
|
||||||
|
self.cancel.setText(_translate("MainWidget", "Cancel"))
|
||||||
|
self.label.setText(_translate("MainWidget", "Profile:"))
|
||||||
|
self.addBut.setText(_translate("MainWidget", "Add"))
|
||||||
|
self.editBut.setText(_translate("MainWidget", "Edit"))
|
||||||
|
self.removeBut.setText(_translate("MainWidget", "Remove"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
app = QtWidgets.QApplication(sys.argv)
|
||||||
|
MainWidget = QtWidgets.QWidget()
|
||||||
|
ui = Ui_MainWidget()
|
||||||
|
ui.setupUi(MainWidget)
|
||||||
|
MainWidget.show()
|
||||||
|
sys.exit(app.exec_())
|
|
@ -0,0 +1,218 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Form implementation generated from reading ui file 'mouseactioneditwnd.ui'
|
||||||
|
#
|
||||||
|
# Created by: PyQt5 UI code generator 5.12.1
|
||||||
|
#
|
||||||
|
# WARNING! All changes made in this file will be lost!
|
||||||
|
|
||||||
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
|
|
||||||
|
class Ui_MouseActionEditDialog(object):
|
||||||
|
def setupUi(self, MouseActionEditDialog):
|
||||||
|
MouseActionEditDialog.setObjectName("MouseActionEditDialog")
|
||||||
|
MouseActionEditDialog.resize(651, 268)
|
||||||
|
font = QtGui.QFont()
|
||||||
|
font.setPointSize(10)
|
||||||
|
MouseActionEditDialog.setFont(font)
|
||||||
|
self.gridLayout = QtWidgets.QGridLayout(MouseActionEditDialog)
|
||||||
|
self.gridLayout.setVerticalSpacing(20)
|
||||||
|
self.gridLayout.setObjectName("gridLayout")
|
||||||
|
self.mouseActionTabWidget = QtWidgets.QTabWidget(MouseActionEditDialog)
|
||||||
|
self.mouseActionTabWidget.setObjectName("mouseActionTabWidget")
|
||||||
|
self.clickActionTab = QtWidgets.QWidget()
|
||||||
|
self.clickActionTab.setObjectName("clickActionTab")
|
||||||
|
self.gridLayout_2 = QtWidgets.QGridLayout(self.clickActionTab)
|
||||||
|
self.gridLayout_2.setContentsMargins(-1, 20, -1, 9)
|
||||||
|
self.gridLayout_2.setObjectName("gridLayout_2")
|
||||||
|
self.leftClick = QtWidgets.QRadioButton(self.clickActionTab)
|
||||||
|
self.leftClick.setObjectName("leftClick")
|
||||||
|
self.gridLayout_2.addWidget(self.leftClick, 0, 0, 1, 1)
|
||||||
|
self.rightClick = QtWidgets.QRadioButton(self.clickActionTab)
|
||||||
|
self.rightClick.setObjectName("rightClick")
|
||||||
|
self.gridLayout_2.addWidget(self.rightClick, 0, 1, 1, 1)
|
||||||
|
self.middleClick = QtWidgets.QRadioButton(self.clickActionTab)
|
||||||
|
self.middleClick.setObjectName("middleClick")
|
||||||
|
self.gridLayout_2.addWidget(self.middleClick, 0, 2, 1, 1)
|
||||||
|
self.leftDclick = QtWidgets.QRadioButton(self.clickActionTab)
|
||||||
|
self.leftDclick.setObjectName("leftDclick")
|
||||||
|
self.gridLayout_2.addWidget(self.leftDclick, 1, 0, 1, 1)
|
||||||
|
self.rightDclick = QtWidgets.QRadioButton(self.clickActionTab)
|
||||||
|
self.rightDclick.setObjectName("rightDclick")
|
||||||
|
self.gridLayout_2.addWidget(self.rightDclick, 1, 1, 1, 1)
|
||||||
|
self.middleDclick = QtWidgets.QRadioButton(self.clickActionTab)
|
||||||
|
self.middleDclick.setObjectName("middleDclick")
|
||||||
|
self.gridLayout_2.addWidget(self.middleDclick, 1, 2, 1, 1)
|
||||||
|
self.leftDown = QtWidgets.QRadioButton(self.clickActionTab)
|
||||||
|
self.leftDown.setObjectName("leftDown")
|
||||||
|
self.gridLayout_2.addWidget(self.leftDown, 2, 0, 1, 1)
|
||||||
|
self.rightDown = QtWidgets.QRadioButton(self.clickActionTab)
|
||||||
|
self.rightDown.setObjectName("rightDown")
|
||||||
|
self.gridLayout_2.addWidget(self.rightDown, 2, 1, 1, 1)
|
||||||
|
self.middleDown = QtWidgets.QRadioButton(self.clickActionTab)
|
||||||
|
self.middleDown.setObjectName("middleDown")
|
||||||
|
self.gridLayout_2.addWidget(self.middleDown, 2, 2, 1, 1)
|
||||||
|
self.leftUp = QtWidgets.QRadioButton(self.clickActionTab)
|
||||||
|
self.leftUp.setObjectName("leftUp")
|
||||||
|
self.gridLayout_2.addWidget(self.leftUp, 3, 0, 1, 1)
|
||||||
|
self.rightUp = QtWidgets.QRadioButton(self.clickActionTab)
|
||||||
|
self.rightUp.setObjectName("rightUp")
|
||||||
|
self.gridLayout_2.addWidget(self.rightUp, 3, 1, 1, 1)
|
||||||
|
self.middleUp = QtWidgets.QRadioButton(self.clickActionTab)
|
||||||
|
self.middleUp.setObjectName("middleUp")
|
||||||
|
self.gridLayout_2.addWidget(self.middleUp, 3, 2, 1, 1)
|
||||||
|
self.mouseActionTabWidget.addTab(self.clickActionTab, "")
|
||||||
|
self.moveActionTab = QtWidgets.QWidget()
|
||||||
|
self.moveActionTab.setObjectName("moveActionTab")
|
||||||
|
self.gridLayout_3 = QtWidgets.QGridLayout(self.moveActionTab)
|
||||||
|
self.gridLayout_3.setContentsMargins(-1, 20, -1, -1)
|
||||||
|
self.gridLayout_3.setObjectName("gridLayout_3")
|
||||||
|
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
|
||||||
|
self.moveTo = QtWidgets.QRadioButton(self.moveActionTab)
|
||||||
|
self.moveTo.setMinimumSize(QtCore.QSize(100, 0))
|
||||||
|
self.moveTo.setObjectName("moveTo")
|
||||||
|
self.horizontalLayout_2.addWidget(self.moveTo)
|
||||||
|
self.label_2 = QtWidgets.QLabel(self.moveActionTab)
|
||||||
|
self.label_2.setMinimumSize(QtCore.QSize(25, 0))
|
||||||
|
self.label_2.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
|
||||||
|
self.label_2.setObjectName("label_2")
|
||||||
|
self.horizontalLayout_2.addWidget(self.label_2)
|
||||||
|
self.xEdit = QtWidgets.QLineEdit(self.moveActionTab)
|
||||||
|
self.xEdit.setObjectName("xEdit")
|
||||||
|
self.horizontalLayout_2.addWidget(self.xEdit)
|
||||||
|
self.label_3 = QtWidgets.QLabel(self.moveActionTab)
|
||||||
|
self.label_3.setMinimumSize(QtCore.QSize(25, 0))
|
||||||
|
self.label_3.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
|
||||||
|
self.label_3.setObjectName("label_3")
|
||||||
|
self.horizontalLayout_2.addWidget(self.label_3)
|
||||||
|
self.yEdit = QtWidgets.QLineEdit(self.moveActionTab)
|
||||||
|
self.yEdit.setObjectName("yEdit")
|
||||||
|
self.horizontalLayout_2.addWidget(self.yEdit)
|
||||||
|
self.gridLayout_3.addLayout(self.horizontalLayout_2, 1, 0, 1, 3)
|
||||||
|
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
|
||||||
|
self.moveOffset = QtWidgets.QRadioButton(self.moveActionTab)
|
||||||
|
self.moveOffset.setMinimumSize(QtCore.QSize(100, 0))
|
||||||
|
self.moveOffset.setAutoExclusive(True)
|
||||||
|
self.moveOffset.setObjectName("moveOffset")
|
||||||
|
self.horizontalLayout_3.addWidget(self.moveOffset)
|
||||||
|
self.label_5 = QtWidgets.QLabel(self.moveActionTab)
|
||||||
|
self.label_5.setMinimumSize(QtCore.QSize(25, 0))
|
||||||
|
self.label_5.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
|
||||||
|
self.label_5.setObjectName("label_5")
|
||||||
|
self.horizontalLayout_3.addWidget(self.label_5)
|
||||||
|
self.xOffsetEdit = QtWidgets.QLineEdit(self.moveActionTab)
|
||||||
|
self.xOffsetEdit.setObjectName("xOffsetEdit")
|
||||||
|
self.horizontalLayout_3.addWidget(self.xOffsetEdit)
|
||||||
|
self.label_4 = QtWidgets.QLabel(self.moveActionTab)
|
||||||
|
self.label_4.setMinimumSize(QtCore.QSize(70, 0))
|
||||||
|
self.label_4.setObjectName("label_4")
|
||||||
|
self.horizontalLayout_3.addWidget(self.label_4)
|
||||||
|
self.label_6 = QtWidgets.QLabel(self.moveActionTab)
|
||||||
|
self.label_6.setMinimumSize(QtCore.QSize(25, 0))
|
||||||
|
self.label_6.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
|
||||||
|
self.label_6.setObjectName("label_6")
|
||||||
|
self.horizontalLayout_3.addWidget(self.label_6)
|
||||||
|
self.yOffsetEdit = QtWidgets.QLineEdit(self.moveActionTab)
|
||||||
|
self.yOffsetEdit.setObjectName("yOffsetEdit")
|
||||||
|
self.horizontalLayout_3.addWidget(self.yOffsetEdit)
|
||||||
|
self.label_7 = QtWidgets.QLabel(self.moveActionTab)
|
||||||
|
self.label_7.setMinimumSize(QtCore.QSize(70, 0))
|
||||||
|
self.label_7.setObjectName("label_7")
|
||||||
|
self.horizontalLayout_3.addWidget(self.label_7)
|
||||||
|
self.gridLayout_3.addLayout(self.horizontalLayout_3, 2, 0, 1, 6)
|
||||||
|
self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout_4.setObjectName("horizontalLayout_4")
|
||||||
|
self.scrollUp = QtWidgets.QRadioButton(self.moveActionTab)
|
||||||
|
self.scrollUp.setMinimumSize(QtCore.QSize(100, 0))
|
||||||
|
self.scrollUp.setAutoExclusive(True)
|
||||||
|
self.scrollUp.setObjectName("scrollUp")
|
||||||
|
self.horizontalLayout_4.addWidget(self.scrollUp)
|
||||||
|
self.scrollUpEdit = QtWidgets.QLineEdit(self.moveActionTab)
|
||||||
|
self.scrollUpEdit.setObjectName("scrollUpEdit")
|
||||||
|
self.horizontalLayout_4.addWidget(self.scrollUpEdit)
|
||||||
|
self.label_8 = QtWidgets.QLabel(self.moveActionTab)
|
||||||
|
self.label_8.setMinimumSize(QtCore.QSize(80, 0))
|
||||||
|
self.label_8.setObjectName("label_8")
|
||||||
|
self.horizontalLayout_4.addWidget(self.label_8)
|
||||||
|
self.scrollDown = QtWidgets.QRadioButton(self.moveActionTab)
|
||||||
|
self.scrollDown.setMinimumSize(QtCore.QSize(110, 0))
|
||||||
|
self.scrollDown.setAutoExclusive(True)
|
||||||
|
self.scrollDown.setObjectName("scrollDown")
|
||||||
|
self.horizontalLayout_4.addWidget(self.scrollDown)
|
||||||
|
self.scrollDownEdit = QtWidgets.QLineEdit(self.moveActionTab)
|
||||||
|
self.scrollDownEdit.setObjectName("scrollDownEdit")
|
||||||
|
self.horizontalLayout_4.addWidget(self.scrollDownEdit)
|
||||||
|
self.label_9 = QtWidgets.QLabel(self.moveActionTab)
|
||||||
|
self.label_9.setMinimumSize(QtCore.QSize(80, 0))
|
||||||
|
self.label_9.setObjectName("label_9")
|
||||||
|
self.horizontalLayout_4.addWidget(self.label_9)
|
||||||
|
self.gridLayout_3.addLayout(self.horizontalLayout_4, 3, 0, 1, 1)
|
||||||
|
self.mouseActionTabWidget.addTab(self.moveActionTab, "")
|
||||||
|
self.gridLayout.addWidget(self.mouseActionTabWidget, 0, 0, 1, 1)
|
||||||
|
self.horizontalLayout = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout.setSpacing(20)
|
||||||
|
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||||
|
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||||
|
self.horizontalLayout.addItem(spacerItem)
|
||||||
|
self.ok = QtWidgets.QPushButton(MouseActionEditDialog)
|
||||||
|
self.ok.setMinimumSize(QtCore.QSize(130, 0))
|
||||||
|
self.ok.setAutoDefault(False)
|
||||||
|
self.ok.setObjectName("ok")
|
||||||
|
self.horizontalLayout.addWidget(self.ok)
|
||||||
|
self.cancel = QtWidgets.QPushButton(MouseActionEditDialog)
|
||||||
|
self.cancel.setMinimumSize(QtCore.QSize(130, 0))
|
||||||
|
self.cancel.setAutoDefault(False)
|
||||||
|
self.cancel.setObjectName("cancel")
|
||||||
|
self.horizontalLayout.addWidget(self.cancel)
|
||||||
|
self.gridLayout.addLayout(self.horizontalLayout, 3, 0, 1, 1)
|
||||||
|
|
||||||
|
self.retranslateUi(MouseActionEditDialog)
|
||||||
|
self.mouseActionTabWidget.setCurrentIndex(0)
|
||||||
|
QtCore.QMetaObject.connectSlotsByName(MouseActionEditDialog)
|
||||||
|
|
||||||
|
def retranslateUi(self, MouseActionEditDialog):
|
||||||
|
_translate = QtCore.QCoreApplication.translate
|
||||||
|
MouseActionEditDialog.setWindowTitle(_translate("MouseActionEditDialog", "Dialog"))
|
||||||
|
self.leftClick.setText(_translate("MouseActionEditDialog", "Click left button"))
|
||||||
|
self.rightClick.setText(_translate("MouseActionEditDialog", "Click right button"))
|
||||||
|
self.middleClick.setText(_translate("MouseActionEditDialog", "Click middle button"))
|
||||||
|
self.leftDclick.setText(_translate("MouseActionEditDialog", "Double-click left button"))
|
||||||
|
self.rightDclick.setText(_translate("MouseActionEditDialog", "Double-click right button"))
|
||||||
|
self.middleDclick.setText(_translate("MouseActionEditDialog", "Double-click middle button"))
|
||||||
|
self.leftDown.setText(_translate("MouseActionEditDialog", "left button down"))
|
||||||
|
self.rightDown.setText(_translate("MouseActionEditDialog", "right button down"))
|
||||||
|
self.middleDown.setText(_translate("MouseActionEditDialog", "middle button down"))
|
||||||
|
self.leftUp.setText(_translate("MouseActionEditDialog", "left button up"))
|
||||||
|
self.rightUp.setText(_translate("MouseActionEditDialog", "right button up"))
|
||||||
|
self.middleUp.setText(_translate("MouseActionEditDialog", "middle button up"))
|
||||||
|
self.mouseActionTabWidget.setTabText(self.mouseActionTabWidget.indexOf(self.clickActionTab), _translate("MouseActionEditDialog", "Click"))
|
||||||
|
self.moveTo.setText(_translate("MouseActionEditDialog", "Move to:"))
|
||||||
|
self.label_2.setText(_translate("MouseActionEditDialog", "X"))
|
||||||
|
self.label_3.setText(_translate("MouseActionEditDialog", "Y"))
|
||||||
|
self.moveOffset.setText(_translate("MouseActionEditDialog", "Move offset"))
|
||||||
|
self.label_5.setText(_translate("MouseActionEditDialog", "X"))
|
||||||
|
self.label_4.setText(_translate("MouseActionEditDialog", "pixels"))
|
||||||
|
self.label_6.setText(_translate("MouseActionEditDialog", "Y"))
|
||||||
|
self.label_7.setText(_translate("MouseActionEditDialog", "pixels"))
|
||||||
|
self.scrollUp.setText(_translate("MouseActionEditDialog", "Scroll up"))
|
||||||
|
self.label_8.setText(_translate("MouseActionEditDialog", "pixels"))
|
||||||
|
self.scrollDown.setText(_translate("MouseActionEditDialog", "Scroll down"))
|
||||||
|
self.label_9.setText(_translate("MouseActionEditDialog", "pixels"))
|
||||||
|
self.mouseActionTabWidget.setTabText(self.mouseActionTabWidget.indexOf(self.moveActionTab), _translate("MouseActionEditDialog", "Move"))
|
||||||
|
self.ok.setText(_translate("MouseActionEditDialog", "OK"))
|
||||||
|
self.cancel.setText(_translate("MouseActionEditDialog", "Cancel"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
app = QtWidgets.QApplication(sys.argv)
|
||||||
|
MouseActionEditDialog = QtWidgets.QDialog()
|
||||||
|
ui = Ui_MouseActionEditDialog()
|
||||||
|
ui.setupUi(MouseActionEditDialog)
|
||||||
|
MouseActionEditDialog.show()
|
||||||
|
sys.exit(app.exec_())
|
|
@ -0,0 +1,69 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Form implementation generated from reading ui file 'pauseactioneditwnd.ui'
|
||||||
|
#
|
||||||
|
# Created by: PyQt5 UI code generator 5.12.1
|
||||||
|
#
|
||||||
|
# WARNING! All changes made in this file will be lost!
|
||||||
|
|
||||||
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
|
|
||||||
|
class Ui_PauseActionEditDialog(object):
|
||||||
|
def setupUi(self, PauseActionEditDialog):
|
||||||
|
PauseActionEditDialog.setObjectName("PauseActionEditDialog")
|
||||||
|
PauseActionEditDialog.resize(271, 133)
|
||||||
|
font = QtGui.QFont()
|
||||||
|
font.setPointSize(10)
|
||||||
|
PauseActionEditDialog.setFont(font)
|
||||||
|
self.gridLayout = QtWidgets.QGridLayout(PauseActionEditDialog)
|
||||||
|
self.gridLayout.setObjectName("gridLayout")
|
||||||
|
self.horizontalLayout = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||||
|
self.label = QtWidgets.QLabel(PauseActionEditDialog)
|
||||||
|
self.label.setObjectName("label")
|
||||||
|
self.horizontalLayout.addWidget(self.label)
|
||||||
|
self.secondEdit = QtWidgets.QLineEdit(PauseActionEditDialog)
|
||||||
|
self.secondEdit.setObjectName("secondEdit")
|
||||||
|
self.horizontalLayout.addWidget(self.secondEdit)
|
||||||
|
self.label_2 = QtWidgets.QLabel(PauseActionEditDialog)
|
||||||
|
self.label_2.setObjectName("label_2")
|
||||||
|
self.horizontalLayout.addWidget(self.label_2)
|
||||||
|
self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1)
|
||||||
|
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout_2.setSpacing(20)
|
||||||
|
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
|
||||||
|
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||||
|
self.horizontalLayout_2.addItem(spacerItem)
|
||||||
|
self.ok = QtWidgets.QPushButton(PauseActionEditDialog)
|
||||||
|
self.ok.setMinimumSize(QtCore.QSize(100, 0))
|
||||||
|
self.ok.setObjectName("ok")
|
||||||
|
self.horizontalLayout_2.addWidget(self.ok)
|
||||||
|
self.cancel = QtWidgets.QPushButton(PauseActionEditDialog)
|
||||||
|
self.cancel.setMinimumSize(QtCore.QSize(100, 0))
|
||||||
|
self.cancel.setObjectName("cancel")
|
||||||
|
self.horizontalLayout_2.addWidget(self.cancel)
|
||||||
|
self.gridLayout.addLayout(self.horizontalLayout_2, 1, 0, 1, 1)
|
||||||
|
|
||||||
|
self.retranslateUi(PauseActionEditDialog)
|
||||||
|
QtCore.QMetaObject.connectSlotsByName(PauseActionEditDialog)
|
||||||
|
|
||||||
|
def retranslateUi(self, PauseActionEditDialog):
|
||||||
|
_translate = QtCore.QCoreApplication.translate
|
||||||
|
PauseActionEditDialog.setWindowTitle(_translate("PauseActionEditDialog", "Pause Action Edit Dialog"))
|
||||||
|
self.label.setText(_translate("PauseActionEditDialog", "Pause for "))
|
||||||
|
self.label_2.setText(_translate("PauseActionEditDialog", "seconds"))
|
||||||
|
self.ok.setText(_translate("PauseActionEditDialog", "OK"))
|
||||||
|
self.cancel.setText(_translate("PauseActionEditDialog", "Cancel"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
app = QtWidgets.QApplication(sys.argv)
|
||||||
|
PauseActionEditDialog = QtWidgets.QDialog()
|
||||||
|
ui = Ui_PauseActionEditDialog()
|
||||||
|
ui.setupUi(PauseActionEditDialog)
|
||||||
|
PauseActionEditDialog.show()
|
||||||
|
sys.exit(app.exec_())
|
|
@ -0,0 +1,109 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Form implementation generated from reading ui file 'profileeditwnd.ui'
|
||||||
|
#
|
||||||
|
# Created by: PyQt5 UI code generator 5.12.1
|
||||||
|
#
|
||||||
|
# WARNING! All changes made in this file will be lost!
|
||||||
|
|
||||||
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
|
|
||||||
|
class Ui_ProfileEditDialog(object):
|
||||||
|
def setupUi(self, ProfileEditDialog):
|
||||||
|
ProfileEditDialog.setObjectName("ProfileEditDialog")
|
||||||
|
ProfileEditDialog.resize(1191, 591)
|
||||||
|
self.gridLayout_2 = QtWidgets.QGridLayout(ProfileEditDialog)
|
||||||
|
self.gridLayout_2.setObjectName("gridLayout_2")
|
||||||
|
self.horizontalLayout = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||||
|
self.label = QtWidgets.QLabel(ProfileEditDialog)
|
||||||
|
self.label.setMinimumSize(QtCore.QSize(60, 0))
|
||||||
|
self.label.setObjectName("label")
|
||||||
|
self.horizontalLayout.addWidget(self.label)
|
||||||
|
self.profileNameEdit = QtWidgets.QLineEdit(ProfileEditDialog)
|
||||||
|
self.profileNameEdit.setObjectName("profileNameEdit")
|
||||||
|
self.horizontalLayout.addWidget(self.profileNameEdit)
|
||||||
|
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||||
|
self.horizontalLayout.addItem(spacerItem)
|
||||||
|
self.gridLayout_2.addLayout(self.horizontalLayout, 0, 0, 1, 1)
|
||||||
|
self.groupBox = QtWidgets.QGroupBox(ProfileEditDialog)
|
||||||
|
self.groupBox.setObjectName("groupBox")
|
||||||
|
self.gridLayout = QtWidgets.QGridLayout(self.groupBox)
|
||||||
|
self.gridLayout.setObjectName("gridLayout")
|
||||||
|
self.cmdTable = QtWidgets.QTableWidget(self.groupBox)
|
||||||
|
self.cmdTable.setMinimumSize(QtCore.QSize(0, 200))
|
||||||
|
font = QtGui.QFont()
|
||||||
|
font.setPointSize(11)
|
||||||
|
self.cmdTable.setFont(font)
|
||||||
|
self.cmdTable.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||||
|
self.cmdTable.setDragDropOverwriteMode(False)
|
||||||
|
self.cmdTable.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
|
||||||
|
self.cmdTable.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
||||||
|
self.cmdTable.setColumnCount(2)
|
||||||
|
self.cmdTable.setObjectName("cmdTable")
|
||||||
|
self.cmdTable.setRowCount(0)
|
||||||
|
self.cmdTable.horizontalHeader().setDefaultSectionSize(160)
|
||||||
|
self.cmdTable.horizontalHeader().setMinimumSectionSize(160)
|
||||||
|
self.gridLayout.addWidget(self.cmdTable, 0, 0, 1, 1)
|
||||||
|
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout_2.setSpacing(20)
|
||||||
|
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
|
||||||
|
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||||
|
self.horizontalLayout_2.addItem(spacerItem1)
|
||||||
|
spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||||
|
self.horizontalLayout_2.addItem(spacerItem2)
|
||||||
|
self.newCmd = QtWidgets.QPushButton(self.groupBox)
|
||||||
|
self.newCmd.setMinimumSize(QtCore.QSize(130, 0))
|
||||||
|
self.newCmd.setObjectName("newCmd")
|
||||||
|
self.horizontalLayout_2.addWidget(self.newCmd)
|
||||||
|
self.editCmd = QtWidgets.QPushButton(self.groupBox)
|
||||||
|
self.editCmd.setMinimumSize(QtCore.QSize(130, 0))
|
||||||
|
self.editCmd.setObjectName("editCmd")
|
||||||
|
self.horizontalLayout_2.addWidget(self.editCmd)
|
||||||
|
self.deleteCmd = QtWidgets.QPushButton(self.groupBox)
|
||||||
|
self.deleteCmd.setMinimumSize(QtCore.QSize(130, 0))
|
||||||
|
self.deleteCmd.setObjectName("deleteCmd")
|
||||||
|
self.horizontalLayout_2.addWidget(self.deleteCmd)
|
||||||
|
self.gridLayout.addLayout(self.horizontalLayout_2, 1, 0, 1, 1)
|
||||||
|
self.gridLayout_2.addWidget(self.groupBox, 1, 0, 1, 1)
|
||||||
|
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
|
||||||
|
self.horizontalLayout_3.setSpacing(20)
|
||||||
|
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
|
||||||
|
spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||||
|
self.horizontalLayout_3.addItem(spacerItem3)
|
||||||
|
self.ok = QtWidgets.QPushButton(ProfileEditDialog)
|
||||||
|
self.ok.setMinimumSize(QtCore.QSize(130, 0))
|
||||||
|
self.ok.setObjectName("ok")
|
||||||
|
self.horizontalLayout_3.addWidget(self.ok)
|
||||||
|
self.cancel = QtWidgets.QPushButton(ProfileEditDialog)
|
||||||
|
self.cancel.setMinimumSize(QtCore.QSize(130, 0))
|
||||||
|
self.cancel.setObjectName("cancel")
|
||||||
|
self.horizontalLayout_3.addWidget(self.cancel)
|
||||||
|
self.gridLayout_2.addLayout(self.horizontalLayout_3, 2, 0, 1, 1)
|
||||||
|
|
||||||
|
self.retranslateUi(ProfileEditDialog)
|
||||||
|
QtCore.QMetaObject.connectSlotsByName(ProfileEditDialog)
|
||||||
|
|
||||||
|
def retranslateUi(self, ProfileEditDialog):
|
||||||
|
_translate = QtCore.QCoreApplication.translate
|
||||||
|
ProfileEditDialog.setWindowTitle(_translate("ProfileEditDialog", "Dialog"))
|
||||||
|
self.label.setText(_translate("ProfileEditDialog", "Profile:"))
|
||||||
|
self.groupBox.setTitle(_translate("ProfileEditDialog", "Commands"))
|
||||||
|
self.newCmd.setText(_translate("ProfileEditDialog", "New"))
|
||||||
|
self.editCmd.setText(_translate("ProfileEditDialog", "Edit"))
|
||||||
|
self.deleteCmd.setText(_translate("ProfileEditDialog", "Delete"))
|
||||||
|
self.ok.setText(_translate("ProfileEditDialog", "OK"))
|
||||||
|
self.cancel.setText(_translate("ProfileEditDialog", "Cancel"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
app = QtWidgets.QApplication(sys.argv)
|
||||||
|
ProfileEditDialog = QtWidgets.QDialog()
|
||||||
|
ui = Ui_ProfileEditDialog()
|
||||||
|
ui.setupUi(ProfileEditDialog)
|
||||||
|
ProfileEditDialog.show()
|
||||||
|
sys.exit(app.exec_())
|
Loading…
Reference in New Issue