From d1aad0293b25c7e13c2e9a5d0895cd09d8dff485 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sat, 19 Oct 2019 20:01:04 -0500 Subject: [PATCH] Add support for forms buttons --- source/easymacro.py | 422 +++++++++++++++++++++++++++--- source/images/close.png | Bin 0 -> 568 bytes source/images/console-24.png | Bin 0 -> 245 bytes source/images/error.png | Bin 0 -> 656 bytes source/images/install.png | Bin 0 -> 392 bytes source/images/ok.png | Bin 0 -> 660 bytes source/images/python.png | Bin 0 -> 1845 bytes source/images/python_48.png | Bin 0 -> 1084 bytes source/images/question.png | Bin 0 -> 658 bytes source/images/search-24.png | Bin 0 -> 435 bytes source/images/search-48.png | Bin 0 -> 861 bytes source/images/uninstalling-32.png | Bin 0 -> 318 bytes 12 files changed, 382 insertions(+), 40 deletions(-) create mode 100644 source/images/close.png create mode 100644 source/images/console-24.png create mode 100644 source/images/error.png create mode 100644 source/images/install.png create mode 100644 source/images/ok.png create mode 100644 source/images/python.png create mode 100644 source/images/python_48.png create mode 100644 source/images/question.png create mode 100644 source/images/search-24.png create mode 100644 source/images/search-48.png create mode 100644 source/images/uninstalling-32.png diff --git a/source/easymacro.py b/source/easymacro.py index 0bbeab1..8f9f5b9 100644 --- a/source/easymacro.py +++ b/source/easymacro.py @@ -22,6 +22,7 @@ import csv import ctypes import datetime import errno +import gettext import getpass import hashlib import json @@ -46,6 +47,8 @@ from functools import wraps from operator import itemgetter from pathlib import Path, PurePath from pprint import pprint +from urllib.request import Request, urlopen +from urllib.error import URLError, HTTPError from string import Template from subprocess import PIPE @@ -75,6 +78,7 @@ from com.sun.star.table.CellContentType import EMPTY, VALUE, TEXT, FORMULA from com.sun.star.text.ControlCharacter import PARAGRAPH_BREAK from com.sun.star.text.TextContentAnchorType import AS_CHARACTER +from com.sun.star.script import ScriptEventDescriptor from com.sun.star.lang import XEventListener from com.sun.star.awt import XActionListener from com.sun.star.awt import XMouseListener @@ -94,6 +98,20 @@ except ImportError: pass +ID_EXTENSION = '' + +DIR = { + 'images': 'images', + 'locales': 'locales', +} + +KEY = { + 'enter': 1280, +} + +SEPARATION = 5 + + MSG_LANG = { 'es': { 'OK': 'Aceptar', @@ -117,6 +135,9 @@ LOG_NAME = 'ZAZ' CLIPBOARD_FORMAT_TEXT = 'text/plain;charset=utf-16' +PYTHON = 'python' +if IS_WIN: + PYTHON = 'python.exe' CALC = 'calc' WRITER = 'writer' OBJ_CELL = 'ScCellObj' @@ -133,9 +154,15 @@ TYPE_DOC = { 'base': 'com.sun.star.sdb.DocumentDataSource', 'math': 'com.sun.star.formula.FormulaProperties', 'basic': 'com.sun.star.script.BasicIDE', + 'main': 'com.sun.star.frame.StartModule', } NODE_MENUBAR = 'private:resource/menubar/menubar' +MENUS_MAIN = { + 'file': '.uno:PickList', + 'tools': '.uno:ToolsMenu', + 'help': '.uno:HelpMenu', +} MENUS_CALC = { 'file': '.uno:PickList', 'edit': '.uno:EditMenu', @@ -164,6 +191,7 @@ MENUS_WRITER = { } MENUS_APP = { + 'main': MENUS_MAIN, 'calc': MENUS_CALC, 'writer': MENUS_WRITER, } @@ -274,16 +302,17 @@ def info(data): return -def debug(info): +def debug(*info): if IS_WIN: doc = get_document(FILE_NAME_DEBUG) if doc is None: return doc = LogWin(doc.obj) - doc.write(info) + doc.write(str(info)) return - log.debug(str(info)) + data = [str(d) for d in info] + log.debug('\t'.join(data)) return @@ -694,10 +723,117 @@ class LODocument(object): return path_pdf +class FormControlBase(object): + EVENTS = { + 'action': 'actionPerformed', + 'click': 'mousePressed', + } + TYPES = { + 'actionPerformed': 'XActionListener', + 'mousePressed': 'XMouseListener', + } + + def __init__(self, obj): + self._obj = obj + self._index = -1 + self._rules = {} + + @property + def obj(self): + return self._obj + + @property + def name(self): + return self.obj.Name + + @property + def form(self): + return self.obj.getParent() + + @property + def index(self): + return self._index + @index.setter + def index(self, value): + self._index = value + + @property + def events(self): + return self.form.getScriptEvents(self.index) + + def remove_event(self, name=''): + for ev in self.events: + if name and \ + ev.EventMethod == self.EVENTS[name] and \ + ev.ListenerType == self.TYPES[ev.EventMethod]: + self.form.revokeScriptEvent(self.index, + ev.ListenerType, ev.EventMethod, ev.AddListenerParam) + break + else: + self.form.revokeScriptEvent(self.index, + ev.ListenerType, ev.EventMethod, ev.AddListenerParam) + return + + def add_event(self, name, macro): + if not 'name' in macro: + macro['name'] = '{}_{}'.format(self.name, name) + + event = ScriptEventDescriptor() + event.AddListenerParam = '' + event.EventMethod = self.EVENTS[name] + event.ListenerType = self.TYPES[event.EventMethod] + event.ScriptCode = _get_url_script(macro) + event.ScriptType = 'Script' + + for ev in self.events: + if ev.EventMethod == event.EventMethod and \ + ev.ListenerType == event.ListenerType: + self.form.revokeScriptEvent(self.index, + event.ListenerType, event.EventMethod, event.AddListenerParam) + break + + self.form.registerScriptEvent(self.index, event) + return + + +class FormButton(FormControlBase): + + def __init__(self, obj): + super().__init__(obj) + + + class LOForm(ObjectBase): def __init__(self, obj): super().__init__(obj) + self._init_controls() + + def __getitem__(self, index): + if isinstance(index, int): + return self._controls[index] + else: + return getattr(self, index) + + def _get_type_control(self, name): + types = { + # ~ 'stardiv.Toolkit.UnoFixedTextControl': 'label', + 'com.sun.star.form.OButtonModel': 'formbutton', + # ~ 'stardiv.Toolkit.UnoEditControl': 'text', + # ~ 'stardiv.Toolkit.UnoRoadmapControl': 'roadmap', + # ~ 'stardiv.Toolkit.UnoFixedHyperlinkControl': 'link', + # ~ 'stardiv.Toolkit.UnoListBoxControl': 'listbox', + } + return types[name] + + def _init_controls(self): + self._controls = [] + for i, c in enumerate(self.obj.ControlModels): + tipo = self._get_type_control(c.ImplementationName) + control = get_custom_class(tipo, c) + control.index = i + self._controls.append(control) + setattr(self, c.Name, control) @property def name(self): @@ -1806,7 +1942,7 @@ class EventsMouse(EventsListenerBase, XMouseListener, XMouseMotionListener): def mousePressed(self, event): event_name = '{}_click'.format(self._name) if event.ClickCount == 2: - event_name = '{}_double_click'.format(name) + event_name = '{}_double_click'.format(self._name) if hasattr(self._controller, event_name): getattr(self._controller, event_name)(event) return @@ -1859,13 +1995,13 @@ class EventsMouseGrid(EventsMouse): return def mouseReleased(self, event): - obj = event.Source - col = obj.getColumnAtPoint(event.X, event.Y) - row = obj.getRowAtPoint(event.X, event.Y) - if row == -1 and col > -1: - gdm = obj.Model.GridDataModel - for i in range(gdm.RowCount): - gdm.updateRowHeading(i, i + 1) + # ~ obj = event.Source + # ~ col = obj.getColumnAtPoint(event.X, event.Y) + # ~ row = obj.getRowAtPoint(event.X, event.Y) + # ~ if row == -1 and col > -1: + # ~ gdm = obj.Model.GridDataModel + # ~ for i in range(gdm.RowCount): + # ~ gdm.updateRowHeading(i, i + 1) return @@ -1907,6 +2043,9 @@ class EventsFocus(EventsListenerBase, XFocusListener): super().__init__(controller, name) def focusGained(self, event): + service = event.Source.Model.ImplementationName + if service == 'stardiv.Toolkit.UnoControlListBoxModel': + return obj = event.Source.Model obj.BackgroundColor = COLOR_ON_FOCUS @@ -1923,6 +2062,27 @@ class EventsKey(EventsListenerBase, XKeyListener): event.Modifiers """ + def __init__(self, controller, name): + super().__init__(controller, name) + + def keyPressed(self, event): + pass + + def keyReleased(self, event): + event_name = '{}_key_released'.format(self._name) + if hasattr(self._controller, event_name): + getattr(self._controller, event_name)(event) + return + + +class EventsKeyWindow(EventsListenerBase, XKeyListener): + """ + event.KeyChar + event.KeyCode + event.KeyFunc + event.Modifiers + """ + def __init__(self, cls): super().__init__(cls.events, cls.name) self._cls = cls @@ -2016,7 +2176,6 @@ class EventsMenu(EventsListenerBase, XMenuListener): def itemHighlighted(self, event): pass - @catch_exception def itemSelected(self, event): name = event.Source.getCommand(event.MenuId) if name.startswith('menu'): @@ -2115,6 +2274,20 @@ class UnoBaseObject(object): def tag(self, value): self.model.Tag = value + @property + def visible(self): + return self.obj.Visible + @visible.setter + def visible(self, value): + self.obj.setVisible(value) + + @property + def enabled(self): + return self.model.Enabled + @enabled.setter + def enabled(self, value): + self.model.Enabled = value + @property def step(self): return self.model.Step @@ -2153,16 +2326,17 @@ class UnoBaseObject(object): return def move(self, origin, x=0, y=5): - w = 0 - h = 0 if x: - w = origin.width + self.x = origin.x + origin.width + x if y: - h = origin.height - x = origin.x + x + w - y = origin.y + y + h - self.x = x - self.y = y + self.y = origin.y + origin.height + y + return + + def possize(self, origin): + self.x = origin.x + self.y = origin.y + self.width = origin.width + self.height = origin.height return @@ -2235,7 +2409,6 @@ class UnoListBox(UnoBaseObject): def __init__(self, obj): super().__init__(obj) - self._data = [] @property def type(self): @@ -2245,13 +2418,49 @@ class UnoListBox(UnoBaseObject): def value(self): return self.obj.SelectedItem + @property + def count(self): + return len(self.data) + @property def data(self): - return self._data + return self.model.StringItemList @data.setter def data(self, values): - self._data = list(sorted(values)) - self.model.StringItemList = self.data + self.model.StringItemList = list(sorted(values)) + return + + def select(self, pos=0): + if isinstance(pos, str): + self.obj.selectItem(pos, True) + else: + self.obj.selectItemPos(pos, True) + return + + def clear(self): + self.obj.removeItems(0, self.count) + return + + def _set_image_url(self, image): + if exists_path(image): + return _path_url(image) + + if not ID_EXTENSION: + return '' + + path = get_path_extension(ID_EXTENSION) + path = join(path, DIR['images'], image) + return _path_url(path) + + def insert(self, value, path='', pos=-1, show=True): + if pos < 0: + pos = self.count + if path: + self.model.insertItem(pos, value, self._set_image_url(path)) + else: + self.model.insertItemText(pos, value) + if show: + self.select(pos) return @@ -2386,6 +2595,15 @@ class UnoRoadmap(UnoBaseObject): self.model.insertByIndex(i, opt) return + @property + def enabled(self): + return True + @enabled.setter + def enabled(self, value): + for m in self.model: + m.Enabled = value + return + def set_enabled(self, index, value): self.model.getByIndex(index).Enabled = value return @@ -2405,6 +2623,7 @@ def get_custom_class(tipo, obj): # ~ 'radio': UnoRadio, # ~ 'groupbox': UnoGroupBox, # ~ 'tree': UnoTree, + 'formbutton': FormButton, } return classes[tipo](obj) @@ -2415,6 +2634,7 @@ def add_listeners(events, control, name=''): 'addMouseListener': EventsMouse, 'addItemListener': EventsItem, 'addFocusListener': EventsFocus, + 'addKeyListener': EventsKey, } if hasattr(control, 'obj'): control = contro.obj @@ -2426,7 +2646,7 @@ def add_listeners(events, control, name=''): for key, value in listeners.items(): if hasattr(control, key): if is_grid and key == 'addMouseListener': - control.addMouseListener(EventsMouseGrid(events)) + control.addMouseListener(EventsMouseGrid(events, name)) continue if is_link and key == 'addMouseListener': control.addMouseListener(EventsMouseLink(events, name)) @@ -2434,6 +2654,7 @@ def add_listeners(events, control, name=''): if is_roadmap and key == 'addItemListener': control.addItemListener(EventsItemRoadmap(events, name)) continue + getattr(control, key)(listeners[key](events, name)) return @@ -2886,6 +3107,8 @@ class LODialog(object): self._init_controls() self._events = None self._color_on_focus = -1 + self._id_extension = '' + self._images = 'images' return def _create(self, properties): @@ -2925,6 +3148,7 @@ class LODialog(object): 'stardiv.Toolkit.UnoEditControl': 'text', 'stardiv.Toolkit.UnoRoadmapControl': 'roadmap', 'stardiv.Toolkit.UnoFixedHyperlinkControl': 'link', + 'stardiv.Toolkit.UnoListBoxControl': 'listbox', } return types[name] @@ -2944,6 +3168,22 @@ class LODialog(object): def model(self): return self._model + @property + def id_extension(self): + return self._id_extension + @id_extension.setter + def id_extension(self, value): + global ID_EXTENSION + ID_EXTENSION = value + self._id_extension = value + + @property + def images(self): + return self._images + @images.setter + def images(self, value): + self._images = value + @property def height(self): return self.model.Height @@ -2951,6 +3191,13 @@ class LODialog(object): def height(self, value): self.model.Height = value + @property + def width(self): + return self.model.Width + @width.setter + def width(self, value): + self.model.Width = value + @property def color_on_focus(self): return self._color_on_focus @@ -3012,10 +3259,16 @@ class LODialog(object): column_model.addColumn(grid_column) return column_model - def _set_image_url(self, path): - if exists_path(path): - return _path_url(path) - return '' + def _set_image_url(self, image): + if exists_path(image): + return _path_url(image) + + if not self.id_extension: + return '' + + path = get_path_extension(self.id_extension) + path = join(path, self.images, image) + return _path_url(path) def _special_properties(self, tipo, properties): columns = properties.pop('Columns', ()) @@ -3023,8 +3276,11 @@ class LODialog(object): properties['ColumnModel'] = self._set_column_model(columns) elif tipo == 'button' and 'ImageURL' in properties: properties['ImageURL'] = self._set_image_url(properties['ImageURL']) - elif tipo == 'roadmap' and not 'Height' in properties: - properties['Height'] = self.height + elif tipo == 'roadmap': + if not 'Height' in properties: + properties['Height'] = self.height + if 'Title' in properties: + properties['Text'] = properties.pop('Title') return properties def add_control(self, properties): @@ -3040,6 +3296,32 @@ class LODialog(object): setattr(self, name, control) return + def center(self, control, x=0, y=0): + w = self.width + h = self.height + + if isinstance(control, tuple): + wt = SEPARATION * -1 + for c in control: + wt += c.width + SEPARATION + x = w / 2 - wt / 2 + for c in control: + c.x = x + x = c.x + c.width + SEPARATION + return + + if x < 0: + x = w + x - control.width + elif x == 0: + x = w / 2 - control.width / 2 + if y < 0: + y = h + y - control.height + elif y == 0: + y = h / 2 - control.height / 2 + control.x = x + control.y = y + return + class LOWindow(object): @@ -3168,7 +3450,7 @@ class LOWindow(object): controller = EventsWindow(self) self._window.addTopWindowListener(controller) self._window.addWindowListener(controller) - self._container.addKeyListener(EventsKey(self)) + self._container.addKeyListener(EventsKeyWindow(self)) return @property @@ -3287,6 +3569,11 @@ def get_config_path(name='Work'): return _path_system(getattr(path, name)) +def get_path_python(): + path = get_config_path('Module') + return join(path, PYTHON) + + # ~ Export ok def get_file(init_dir='', multiple=False, filters=()): """ @@ -3541,7 +3828,7 @@ def open_doc(path, **kwargs): """ path = _path_url(path) opt = dict_to_property(kwargs) - doc = get_desktop().loadComponentFromURL(path, '_blank', 0, opt) + doc = get_desktop().loadComponentFromURL(path, '_default', 0, opt) if doc is None: return @@ -3595,13 +3882,38 @@ def zip_content(path): return names +def popen(command, stdin=None): + try: + proc = subprocess.Popen(shlex.split(command), shell=IS_WIN, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + for line in proc.stdout: + yield line.decode().rstrip() + except Exception as e: + error(e) + yield (e.errno, e.strerror) + + +def url_open(url, options={}, json=False): + data = '' + req = Request(url) + try: + response = urlopen(req) + except HTTPError as e: + error(e) + except URLError as e: + error(e.reason) + else: + if json: + data = json_loads(response.read()) + else: + data = response.read() + + return data + + def run(command, wait=False): - # ~ debug(command) - # ~ debug(shlex.split(command)) try: if wait: - # ~ p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) - # ~ p.wait() result = subprocess.check_output(command, shell=True) else: p = subprocess.Popen(shlex.split(command), stdin=None, @@ -4216,6 +4528,24 @@ def format(template, data): return result +def _get_url_script(macro): + macro['language'] = macro.get('language', 'Python') + macro['location'] = macro.get('location', 'user') + data = macro.copy() + if data['language'] == 'Python': + data['module'] = '.py$' + elif data['language'] == 'Basic': + data['module'] = '.{}.'.format(macro['module']) + if macro['location'] == 'user': + data['location'] = 'application' + else: + data['module'] = '.' + + url = 'vnd.sun.star.script:{library}{module}{name}?language={language}&location={location}' + path = url.format(**data) + return path + + def _call_macro(macro): #~ https://wiki.openoffice.org/wiki/Documentation/DevGuide/Scripting/Scripting_Framework_URI_Specification name = 'com.sun.star.script.provider.MasterScriptProviderFactory' @@ -4236,6 +4566,7 @@ def _call_macro(macro): args = macro.get('args', ()) url = 'vnd.sun.star.script:{library}{module}{name}?language={language}&location={location}' path = url.format(**data) + script = factory.createScriptProvider('').getScript(path) return script.invoke(args, None, None)[0] @@ -4462,6 +4793,7 @@ def import_csv(path, **kwargs): rows = tuple(csv.reader(f, **kwargs)) return rows + def export_csv(path, data, **kwargs): with open(path, 'w') as f: writer = csv.writer(f, **kwargs) @@ -4469,6 +4801,19 @@ def export_csv(path, data, **kwargs): return +def install_locales(path, domain='base', dir_locales=DIR['locales']): + p, *_ = get_info_path(path) + path_locales = join(p, dir_locales) + try: + lang = gettext.translation(domain, path_locales, languages=[LANG]) + lang.install() + _ = lang.gettext + except Exception as e: + from gettext import gettext as _ + error(e) + return _ + + class LIBOServer(object): HOST = 'localhost' PORT = '8100' @@ -4548,12 +4893,9 @@ class LIBOServer(object): # ~ 'CurrencyField': 'com.sun.star.awt.UnoControlCurrencyFieldModel', # ~ 'DateField': 'com.sun.star.awt.UnoControlDateFieldModel', # ~ 'FileControl': 'com.sun.star.awt.UnoControlFileControlModel', - # ~ 'FixedLine': 'com.sun.star.awt.UnoControlFixedLineModel', - # ~ 'FixedText': 'com.sun.star.awt.UnoControlFixedTextModel', # ~ 'FormattedField': 'com.sun.star.awt.UnoControlFormattedFieldModel', # ~ 'GroupBox': 'com.sun.star.awt.UnoControlGroupBoxModel', # ~ 'ImageControl': 'com.sun.star.awt.UnoControlImageControlModel', - # ~ 'ListBox': 'com.sun.star.awt.UnoControlListBoxModel', # ~ 'NumericField': 'com.sun.star.awt.UnoControlNumericFieldModel', # ~ 'PatternField': 'com.sun.star.awt.UnoControlPatternFieldModel', # ~ 'ProgressBar': 'com.sun.star.awt.UnoControlProgressBarModel', diff --git a/source/images/close.png b/source/images/close.png new file mode 100644 index 0000000000000000000000000000000000000000..22a617be9ec09c5dd3d5b0e39d45c5593545b2f1 GIT binary patch literal 568 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 literal 0 HcmV?d00001 diff --git a/source/images/search-24.png b/source/images/search-24.png new file mode 100644 index 0000000000000000000000000000000000000000..12dea78893e815dbc31e8ddde20e8bce55296e6e GIT binary patch literal 435 zcmV;k0ZjghP)kvomgPwwWuNl zEh|j0bb)~uRCT8yB^`WLgBtvY<|NCO^Sk?evF~8n>OdDrff4X6a|(2Tu*ApN_JB_y zH;fOU^`GEZKqf)%fi@5UH6RvvU%qF+rSHiz;0HLhXwO8K;h0IC#c#0%FG6x)jKiID zMSSNNT-!kW5}Pm;VcQe1FMd6na3sRW6A+8vyG=L~Va*e;E`DDoQ2P^tDkg0BG&;&*Nnhp6(;QSaNu5cnMtHAMBRl`S?v9;*PDAa7Lx{(>A=3DA%y d;1xIlm;>BNgrFaIX8`~J002ovPDHLkV1kNEw^RTC literal 0 HcmV?d00001 diff --git a/source/images/search-48.png b/source/images/search-48.png new file mode 100644 index 0000000000000000000000000000000000000000..18c6b3c946e0644038fff63d7cf860d9cb674fe4 GIT binary patch literal 861 zcmV-j1ETziP)>!u$W|yojsGalU8;eMAw6glpsLC!4{gN^kTDL}3I(W=ILZfG zF$3O<@6v-2E7Xt278fy1T13pBwn9O|vt}%xI&rOqmc_GOzQi+-F_vtF0^qH9w%b88*r>TQLJ<4C%p$)6`F>c^5HF-iVl|T*P(~FC|P4E~dy*q5^Dp6`d5K zb2Mi)3V>%K)?FXMT|%^mI!XEFazdO=U*fW)uHd&S?InJoDSOnXo%l7%0)CR{C4Qt_ z1onE+u490B7gtaxDVsSFpoWcHBx=A6P;|E(XDlID4R{WmG|=fIK4E2>1~&OJ9%VEm z=sYli+Kfu5!mI_@2OJOI%{MSE=Rs3?q90YG=o)F%7E!~dQ4-tm*eh+Qs}EQ6Wz>W% zqSpPS-^}pusQCl9;42AGS081PCu3Txgl6SBpOr+k{RT#SCIRa9UhuJi) zVAm7p@-Umm2-kg+|J2kLv)5y6*!oeK^+?P#MS()>64U16}5z} n#U->C&H^7%6ZQ^pDEjgbeftf632sA400000NkvXXu0mjf#c_rD literal 0 HcmV?d00001 diff --git a/source/images/uninstalling-32.png b/source/images/uninstalling-32.png new file mode 100644 index 0000000000000000000000000000000000000000..0af9b7e7fe752fcff036e80f85f8747d62b9dd42 GIT binary patch literal 318 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzwj^(N7l!{JxM1({$v~0Mo-U3d z5v^~hIdU^PO0>x*O}sNDNKenpYp2DD|F`N7=-)I}X9q%a_NPx2%rxs8Pw(EkPus-n_IdC$g}1|kQT+kSoCZTi<@jwaHn#R(rBCeLXrAn( z@KySUR%5{915D{FKF-*Zapv*;buXny+e4oS;^Uv7);Nq0$`FVdQ&MBb@00O6dMF0Q* literal 0 HcmV?d00001