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 0000000..22a617b Binary files /dev/null and b/source/images/close.png differ diff --git a/source/images/console-24.png b/source/images/console-24.png new file mode 100644 index 0000000..abd99af Binary files /dev/null and b/source/images/console-24.png differ diff --git a/source/images/error.png b/source/images/error.png new file mode 100644 index 0000000..c76cd5c Binary files /dev/null and b/source/images/error.png differ diff --git a/source/images/install.png b/source/images/install.png new file mode 100644 index 0000000..26ac51f Binary files /dev/null and b/source/images/install.png differ diff --git a/source/images/ok.png b/source/images/ok.png new file mode 100644 index 0000000..9d99c0c Binary files /dev/null and b/source/images/ok.png differ diff --git a/source/images/python.png b/source/images/python.png new file mode 100644 index 0000000..2f0fce7 Binary files /dev/null and b/source/images/python.png differ diff --git a/source/images/python_48.png b/source/images/python_48.png new file mode 100644 index 0000000..d068a0d Binary files /dev/null and b/source/images/python_48.png differ diff --git a/source/images/question.png b/source/images/question.png new file mode 100644 index 0000000..a279b91 Binary files /dev/null and b/source/images/question.png differ diff --git a/source/images/search-24.png b/source/images/search-24.png new file mode 100644 index 0000000..12dea78 Binary files /dev/null and b/source/images/search-24.png differ diff --git a/source/images/search-48.png b/source/images/search-48.png new file mode 100644 index 0000000..18c6b3c Binary files /dev/null and b/source/images/search-48.png differ diff --git a/source/images/uninstalling-32.png b/source/images/uninstalling-32.png new file mode 100644 index 0000000..0af9b7e Binary files /dev/null and b/source/images/uninstalling-32.png differ