Add modified efck files.
This commit is contained in:
parent
4a65e53ed8
commit
772200e3f3
196
python3-pkg-efck/output.py
Normal file
196
python3-pkg-efck/output.py
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
||||||
|
from . import IS_MACOS, IS_X11, IS_WAYLAND, IS_WIDOWS, PLATFORM
|
||||||
|
from .qt import *
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def type_chars(text: str, force_clipboard):
|
||||||
|
TYPEOUT_COMMANDS = (
|
||||||
|
(IS_X11, ['xdotool', 'type', text]),
|
||||||
|
(IS_X11 or IS_WAYLAND, ['ydotool', 'type', '--next-delay', '0', '--key-delay', '0', text]),
|
||||||
|
(IS_X11 or IS_WAYLAND, ['wtype', text]),
|
||||||
|
(IS_MACOS, lambda: _type_macos(text)),
|
||||||
|
(IS_WIDOWS, lambda: _type_windos(text))
|
||||||
|
)
|
||||||
|
once = False
|
||||||
|
res = -1 # 0/Falsy = success
|
||||||
|
for cond, args in TYPEOUT_COMMANDS:
|
||||||
|
if cond:
|
||||||
|
if not once:
|
||||||
|
logger.info('Typing out text: %s', text)
|
||||||
|
once = True
|
||||||
|
|
||||||
|
if callable(args):
|
||||||
|
res = args()
|
||||||
|
break
|
||||||
|
|
||||||
|
if not shutil.which(args[0]):
|
||||||
|
logger.warning('Platform "%s" but command "%s" unavailable', PLATFORM, args[0])
|
||||||
|
continue
|
||||||
|
logger.info('Executing: %s', args)
|
||||||
|
proc = subprocess.run(args)
|
||||||
|
res = proc.returncode
|
||||||
|
if not res:
|
||||||
|
break
|
||||||
|
logger.warning('Subprocess exit code/error: %s', res)
|
||||||
|
else:
|
||||||
|
logger.error('No command applies. Please see above for additional warnings.')
|
||||||
|
|
||||||
|
if res == 0 and not force_clipboard:
|
||||||
|
# Supposedly we're done
|
||||||
|
return
|
||||||
|
|
||||||
|
# Otherwise
|
||||||
|
_copy_to_clipboard(text)
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_to_clipboard(text):
|
||||||
|
# Copy the emoji to global system clipboard
|
||||||
|
QApplication.instance().clipboard().setText(text, QClipboard.Mode.Clipboard)
|
||||||
|
assert QApplication.instance().clipboard().text(QClipboard.Mode.Clipboard) == text
|
||||||
|
logger.info('Text copied to clipboard: %s', text)
|
||||||
|
|
||||||
|
# And raise a desktop notification about it ...
|
||||||
|
"""
|
||||||
|
# Export emoji to icon for the notification
|
||||||
|
qicon, icon_fname = None, None
|
||||||
|
try:
|
||||||
|
text_is_emoji = len(text) == 1
|
||||||
|
if text_is_emoji:
|
||||||
|
def _emoji_to_qicon(text, filename):
|
||||||
|
pixmap = QPixmap(128, 128)
|
||||||
|
pixmap.fill(Qt.GlobalColor.transparent)
|
||||||
|
painter = QPainter(pixmap)
|
||||||
|
from .tabs import EmojiTab
|
||||||
|
font = QFont(EmojiTab.Delegate.ICON_FONT)
|
||||||
|
font.setPixelSize(int(round(pixmap.rect().height() * .95)))
|
||||||
|
painter.setFont(font)
|
||||||
|
painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, text)
|
||||||
|
del painter # https://stackoverflow.com/a/59605570/1090455
|
||||||
|
res = pixmap.toImage().save(filename)
|
||||||
|
if not res:
|
||||||
|
logger.warning('Failed to save emoji icon to "%s"', filename)
|
||||||
|
return QIcon(pixmap)
|
||||||
|
|
||||||
|
with NamedTemporaryFile(prefix=QApplication.instance().applicationName() + '-',
|
||||||
|
suffix='.png', delete=False) as fd:
|
||||||
|
qicon = _emoji_to_qicon(text, fd.name)
|
||||||
|
icon_fname = fd.name
|
||||||
|
|
||||||
|
notification_msg = [QApplication.instance().applicationName(),
|
||||||
|
f'Text {text!r} copied to clipboard.']
|
||||||
|
if shutil.which('notify-send'):
|
||||||
|
# XDG notification looks better than QSystemTrayIcon message, so try it first
|
||||||
|
subprocess.run(['notify-send',
|
||||||
|
'--expire-time', '5000',
|
||||||
|
'--icon', icon_fname or 'info',
|
||||||
|
'--urgency', 'low',
|
||||||
|
*notification_msg])
|
||||||
|
elif QSystemTrayIcon.isSystemTrayAvailable():
|
||||||
|
from efck.gui import ICON_DIR
|
||||||
|
|
||||||
|
tray = QSystemTrayIcon(QIcon(QPixmap(str(ICON_DIR / 'awesome-emoji.png'))),
|
||||||
|
QApplication.instance(), visible=True)
|
||||||
|
if qicon:
|
||||||
|
notification_msg.append(qicon) # FIXME: Why isn't qicon shown in the tooltip???
|
||||||
|
# notification_msg.append(QSystemTrayIcon.Information) # Yet this works
|
||||||
|
tray.showMessage(*notification_msg, msecs=5000)
|
||||||
|
else:
|
||||||
|
logger.warning('No desktop notification system (notify-send) or systray detected')
|
||||||
|
finally:
|
||||||
|
if icon_fname:
|
||||||
|
os.remove(icon_fname)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _type_macos(text):
|
||||||
|
# https://apple.stackexchange.com/questions/171709/applescript-get-active-application
|
||||||
|
# https://stackoverflow.com/questions/41673019/insert-emoji-into-focused-text-input
|
||||||
|
# https://stackoverflow.com/questions/60385810/inserting-chinese-characters-in-applescript
|
||||||
|
# https://apple.stackexchange.com/questions/288536/is-it-possible-to-keystroke-special-characters-in-applescript
|
||||||
|
_OSASCRIPT = '''
|
||||||
|
set prev_contents to the clipboard
|
||||||
|
set the clipboard to "{}"
|
||||||
|
tell application "System Events"
|
||||||
|
keystroke "v" using command down
|
||||||
|
key code 124 -- right arrow clears the selection
|
||||||
|
delay 0.05
|
||||||
|
end tell
|
||||||
|
set the clipboard to prev_contents
|
||||||
|
'''
|
||||||
|
text = text.replace('"', '\\"')
|
||||||
|
proc = subprocess.run(['sh', '-c', f"sleep .2 && nohup osascript -e '{_OSASCRIPT.format(text)}' & disown"])
|
||||||
|
return proc.returncode
|
||||||
|
|
||||||
|
|
||||||
|
def _type_windos(text):
|
||||||
|
import ctypes
|
||||||
|
from ctypes.wintypes import DWORD, LONG, WORD, PULONG
|
||||||
|
|
||||||
|
# God help us!
|
||||||
|
|
||||||
|
# Adapted from:
|
||||||
|
# https://github.com/Drov3r/Forza/blob/b3e489c1447f2fdc46081e053008aaa1ab77a12a/control.py#L63
|
||||||
|
# https://stackoverflow.com/questions/22291282/using-sendinput-to-send-unicode-characters-beyond-uffff
|
||||||
|
|
||||||
|
class KeyboardInput(ctypes.Structure):
|
||||||
|
_fields_ = [("wVk", WORD),
|
||||||
|
("wScan", WORD),
|
||||||
|
("dwFlags", DWORD),
|
||||||
|
("time", DWORD),
|
||||||
|
("dwExtraInfo", PULONG)]
|
||||||
|
|
||||||
|
class HardwareInput(ctypes.Structure):
|
||||||
|
_fields_ = [("uMsg", DWORD),
|
||||||
|
("wParamL", WORD),
|
||||||
|
("wParamH", WORD)]
|
||||||
|
|
||||||
|
class MouseInput(ctypes.Structure):
|
||||||
|
_fields_ = [("dx", LONG),
|
||||||
|
("dy", LONG),
|
||||||
|
("mouseData", DWORD),
|
||||||
|
("dwFlags", DWORD),
|
||||||
|
("time", DWORD),
|
||||||
|
("dwExtraInfo", PULONG)]
|
||||||
|
|
||||||
|
class Input_I(ctypes.Union):
|
||||||
|
_fields_ = [("ki", KeyboardInput),
|
||||||
|
("mi", MouseInput),
|
||||||
|
("hi", HardwareInput)]
|
||||||
|
|
||||||
|
class Input(ctypes.Structure):
|
||||||
|
_fields_ = [("type", DWORD),
|
||||||
|
("ii", Input_I)]
|
||||||
|
|
||||||
|
def as_wchars(ch):
|
||||||
|
assert len(ch) == 1, (ch, len(ch))
|
||||||
|
bytes = ch.encode('utf-16be')
|
||||||
|
while bytes:
|
||||||
|
yield int.from_bytes(bytes[:2], 'big')
|
||||||
|
bytes = bytes[2:]
|
||||||
|
|
||||||
|
TYPE_KEYBOARD_INPUT = 1
|
||||||
|
KEYEVENTF_UNICODE = 0x4
|
||||||
|
KEYEVENTF_KEYUP = 0x2
|
||||||
|
KEYEVENTF_KEYDOWN = 0
|
||||||
|
|
||||||
|
inputs = []
|
||||||
|
for ch in text:
|
||||||
|
for wch in as_wchars(ch):
|
||||||
|
for flag in (KEYEVENTF_KEYDOWN,
|
||||||
|
KEYEVENTF_KEYUP):
|
||||||
|
input = Input(TYPE_KEYBOARD_INPUT)
|
||||||
|
# XXX: Assuming ctypes.Structure is memset to 0 at initialization?
|
||||||
|
input.ii.ki.wScan = WORD(wch)
|
||||||
|
input.ii.ki.dwFlags = KEYEVENTF_UNICODE | flag
|
||||||
|
inputs.append(input)
|
||||||
|
|
||||||
|
inputs = (Input * len(inputs))(*inputs)
|
||||||
|
n = ctypes.windll.user32.SendInput(len(inputs), inputs, ctypes.sizeof(Input))
|
||||||
|
assert n == len(inputs)
|
||||||
|
return 0
|
90
python3-pkg-efck/tabs/_options.py
Normal file
90
python3-pkg-efck/tabs/_options.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import copy
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ..qt import *
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OptionsTab(QWidget):
|
||||||
|
def __init__(self, parent=None, **kwargs):
|
||||||
|
super().__init__(parent=parent, **kwargs)
|
||||||
|
self.setLayout(QVBoxLayout(self))
|
||||||
|
|
||||||
|
from ..config import config_state
|
||||||
|
|
||||||
|
self._initial_config = copy.deepcopy(config_state)
|
||||||
|
|
||||||
|
def zoom_changed(value):
|
||||||
|
nonlocal ONE_TICK_IN_PCT, slider_label, change_zoom_timer
|
||||||
|
slider_label.setText(f'Zoom: {value * ONE_TICK_IN_PCT:3d}%')
|
||||||
|
change_zoom_timer.start()
|
||||||
|
|
||||||
|
def _change_zoom():
|
||||||
|
nonlocal ONE_TICK_IN_PCT, config_state, zoom_slider, self
|
||||||
|
config_state['zoom'] = zoom = zoom_slider.value() * ONE_TICK_IN_PCT
|
||||||
|
logger.info('Set zoom: %f', zoom)
|
||||||
|
|
||||||
|
change_zoom_timer = QTimer(
|
||||||
|
parent=self,
|
||||||
|
singleShot=True,
|
||||||
|
timeout=_change_zoom,
|
||||||
|
interval=200)
|
||||||
|
|
||||||
|
main_box = QGroupBox(self)
|
||||||
|
self.layout().addWidget(main_box)
|
||||||
|
main_box.setLayout(QVBoxLayout(main_box))
|
||||||
|
force_clipboard_cb = QCheckBox(
|
||||||
|
'Force &clipboard',
|
||||||
|
parent=self,
|
||||||
|
toolTip='Copy selected emoji/text into the clipboard in addition to typing it out. \n'
|
||||||
|
"Useful if typeout (default action) doesn't work on your system.")
|
||||||
|
force_clipboard_cb.setChecked(config_state.__getitem__('force_clipboard'))
|
||||||
|
force_clipboard_cb.stateChanged.connect(
|
||||||
|
lambda state: config_state.__setitem__('force_clipboard', bool(state)))
|
||||||
|
main_box.layout().addWidget(force_clipboard_cb)
|
||||||
|
|
||||||
|
box = QWidget(self)
|
||||||
|
main_box.layout().addWidget(box)
|
||||||
|
box.setLayout(QHBoxLayout(box))
|
||||||
|
box.layout().setContentsMargins(0, 0, 0, 0)
|
||||||
|
ONE_TICK_IN_PCT = 5
|
||||||
|
zoom_slider = QSlider(
|
||||||
|
parent=self,
|
||||||
|
orientation=Qt.Orientation.Horizontal,
|
||||||
|
tickPosition=QSlider.TickPosition.TicksBothSides,
|
||||||
|
minimum=70 // ONE_TICK_IN_PCT,
|
||||||
|
maximum=200 // ONE_TICK_IN_PCT,
|
||||||
|
pageStep=30 // ONE_TICK_IN_PCT,
|
||||||
|
singleStep=10 // ONE_TICK_IN_PCT,
|
||||||
|
tickInterval=10 // ONE_TICK_IN_PCT,
|
||||||
|
value=config_state['zoom'] // ONE_TICK_IN_PCT,
|
||||||
|
)
|
||||||
|
zoom_slider.valueChanged.connect(zoom_changed)
|
||||||
|
slider_label = QLabel(f'Z&oom: {zoom_slider.value() * ONE_TICK_IN_PCT:3d}%')
|
||||||
|
slider_label.setBuddy(zoom_slider)
|
||||||
|
box.layout().addWidget(slider_label)
|
||||||
|
box.layout().addWidget(zoom_slider)
|
||||||
|
|
||||||
|
def add_section(self, name, widget: QWidget):
|
||||||
|
box = QGroupBox(name.replace('&', ''), self)
|
||||||
|
widget.setParent(box)
|
||||||
|
box.setLayout(QVBoxLayout(box))
|
||||||
|
box.layout().setContentsMargins(0, 0, 0, 0)
|
||||||
|
box.layout().addWidget(widget)
|
||||||
|
self.layout().addWidget(box)
|
||||||
|
|
||||||
|
def save_dirty(self, exiting=False) -> bool:
|
||||||
|
"""Returns True if config had changed and emoji need reloading"""
|
||||||
|
from ..config import dump_config, config_state
|
||||||
|
logger.debug('Saving config state if changed')
|
||||||
|
if config_state != self._initial_config:
|
||||||
|
dump_config()
|
||||||
|
self._initial_config = copy.deepcopy(config_state)
|
||||||
|
|
||||||
|
if not exiting:
|
||||||
|
for tab in self.nativeParentWidget().tabs:
|
||||||
|
tab.init_delegate(config=config_state.get(tab.__class__.__name__),
|
||||||
|
zoom=config_state.get('zoom', 100) / 100)
|
||||||
|
return True
|
Loading…
Reference in New Issue
Block a user