"Fossies" - the Fresh Open Source Software Archive

Member "screenkey-1.1/Screenkey/labelmanager.py" (19 May 2020, 20524 Bytes) of package /linux/privat/screenkey-1.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "labelmanager.py" see the Fossies "Dox" file reference documentation and the last Fossies "Diffs" side-by-side code changes report: 0.9_vs_1.0.

    1 # -*- coding: utf-8 -*-
    2 # "screenkey" is distributed under GNU GPLv3+, WITHOUT ANY WARRANTY.
    3 # Copyright(c) 2010-2012: Pablo Seminario <pabluk@gmail.com>
    4 # Copyright(c) 2015-2016: wave++ "Yuri D'Elia" <wavexx@thregr.org>
    5 # Copyright(c) 2019-2020: Yuto Tokunaga <yuntan.sub1@gmail.com>
    6 
    7 from .inputlistener import InputListener, InputType
    8 
    9 from gi.repository import GLib
   10 
   11 from collections import namedtuple
   12 from datetime import datetime
   13 
   14 # Key replacement data:
   15 #
   16 # bk_stop: stops backspace processing in baked mode, but not full mode
   17 #          these keys generally move the caret, and are also padded with a thin space
   18 # silent:  always stops backspace processing (baked/full mode)
   19 #          these keys generally do not emit output in the text and cannot be processed
   20 # spaced:  strong spacing is required around the symbol
   21 
   22 ReplData = namedtuple('ReplData', ['value', 'font', 'suffix'])
   23 KeyRepl  = namedtuple('KeyRepl',  ['bk_stop', 'silent', 'spaced', 'repl'])
   24 KeyData  = namedtuple('KeyData',  ['stamp', 'is_ctrl', 'bk_stop', 'silent', 'spaced', 'markup'])
   25 
   26 REPLACE_SYMS = {
   27     # Regular keys
   28     'Escape':       KeyRepl(True,  True,  True,  _('Esc')),
   29     'Tab':          KeyRepl(True,  False, False, _('↹')),
   30     'ISO_Left_Tab': KeyRepl(True,  False, False, _('↹')),
   31     'Return':       KeyRepl(True,  False, False, _('⏎')),
   32     'space':        KeyRepl(False, False, False, _('␣')),
   33     'BackSpace':    KeyRepl(True,  True,  False, _('⌫')),
   34     'Caps_Lock':    KeyRepl(True,  True,  True,  _('Caps')),
   35     'F1':           KeyRepl(True,  True,  True,  _('F1')),
   36     'F2':           KeyRepl(True,  True,  True,  _('F2')),
   37     'F3':           KeyRepl(True,  True,  True,  _('F3')),
   38     'F4':           KeyRepl(True,  True,  True,  _('F4')),
   39     'F5':           KeyRepl(True,  True,  True,  _('F5')),
   40     'F6':           KeyRepl(True,  True,  True,  _('F6')),
   41     'F7':           KeyRepl(True,  True,  True,  _('F7')),
   42     'F8':           KeyRepl(True,  True,  True,  _('F8')),
   43     'F9':           KeyRepl(True,  True,  True,  _('F9')),
   44     'F10':          KeyRepl(True,  True,  True,  _('F10')),
   45     'F11':          KeyRepl(True,  True,  True,  _('F11')),
   46     'F12':          KeyRepl(True,  True,  True,  _('F12')),
   47     'Up':           KeyRepl(True,  True,  False, _('↑')),
   48     'Left':         KeyRepl(True,  True,  False, _('←')),
   49     'Right':        KeyRepl(True,  True,  False, _('→')),
   50     'Down':         KeyRepl(True,  True,  False, _('↓')),
   51     'Prior':        KeyRepl(True,  True,  True,  _('PgUp')),
   52     'Next':         KeyRepl(True,  True,  True,  _('PgDn')),
   53     'Home':         KeyRepl(True,  True,  True,  _('Home')),
   54     'End':          KeyRepl(True,  True,  True,  _('End')),
   55     'Insert':       KeyRepl(False, True,  True,  _('Ins')),
   56     'Delete':       KeyRepl(True,  False, True,  _('Del')),
   57     'KP_End':       KeyRepl(False, False, True,  _('(1)')),
   58     'KP_Down':      KeyRepl(False, False, True,  _('(2)')),
   59     'KP_Next':      KeyRepl(False, False, True,  _('(3)')),
   60     'KP_Left':      KeyRepl(False, False, True,  _('(4)')),
   61     'KP_Begin':     KeyRepl(False, False, True,  _('(5)')),
   62     'KP_Right':     KeyRepl(False, False, True,  _('(6)')),
   63     'KP_Home':      KeyRepl(False, False, True,  _('(7)')),
   64     'KP_Up':        KeyRepl(False, False, True,  _('(8)')),
   65     'KP_Prior':     KeyRepl(False, False, True,  _('(9)')),
   66     'KP_Insert':    KeyRepl(False, False, True,  _('(0)')),
   67     'KP_Delete':    KeyRepl(False, False, True,  _('(.)')),
   68     'KP_Add':       KeyRepl(False, False, True,  _('(+)')),
   69     'KP_Subtract':  KeyRepl(False, False, True,  _('(-)')),
   70     'KP_Multiply':  KeyRepl(False, False, True,  _('(*)')),
   71     'KP_Divide':    KeyRepl(False, False, True,  _('(/)')),
   72     'KP_Enter':     KeyRepl(True,  False, False, _('⏎')),
   73     'Num_Lock':     KeyRepl(False, True,  True,  _('NumLck')),
   74     'Scroll_Lock':  KeyRepl(False, True,  True,  _('ScrLck')),
   75     'Pause':        KeyRepl(False, True,  True,  _('Pause')),
   76     'Break':        KeyRepl(False, True,  True,  _('Break')),
   77     'Print':        KeyRepl(False, True,  True,  _('Print')),
   78     'Multi_key':    KeyRepl(False, True,  True,  _('Compose')),
   79 
   80     # Multimedia keys
   81     'XF86AudioMute':         KeyRepl(True, True, True, [ReplData(_('\uf026'),  'FontAwesome', None),
   82                                                         ReplData(_('Mute'),    None,          None)]),
   83     'XF86AudioMicMute':      KeyRepl(True, True, True, [ReplData(_('\uf131'),  'FontAwesome', None),
   84                                                         ReplData(_('Rec'),     None,          None)]),
   85     'XF86AudioRaiseVolume':  KeyRepl(True, True, True, [ReplData(_('\uf028'),  'FontAwesome', None),
   86                                                         ReplData(_('Vol'),     None,          '+')]),
   87     'XF86AudioLowerVolume':  KeyRepl(True, True, True, [ReplData(_('\uf027'),  'FontAwesome', None),
   88                                                         ReplData(_('Vol'),     None,          '-')]),
   89     'XF86AudioPrev':         KeyRepl(True, True, True, [ReplData(_('\uf048'),  'FontAwesome', None),
   90                                                         ReplData(_('Prev'),    None,          None)]),
   91     'XF86AudioNext':         KeyRepl(True, True, True, [ReplData(_('\uf051'),  'FontAwesome', None),
   92                                                         ReplData(_('Next'),    None,          None)]),
   93     'XF86AudioPlay':         KeyRepl(True, True, True, [ReplData(_('\uf04b'),  'FontAwesome', None),
   94                                                         ReplData(_('▶'),       None,          None)]),
   95     'XF86AudioStop':         KeyRepl(True, True, True, [ReplData(_('\uf04d'),  'FontAwesome', None),
   96                                                         ReplData(_('⬛'),       None,          None)]),
   97     'XF86Eject':             KeyRepl(True, True, True, [ReplData(_('\uf052'),  'FontAwesome', None),
   98                                                         ReplData(_('Eject'),   None,          None)]),
   99     'XF86MonBrightnessDown': KeyRepl(True, True, True, [ReplData(_('\uf185'),  'FontAwesome', '-'),
  100                                                         ReplData(_('Bright'),  None,          '-')]),
  101     'XF86MonBrightnessUp':   KeyRepl(True, True, True, [ReplData(_('\uf185'),  'FontAwesome', '+'),
  102                                                         ReplData(_('Bright'),  None,          '+')]),
  103     'XF86Display':           KeyRepl(True, True, True, [ReplData(_('\uf108'),  'FontAwesome', None),
  104                                                         ReplData(_('Display'), None,          None)]),
  105     'XF86WLAN':              KeyRepl(True, True, True, [ReplData(_('\uf1eb'),  'FontAwesome', None),
  106                                                         ReplData(_('WLAN'),    None,          None)]),
  107     'XF86Search':            KeyRepl(True, True, True, [ReplData(_('\uf002'),  'FontAwesome', None),
  108                                                         ReplData(_('Search'),  None,          None)]),
  109 }
  110 
  111 WHITESPACE_SYMS = {'Tab', 'ISO_Left_Tab', 'Return', 'space', 'KP_Enter'}
  112 
  113 MODS_SYMS = {
  114     'shift':  {'Shift_L', 'Shift_R'},
  115     'ctrl':   {'Control_L', 'Control_R'},
  116     'alt':    {'Alt_L', 'Alt_R', 'Meta_L', 'Meta_R'},
  117     'super':  {'Super_L', 'Super_R'},
  118     'hyper':  {'Hyper_L', 'Hyper_R'},
  119     'alt_gr': {'ISO_Level3_Shift'},
  120 }
  121 
  122 REPLACE_MODS = {
  123     'shift':  {'normal': _('Shift+'), 'emacs': 'S-', 'mac': _('⇧+')},
  124     'ctrl':   {'normal': _('Ctrl+'),  'emacs': 'C-', 'mac': _('⌘+')},
  125     'alt':    {'normal': _('Alt+'),   'emacs': 'M-', 'mac': _('⌥+')},
  126     'super':  {'normal': _('Super+'), 'emacs': 's-',
  127                'win': [ReplData(_('\uf17a'), 'FontAwesome', '+'),
  128                        ReplData(_('Win'),    None,          '+')],
  129                'tux': [ReplData(_('\uf17c'), 'FontAwesome', '+'),
  130                        ReplData(_('Super'),  None,          '+')]},
  131     'hyper':  {'normal': _('Hyper+'), 'emacs': 'H-'},
  132     'alt_gr': {'normal': _('AltGr+'), 'emacs': 'AltGr-'},
  133 }
  134 
  135 
  136 def keysym_to_mod(keysym):
  137     for k, v in MODS_SYMS.items():
  138         if keysym in v:
  139             return k
  140     return None
  141 
  142 
  143 class LabelManager(object):
  144     def __init__(self, listener, logger, key_mode, bak_mode, mods_mode, mods_only,
  145                  multiline, vis_shift, vis_space, recent_thr, compr_cnt, ignore, pango_ctx):
  146         self.key_mode = key_mode
  147         self.bak_mode = bak_mode
  148         self.mods_mode = mods_mode
  149         self.logger = logger
  150         self.listener = listener
  151         self.data = []
  152         self.enabled = True
  153         self.mods_only = mods_only
  154         self.multiline = multiline
  155         self.vis_shift = vis_shift
  156         self.vis_space = vis_space
  157         self.recent_thr = recent_thr
  158         self.compr_cnt = compr_cnt
  159         self.ignore = ignore
  160         self.kl = None
  161         self.font_families = {x.get_name() for x in pango_ctx.list_families()}
  162         self.update_replacement_map()
  163 
  164 
  165     def __del__(self):
  166         self.stop()
  167 
  168 
  169     def start(self):
  170         self.stop()
  171         compose = (self.key_mode == 'composed')
  172         translate = (self.key_mode in ['composed', 'translated'])
  173         self.kl = InputListener(self.key_press, InputType.keyboard, compose, translate)
  174         self.kl.start()
  175         self.logger.debug("Thread started.")
  176 
  177 
  178     def stop(self):
  179         if self.kl:
  180             self.kl.stop()
  181             self.logger.debug("Thread stopped.")
  182             self.kl.join()
  183             self.kl = None
  184 
  185 
  186     def clear(self):
  187         self.data = []
  188 
  189 
  190     def get_repl_markup(self, repl):
  191         if type(repl) != list:
  192             repl = [repl]
  193         for c in repl:
  194             # no replacement data
  195             if type(c) != ReplData:
  196                 return GLib.markup_escape_text(c)
  197 
  198             # plain suffix
  199             if c.suffix is None:
  200                 sfx = ''
  201             else:
  202                 sfx = GLib.markup_escape_text(c.suffix)
  203 
  204             if c.font is None:
  205                 # regular font
  206                 return GLib.markup_escape_text(c.value) + sfx;
  207             elif c.font in self.font_families:
  208                 # custom symbol
  209                 return '<span font_family="' + c.font + '" font_weight="regular">' + \
  210                     GLib.markup_escape_text(c.value) + '</span>' + sfx;
  211 
  212 
  213     def update_replacement_map(self):
  214         self.replace_syms = {}
  215         for k, v in REPLACE_SYMS.items():
  216             markup = self.get_repl_markup(v.repl)
  217             self.replace_syms[k] = KeyRepl(v.bk_stop, v.silent, v.spaced, markup)
  218 
  219         self.replace_mods = {}
  220         for k, v in REPLACE_MODS.items():
  221             data = v.get(self.mods_mode, v['normal'])
  222             self.replace_mods[k] = self.get_repl_markup(data)
  223 
  224 
  225     def update_text(self, synthetic=False):
  226         markup = ""
  227         recent = False
  228         stamp = datetime.now()
  229         repeats = 0
  230         for i, key in enumerate(self.data):
  231             if i != 0:
  232                 last = self.data[i - 1]
  233 
  234                 # compress repeats
  235                 if self.compr_cnt and key.markup == last.markup:
  236                     repeats += 1
  237                     if repeats < self.compr_cnt:
  238                         pass
  239                     elif i == len(self.data) - 1 or key.markup != self.data[i + 1].markup:
  240                         if not recent and (stamp - key.stamp).total_seconds() < self.recent_thr:
  241                             markup += '<u>'
  242                             recent = True
  243                         markup += '<sub><small>…{}×</small></sub>'.format(repeats + 1)
  244                         if len(key.markup) and key.markup[-1] == '\n':
  245                             markup += '\n'
  246                         continue
  247                     else:
  248                         continue
  249 
  250                 # character block spacing
  251                 if len(last.markup) and last.markup[-1] == '\n':
  252                     pass
  253                 elif key.is_ctrl or last.is_ctrl or key.spaced or last.spaced:
  254                     markup += ' '
  255                 elif key.bk_stop or last.bk_stop or repeats > self.compr_cnt:
  256                     markup += '<span font_family="sans">\u2009</span>'
  257                 if key.markup != last.markup:
  258                     repeats = 0
  259 
  260             key_markup = key.markup
  261             if type(key_markup) is bytes:
  262                 key_markup = key_markup.decode()
  263             if not recent and (stamp - key.stamp).total_seconds() < self.recent_thr:
  264                 recent = True
  265                 key_markup = '<u>' + key_markup
  266 
  267             # disable ligatures
  268             if len(key.markup) == 1 and 0x0300 <= ord(key.markup) <= 0x036F:
  269                 # workaround for pango not handling ZWNJ correctly for combining marks
  270                 markup += '\u180e' + key_markup + '\u200a'
  271             elif len(key_markup):
  272                 markup += '\u200c' + key_markup
  273 
  274         if len(markup) and markup[-1] == '\n':
  275             markup = markup.rstrip('\n')
  276             if not self.vis_space and not self.data[-1].is_ctrl:
  277                 # always show some return symbol at the last line
  278                 markup += self.replace_syms['Return'].repl
  279         if recent:
  280             markup += '</u>'
  281         self.logger.debug("Label updated: %s." % repr(markup))
  282         self.listener(markup, synthetic)
  283 
  284 
  285     def queue_update(self):
  286         self.update_text(True)
  287 
  288 
  289     def key_press(self, event):
  290         if event is None:
  291             self.logger.debug("inputlistener failure: {}".format(str(self.kl.error)))
  292             self.listener(None, None)
  293             return
  294         symbol = event.symbol.decode()
  295         if event.pressed == False:
  296             self.logger.debug("Key released {:5}(ks): {}".format(event.keysym, symbol))
  297             return
  298         if symbol in self.ignore:
  299             self.logger.debug("Key ignored  {:5}(ks): {}".format(event.keysym, symbol))
  300             return
  301         if event.filtered:
  302             self.logger.debug("Key filtered {:5}(ks): {}".format(event.keysym, symbol))
  303         else:
  304             state = "repeated" if event.repeated else "pressed"
  305             string = repr(event.string)
  306             self.logger.debug("Key {:8} {:5}(ks): {} ({}, mask: {:08b})".format
  307                               (state, event.keysym, string, symbol, event.mods_mask))
  308 
  309         # Stealth enable/disable handling
  310         for mod in ['shift', 'ctrl', 'alt']:
  311             if not event.repeated and event.modifiers[mod] \
  312                and symbol in MODS_SYMS[mod]:
  313                 self.enabled = not self.enabled
  314                 state = 'enabled' if self.enabled else 'disabled'
  315                 self.logger.info("{mod}+{mod} detected: screenkey {state}".format(
  316                     mod=mod.capitalize(), state=state))
  317         if not self.enabled:
  318             return False
  319 
  320         # keep the window alive as the user is composing
  321         mod_pressed = keysym_to_mod(symbol) is not None
  322         update = len(self.data) and (event.filtered or mod_pressed)
  323 
  324         if not event.filtered:
  325             if self.key_mode in ['translated', 'composed']:
  326                 update |= self.key_normal_mode(event)
  327             elif self.key_mode == 'raw':
  328                 update |= self.key_raw_mode(event)
  329             else:
  330                 update |= self.key_keysyms_mode(event)
  331         if update:
  332             self.update_text()
  333 
  334 
  335     def key_normal_mode(self, event):
  336         self.logger.debug("key_normal_mode")
  337         # Visible modifiers
  338         mod = ''
  339         for cap in ['ctrl', 'alt', 'super', 'hyper']:
  340             if event.modifiers[cap]:
  341                 mod = mod + self.replace_mods[cap]
  342 
  343         # Backspace handling
  344         symbol = event.symbol.decode()
  345         if symbol == 'BackSpace' and not self.mods_only and \
  346            mod == '' and not event.modifiers['shift']:
  347             key_repl = self.replace_syms.get(symbol)
  348             if self.bak_mode == 'normal':
  349                 self.data.append(KeyData(datetime.now(), False, *key_repl))
  350                 return True
  351             else:
  352                 if not len(self.data):
  353                     pop = False
  354                 else:
  355                     last = self.data[-1]
  356                     if last.is_ctrl:
  357                         pop = False
  358                     elif self.bak_mode == 'baked':
  359                         pop = not last.bk_stop
  360                     else:
  361                         pop = not last.silent
  362                 if pop:
  363                     self.data.pop()
  364                 else:
  365                     self.data.append(KeyData(datetime.now(), False, *key_repl))
  366                 return True
  367 
  368         # Regular keys
  369         key_repl = self.replace_syms.get(symbol)
  370         replaced = key_repl is not None
  371         if key_repl is None:
  372             if keysym_to_mod(symbol):
  373                 return False
  374             else:
  375                 repl = event.string or symbol
  376                 markup = GLib.markup_escape_text(repl)
  377                 key_repl = KeyRepl(False, False, len(repl) > 1, markup)
  378 
  379         if event.modifiers['shift'] and \
  380            (replaced or (mod != '' and \
  381                          self.vis_shift and \
  382                          self.mods_mode != 'emacs')):
  383             # add back shift for translated keys
  384             mod = mod + self.replace_mods['shift']
  385 
  386         # Whitespace handling
  387         if not self.vis_space and mod == '' and symbol in WHITESPACE_SYMS:
  388             if symbol not in ['Return', 'KP_Enter']:
  389                 repl = event.string
  390             elif self.multiline:
  391                 repl = ''
  392             else:
  393                 repl = key_repl.repl
  394             key_repl = KeyRepl(key_repl.bk_stop, key_repl.silent, key_repl.spaced, repl)
  395 
  396         # Multiline
  397         if symbol in ['Return', 'KP_Enter'] and self.multiline == True:
  398             key_repl = KeyRepl(key_repl.bk_stop, key_repl.silent,
  399                                key_repl.spaced, key_repl.repl + '\n')
  400 
  401         if mod == '':
  402             if not self.mods_only:
  403                 repl = key_repl.repl
  404 
  405                 # switches
  406                 if symbol in ['Caps_Lock', 'Num_Lock']:
  407                     state = event.modifiers[symbol.lower()]
  408                     repl += '(%s)' % (_('off') if state else _('on'))
  409 
  410                 self.data.append(KeyData(datetime.now(), False, key_repl.bk_stop,
  411                                          key_repl.silent, key_repl.spaced, repl))
  412                 return True
  413         else:
  414             if self.mods_mode == 'emacs' or key_repl.repl[0] != mod[-1]:
  415                 repl = mod + key_repl.repl
  416             else:
  417                 repl = mod + '‟' + key_repl.repl + '”'
  418             self.data.append(KeyData(datetime.now(), True, key_repl.bk_stop,
  419                                      key_repl.silent, key_repl.spaced, repl))
  420             return True
  421 
  422         return False
  423 
  424 
  425     def key_raw_mode(self, event):
  426         # modifiers
  427         mod = ''
  428         for cap in REPLACE_MODS.keys():
  429             if event.modifiers[cap]:
  430                 mod = mod + self.replace_mods[cap]
  431 
  432         # keycaps
  433         symbol = event.symbol.decode()
  434         key_repl = self.replace_syms.get(symbol)
  435         if key_repl is None:
  436             if keysym_to_mod(symbol):
  437                 return False
  438             else:
  439                 repl = event.string.upper() if event.string else symbol
  440                 markup = GLib.markup_escape_text(repl)
  441                 key_repl = KeyRepl(False, False, len(repl) > 1, markup)
  442 
  443         if mod == '':
  444             repl = key_repl.repl
  445 
  446             # switches
  447             if symbol in ['Caps_Lock', 'Num_Lock']:
  448                 state = event.modifiers[symbol.lower()]
  449                 repl += '(%s)' % (_('off') if state else _('on'))
  450 
  451             self.data.append(KeyData(datetime.now(), False, key_repl.bk_stop,
  452                                      key_repl.silent, key_repl.spaced, repl))
  453         else:
  454             if self.mods_mode == 'emacs' or key_repl.repl[0] != mod[-1]:
  455                 repl = mod + key_repl.repl
  456             else:
  457                 repl = mod + '‟' + key_repl.repl + '”'
  458             self.data.append(KeyData(datetime.now(), True, key_repl.bk_stop,
  459                                      key_repl.silent, key_repl.spaced, repl))
  460         return True
  461 
  462 
  463     def key_keysyms_mode(self, event):
  464         symbol = event.symbol.decode()
  465         if symbol in REPLACE_SYMS:
  466             value = symbol
  467         else:
  468             value = event.string or symbol
  469         self.data.append(KeyData(datetime.now(), True, True, True, True, value))
  470         return True