diff --git a/12-logitech-usb.rules b/12-logitech-usb.rules --- a/12-logitech-usb.rules +++ b/12-logitech-usb.rules @@ -1,11 +1,12 @@ ACTION!="add|remove", GOTO="usb_lt_end" SUBSYSTEMS=="usb", GOTO="usb_lt_check" GOTO="usb_lt_end" LABEL="usb_lt_check" ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c538", MODE="0666" +ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c540", MODE="0666" ATTRS{idVendor}=="0458", ATTRS{idProduct}=="00f1", MODE="0666" ATTRS{idVendor}=="05b8", ATTRS{idProduct}=="3223", MODE="0666" LABEL="usb_lt_end" diff --git a/lt_presentation.py b/lt_presentation.py --- a/lt_presentation.py +++ b/lt_presentation.py @@ -1,385 +1,392 @@ #!/usr/bin/env python # This is lt_presentation # A simple system tray tool to send LogiTech Presenter events # directly to the desired presentation tool. # # It wraps around Atril, xdotool and wmctrl and requires python-evdev # to communicate with the Presenter. # # Xfce's Presentation Mode is automatically activated and reset whenever # a presentation is active. # # (c) 2017 Andreas Böhler # This file is free software; you can redistribute it and/or modify # it under the terms of either the GNU General Public License version 2 # or the GNU Lesser General Public License version 2.1, both as # published by the Free Software Foundation. import dbus import evdev import subprocess import sys import time import os import configparser from PyQt5.QtCore import QTimer from PyQt5 import QtGui, QtWidgets class ConfigManager(QtWidgets.QWidget): def __init__(self, config, parent = None): QtWidgets.QWidget.__init__(self, parent) self.config = config class Manager(): def __init__(self, config, parent = None): self.config = config self.parent = parent self.timer = QTimer() self.timer.timeout.connect(self.check_events) self.process = None self.device = None self.wid = None self.pm_cookie = None self.presentation_mode = False self.presentation_active = False self.workrave_mode = None self.viewer = self.which(self.config['programs']['pdfviewer']) self.wmctrl = self.which('wmctrl') self.xdotool = self.which('xdotool') def check_events(self): if self.process: if self.process.poll() == None: try: for event in self.device.read(): if event.type == evdev.ecodes.EV_KEY: - if event.code == evdev.ecodes.KEY_PAGEUP and event.value == 1: + if event.code == evdev.ecodes.KEY_LEFT and event.value == 1: + self.send_key(self.wid, 'Page_Up') + elif event.code == evdev.ecodes.KEY_RIGHT and event.value == 1: + self.send_key(self.wid, 'Page_Down') + elif event.code == evdev.ecodes.KEY_PAGEUP and event.value == 1: self.send_key(self.wid, 'Page_Up') elif event.code == evdev.ecodes.KEY_PAGEDOWN and event.value == 1: self.send_key(self.wid, 'Page_Down') elif event.code == evdev.ecodes.KEY_F5 and event.value == 1: self.send_key(self.wid, 'F5') elif event.code == evdev.ecodes.KEY_ESC and event.value == 1: self.send_key(self.wid, 'Escape') elif event.code == evdev.ecodes.KEY_DOT and event.value == 1: self.send_key(self.wid, 'b') except BlockingIOError: pass else: self.stop_presentation() else: self.stop_presentation() def stop_presentation(self): print('Stopping Presentation') self.presentation_active = False self.timer.stop() try: self.device.ungrab() except IOError: pass self.device.close() self.device = None self.wid = None if self.config['general']['inhibit_xdg_pm'] == 'yes': try: pm = dbus.SessionBus().get_object("org.freedesktop.PowerManagement", "/org/freedesktop/PowerManagement/Inhibit") pm.UnInhibit(self.pm_cookie, dbus_interface='org.freedesktop.PowerManagement.Inhibit') except: QtWidgets.QMessageBox.critical(self.parent, 'Error resetting PM!', 'Failed to reset the Power Manager') if self.config['general']['xfce_pm_presentation_mode'] == 'yes': try: xfc = dbus.SessionBus().get_object('org.xfce.Xfconf', '/org/xfce/Xfconf') xfc.SetProperty('xfce4-power-manager', '/xfce4-power-manager/presentation-mode', self.presentation_mode) except: QtWidgets.QMessageBox.critical(self.parent, 'Error resetting Xfce PM!', 'Failed to reset the Xfce Power Manager') if self.config['general']['inhibit_workrave'] == 'yes': try: wr = dbus.SessionBus().get_object('org.workrave.Workrave', '/org/workrave/Workrave/Core') iface = dbus.Interface(wr, 'org.workrave.CoreInterface') iface.SetOperationMode(self.workrave_mode) except: QtWidgets.QMessageBox.critical(self.parent, 'Error resetting Workrave!', 'Failed to reset Workrave state') def check_viewer(self): if self.viewer: return True else: return False def check_xdotool(self): if self.xdotool: ret = subprocess.run([self.xdotool, "--version"], stdout = subprocess.PIPE, universal_newlines = True) if ret.returncode == 0: version = ret.stdout.replace('xdotool version ', '').replace('\n', '') return version return False def check_wmctrl(self): if self.wmctrl: ret = subprocess.run([self.wmctrl, "--version"], stdout = subprocess.PIPE, universal_newlines = True) if ret.returncode == 0: version = ret.stdout.replace('\n', '') return version return False def find_window_for_pid(self, pid): print('Looking for window for PID: ' + str(pid)) ret = subprocess.run([self.wmctrl, "-l", "-p"], stdout = subprocess.PIPE, universal_newlines = True) if ret.returncode == 0: wpid = 0 wmid = 0 for line in ret.stdout.split('\n'): args = line.split(' ') pos = 1 for arg in args: if arg == '': continue if pos == 1: wmid = arg elif pos == 3: wpid = arg pos += 1 if wpid == str(pid): return wmid return False else: return False def get_window(self, pattern): ret = subprocess.run([self.xdotool, "search", "--name", pattern], stdout = subprocess.PIPE, universal_newlines = True) if ret.returncode == 0: return ret.stdout.replace('\n', '') else: return False def get_device(self, devname): devnames = [a.strip() for a in devname.split(',')] devices = [evdev.InputDevice(fn) for fn in evdev.list_devices()] for dev in devices: if dev.name in devnames: print('Found possible device: ', dev.path) # Check for pointing devices - if 3 in dev.capabilities(): + # 1 = EV_KEY + # + caps = dev.capabilities() + if not 1 in caps or 3 in caps: continue return dev.path return False def send_key(self, wid, key): # Save window focus # Focus window # Send key # Restore focused window ret = subprocess.run([self.xdotool, "getwindowfocus"], stdout = subprocess.PIPE, universal_newlines = True) if ret.returncode == 0: wid_save = ret.stdout.replace('\n', '') else: return ret.returncode subprocess.run([self.xdotool, "windowfocus", "--sync", wid]) ret = subprocess.run([self.xdotool, "key", key]) subprocess.run([self.xdotool, "windowfocus", "--sync", wid_save]) return ret.returncode def startPresentation(self): print('Start Presentation') if self.presentation_active: QtWidgets.QMessageBox.critical(self.parent, 'Presentation already running!', 'There is already a presentation running.') return False device = self.get_device(self.config['devices']['presenter']) if not device: QtWidgets.QMessageBox.critical(self.parent, 'Device not found!', 'Could not find presentation remote control') return False self.device = evdev.InputDevice(device) try: self.device.grab() except IOError: QtWidgets.QMessageBox.critical(self.parent, 'Error grabbing device', 'Could not grab Input device.') return False fname, ffilter = QtWidgets.QFileDialog.getOpenFileName(self.parent, 'Open file', '', 'PDF (*.pdf)') if fname == "": return False opts = self.config['programs']['pdfvieweropts'] print(self.viewer) print(fname) if opts: self.process = subprocess.Popen([self.viewer, opts, fname]) else: self.process = subprocess.Popen([self.viewer, fname]) # Give the PDF viewer some time to show its window wid = self.find_window_for_pid(self.process.pid) count = 0 while not wid and count < 10: time.sleep(0.5) wid = self.find_window_for_pid(self.process.pid) count += 1 if not wid: QtWidgets.QMessageBox.critical(self.parent, 'PDF Viewer not found!', 'Could not find the PDF presentation window in time') return False self.wid = wid print('Found PDF Viewer Window: ' + wid) # Inhibit power management if self.config['general']['inhibit_xdg_pm'] == 'yes': try: pm = dbus.SessionBus().get_object("org.freedesktop.PowerManagement", "/org/freedesktop/PowerManagement/Inhibit") self.pm_cookie = pm.Inhibit("lt_presentation", "Presentation Starting", dbus_interface='org.freedesktop.PowerManagement.Inhibit') except: QtWidgets.QMessageBox.critical(self.parent, 'Power Management!', 'Could not inhibit Power Management. Is the daemon active?') if self.config['general']['xfce_pm_presentation_mode'] == 'yes': try: xfc = dbus.SessionBus().get_object('org.xfce.Xfconf', '/org/xfce/Xfconf') self.presentation_mode = xfc.GetProperty('xfce4-power-manager', '/xfce4-power-manager/presentation-mode') xfc.SetProperty('xfce4-power-manager', '/xfce4-power-manager/presentation-mode', dbus.Boolean(True, variant_level=1)) except: QtWidgets.QMessageBox.critical(self.parent, 'Xfce Power Manager!', 'Could not set the Xfce Power Manager to presentation mode! Is it active?') if self.config['general']['inhibit_workrave'] == 'yes': try: wr = dbus.SessionBus().get_object('org.workrave.Workrave', '/org/workrave/Workrave/Core') iface = dbus.Interface(wr, 'org.workrave.CoreInterface') self.workrave_mode = iface.GetOperationMode() iface.SetOperationMode('suspended') except: QtWidgets.QMessageBox.critical(self.parent, 'Workrave!', 'Could not suspend Workrave. Is it running?') self.presentation_active = True self.timer.start(10) def quit(self): if self.presentation_active: self.stop_presentation() QtWidgets.qApp.quit() def which(self, program): def is_exe(fpath): return os.path.isfile(fpath) and os.access(fpath, os.X_OK) fpath, fname = os.path.split(program) if fpath: if is_exe(program): return program else: for path in os.environ["PATH"].split(os.pathsep): path = path.strip('"') exe_file = os.path.join(path, program) if is_exe(exe_file): return exe_file return None class SystemTrayIcon(QtWidgets.QSystemTrayIcon): def __init__(self, icon, m, parent=None): self.parent = parent QtWidgets.QSystemTrayIcon.__init__(self, icon, parent) menu = QtWidgets.QMenu(parent) presentationAction = menu.addAction("Start Presentation...") presentationAction.triggered.connect(m.startPresentation) exitAction = menu.addAction("Exit") exitAction.triggered.connect(m.quit) self.setContextMenu(menu) def load_config(cfgFile): config = configparser.ConfigParser() config.read(cfgFile) return config def save_config(config, cfgFile): with open(cfgFile, 'w') as configFile: config.write(configFile) def recreate_config(cfgFile): config = configparser.ConfigParser() config['general'] = { 'xfce_pm_presentation_mode' : 'yes', 'inhibit_xdg_pm' : 'yes', 'inhibit_workrave' : 'yes' } config['programs'] = { 'pdfviewer' : 'okular', 'pdfvieweropts' : '--presentation', } config['devices'] = { 'presenter' : 'Logitech USB Receiver, Genius Ring Presenter, Wireless Presenter' } save_config(config, cfgFile) def main(): app = QtWidgets.QApplication(sys.argv) app.setQuitOnLastWindowClosed(False); cfgPath = os.path.expanduser("~") + "/.lt_presentation/" if not os.path.isdir(cfgPath): os.makedirs(cfgPath) cfgFile = cfgPath + "config.ini" if not os.path.exists(cfgFile): recreate_config(cfgFile) cfg = load_config(cfgFile) w = QtWidgets.QWidget() m = Manager(cfg, w) version = m.check_xdotool() if version: print('Running on xdotool version ' + version) else: QtWidgets.QMessageBox.critical(w, 'Tool not found', 'Could not find xdotool, will now quit') sys.exit(1) viewer = m.check_viewer() if viewer: print('Found configured PDF viewer ' + cfg['programs']['pdfviewer']) else: QtWidgets.QMessageBox.critical(w, 'Tool not found', 'Could not find PDF viewer, will now quit') sys.exit(1) wmctrlVersion = m.check_wmctrl() if wmctrlVersion: print('Running on wmctrl version ' + wmctrlVersion) else: QtWidgets.QMessageBox.critical(w, 'Tool not found', 'Could not find wmctrl, will now quit') sys.exit(1) # Wait for up to 30 seconds for the systemTray to become available (required for, e.g., Xfce) count = 0 while not QtWidgets.QSystemTrayIcon.isSystemTrayAvailable() and count < 30: time.sleep(1) count += 1 trayIcon = SystemTrayIcon(QtGui.QIcon("/usr/share/icons/lt_presentation.png"), m, w) trayIcon.show() sys.exit(app.exec_()) if __name__ == '__main__': main()