mirror of
https://github.com/aidygus/LinVAM.git
synced 2024-11-26 10:38:06 +11:00
initial commit
This commit is contained in:
parent
cab4cd7eed
commit
821cee0db2
168
commandeditwnd.py
Normal file
168
commandeditwnd.py
Normal file
@ -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()
|
||||||
|
|
390
commandeditwnd.ui
Normal file
390
commandeditwnd.ui
Normal file
@ -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>
|
152
keyactioneditwnd.py
Normal file
152
keyactioneditwnd.py
Normal file
@ -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
|
||||||
|
|
177
keyactioneditwnd.ui
Normal file
177
keyactioneditwnd.ui
Normal file
@ -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>
|
1155
keyboard/__init__.py
Normal file
1155
keyboard/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
13
keyboard/__main__.py
Normal file
13
keyboard/__main__.py
Normal file
@ -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())
|
BIN
keyboard/__pycache__/__init__.cpython-36.pyc
Normal file
BIN
keyboard/__pycache__/__init__.cpython-36.pyc
Normal file
Binary file not shown.
BIN
keyboard/__pycache__/_canonical_names.cpython-36.pyc
Normal file
BIN
keyboard/__pycache__/_canonical_names.cpython-36.pyc
Normal file
Binary file not shown.
BIN
keyboard/__pycache__/_generic.cpython-36.pyc
Normal file
BIN
keyboard/__pycache__/_generic.cpython-36.pyc
Normal file
Binary file not shown.
BIN
keyboard/__pycache__/_keyboard_event.cpython-36.pyc
Normal file
BIN
keyboard/__pycache__/_keyboard_event.cpython-36.pyc
Normal file
Binary file not shown.
BIN
keyboard/__pycache__/_nixcommon.cpython-36.pyc
Normal file
BIN
keyboard/__pycache__/_nixcommon.cpython-36.pyc
Normal file
Binary file not shown.
BIN
keyboard/__pycache__/_nixkeyboard.cpython-36.pyc
Normal file
BIN
keyboard/__pycache__/_nixkeyboard.cpython-36.pyc
Normal file
Binary file not shown.
BIN
keyboard/__pycache__/_winkeyboard.cpython-36.pyc
Normal file
BIN
keyboard/__pycache__/_winkeyboard.cpython-36.pyc
Normal file
Binary file not shown.
1246
keyboard/_canonical_names.py
Normal file
1246
keyboard/_canonical_names.py
Normal file
File diff suppressed because it is too large
Load Diff
442
keyboard/_darwinkeyboard.py
Normal file
442
keyboard/_darwinkeyboard.py
Normal file
@ -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)
|
173
keyboard/_darwinmouse.py
Normal file
173
keyboard/_darwinmouse.py
Normal file
@ -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)
|
73
keyboard/_generic.py
Normal file
73
keyboard/_generic.py
Normal file
@ -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)
|
53
keyboard/_keyboard_event.py
Normal file
53
keyboard/_keyboard_event.py
Normal file
@ -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
|
||||||
|
)
|
||||||
|
)
|
827
keyboard/_keyboard_tests.py
Normal file
827
keyboard/_keyboard_tests.py
Normal file
@ -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()
|
20
keyboard/_mouse_event.py
Normal file
20
keyboard/_mouse_event.py
Normal file
@ -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'])
|
271
keyboard/_mouse_tests.py
Normal file
271
keyboard/_mouse_tests.py
Normal file
@ -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()
|
174
keyboard/_nixcommon.py
Normal file
174
keyboard/_nixcommon.py
Normal file
@ -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.')
|
183
keyboard/_nixkeyboard.py
Normal file
183
keyboard/_nixkeyboard.py
Normal file
@ -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)
|
130
keyboard/_nixmouse.py
Normal file
130
keyboard/_nixmouse.py
Normal file
@ -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)
|
620
keyboard/_winkeyboard.py
Normal file
620
keyboard/_winkeyboard.py
Normal file
@ -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)
|
201
keyboard/_winmouse.py
Normal file
201
keyboard/_winmouse.py
Normal file
@ -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)
|
232
keyboard/mouse.py
Normal file
232
keyboard/mouse.py
Normal file
@ -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())
|
231
main.py
Normal file
231
main.py
Normal file
@ -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_())
|
167
mainwnd.ui
Normal file
167
mainwnd.ui
Normal file
@ -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>
|
117
mouseactioneditwnd.py
Normal file
117
mouseactioneditwnd.py
Normal file
@ -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()
|
402
mouseactioneditwnd.ui
Normal file
402
mouseactioneditwnd.ui
Normal file
@ -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>
|
27
pauseactioneditwnd.py
Normal file
27
pauseactioneditwnd.py
Normal file
@ -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()
|
||||||
|
|
93
pauseactioneditwnd.ui
Normal file
93
pauseactioneditwnd.ui
Normal file
@ -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>
|
221
profileeditwnd.py
Normal file
221
profileeditwnd.py
Normal file
@ -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()
|
219
profileeditwnd.ui
Normal file
219
profileeditwnd.ui
Normal file
@ -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>
|
318
profileexecutor.py
Normal file
318
profileexecutor.py
Normal file
@ -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()
|
BIN
profiles.dat
Normal file
BIN
profiles.dat
Normal file
Binary file not shown.
41
pynput/__init__.py
Normal file
41
pynput/__init__.py
Normal file
@ -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
|
BIN
pynput/__pycache__/__init__.cpython-36.pyc
Normal file
BIN
pynput/__pycache__/__init__.cpython-36.pyc
Normal file
Binary file not shown.
19
pynput/_info.py
Normal file
19
pynput/_info.py
Normal file
@ -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)
|
296
pynput/_util/__init__.py
Normal file
296
pynput/_util/__init__.py
Normal file
@ -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)
|
BIN
pynput/_util/__pycache__/__init__.cpython-36.pyc
Normal file
BIN
pynput/_util/__pycache__/__init__.cpython-36.pyc
Normal file
Binary file not shown.
BIN
pynput/_util/__pycache__/win32.cpython-36.pyc
Normal file
BIN
pynput/_util/__pycache__/win32.cpython-36.pyc
Normal file
Binary file not shown.
BIN
pynput/_util/__pycache__/win32_vks.cpython-36.pyc
Normal file
BIN
pynput/_util/__pycache__/win32_vks.cpython-36.pyc
Normal file
Binary file not shown.
BIN
pynput/_util/__pycache__/xorg.cpython-36.pyc
Normal file
BIN
pynput/_util/__pycache__/xorg.cpython-36.pyc
Normal file
Binary file not shown.
BIN
pynput/_util/__pycache__/xorg_keysyms.cpython-36.pyc
Normal file
BIN
pynput/_util/__pycache__/xorg_keysyms.cpython-36.pyc
Normal file
Binary file not shown.
273
pynput/_util/darwin.py
Normal file
273
pynput/_util/darwin.py
Normal file
@ -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()
|
641
pynput/_util/win32.py
Normal file
641
pynput/_util/win32.py
Normal file
@ -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()
|
179
pynput/_util/win32_vks.py
Normal file
179
pynput/_util/win32_vks.py
Normal file
@ -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
|
480
pynput/_util/xorg.py
Normal file
480
pynput/_util/xorg.py
Normal file
@ -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
|
1715
pynput/_util/xorg_keysyms.py
Normal file
1715
pynput/_util/xorg_keysyms.py
Normal file
File diff suppressed because it is too large
Load Diff
56
pynput/mouse/__init__.py
Normal file
56
pynput/mouse/__init__.py
Normal file
@ -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')
|
BIN
pynput/mouse/__pycache__/__init__.cpython-36.pyc
Normal file
BIN
pynput/mouse/__pycache__/__init__.cpython-36.pyc
Normal file
Binary file not shown.
BIN
pynput/mouse/__pycache__/_base.cpython-36.pyc
Normal file
BIN
pynput/mouse/__pycache__/_base.cpython-36.pyc
Normal file
Binary file not shown.
BIN
pynput/mouse/__pycache__/_win32.cpython-36.pyc
Normal file
BIN
pynput/mouse/__pycache__/_win32.cpython-36.pyc
Normal file
Binary file not shown.
BIN
pynput/mouse/__pycache__/_xorg.cpython-36.pyc
Normal file
BIN
pynput/mouse/__pycache__/_xorg.cpython-36.pyc
Normal file
Binary file not shown.
263
pynput/mouse/_base.py
Normal file
263
pynput/mouse/_base.py
Normal file
@ -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
|
215
pynput/mouse/_darwin.py
Normal file
215
pynput/mouse/_darwin.py
Normal file
@ -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)
|
195
pynput/mouse/_win32.py
Normal file
195
pynput/mouse/_win32.py
Normal file
@ -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)
|
174
pynput/mouse/_xorg.py
Normal file
174
pynput/mouse/_xorg.py
Normal file
@ -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
|
186
ui_commandeditwnd.py
Normal file
186
ui_commandeditwnd.py
Normal file
@ -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_())
|
107
ui_keyactioneditwnd.py
Normal file
107
ui_keyactioneditwnd.py
Normal file
@ -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_())
|
91
ui_mainwnd.py
Normal file
91
ui_mainwnd.py
Normal file
@ -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_())
|
218
ui_mouseactioneditwnd.py
Normal file
218
ui_mouseactioneditwnd.py
Normal file
@ -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_())
|
69
ui_pauseactioneditwnd.py
Normal file
69
ui_pauseactioneditwnd.py
Normal file
@ -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_())
|
109
ui_profileeditwnd.py
Normal file
109
ui_profileeditwnd.py
Normal file
@ -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
Block a user