From 598692003cf40ce482ae5429cc9d9451acf51cb1 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Wed, 18 Sep 2019 21:41:47 -0500 Subject: [PATCH] Add support for modify menus --- CHANGELOG | 3 + VERSION | 2 +- source/conf.py.example | 1 - source/easymacro.py | 385 +++++++++++++++++++++++++++++++++++++---- 4 files changed, 352 insertions(+), 39 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 024eb63..607d777 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +v 0.6.0 [18-sep-2019] + - Add support for modify menus + v 0.5.0 [15-sep-2019] --------------------- - Add support for shortcuts diff --git a/VERSION b/VERSION index 8f0916f..a918a2a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.0 +0.6.0 diff --git a/source/conf.py.example b/source/conf.py.example index e503778..5daa760 100644 --- a/source/conf.py.example +++ b/source/conf.py.example @@ -584,7 +584,6 @@ NODE_SHORTCUT = """ {0} {0} """ - NODE_SHORTCUTS = '' if TYPE_EXTENSION == 1: node_global = [] diff --git a/source/easymacro.py b/source/easymacro.py index 15ea311..8b41c24 100644 --- a/source/easymacro.py +++ b/source/easymacro.py @@ -22,10 +22,13 @@ import ctypes import datetime import errno import getpass +import json import logging import os import platform +import re import shlex +import shutil import subprocess import sys import tempfile @@ -33,6 +36,8 @@ import threading import time import zipfile +from collections import OrderedDict +from collections.abc import MutableMapping from datetime import datetime from functools import wraps from pathlib import Path, PurePath @@ -65,26 +70,18 @@ MSG_LANG = { } -FILE_NAME_DEBUG = 'zaz-debug.log' -LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' -LOG_DATE = '%d/%m/%Y %H:%M:%S' -logging.addLevelName(logging.ERROR, '\033[1;41mERROR\033[1;0m') -logging.addLevelName(logging.DEBUG, '\x1b[33mDEBUG\033[1;0m') -logging.addLevelName(logging.INFO, '\x1b[32mINFO\033[1;0m') -logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE) -log = logging.getLogger(__name__) - - OS = platform.system() USER = getpass.getuser() PC = platform.node() DESKTOP = os.environ.get('DESKTOP_SESSION', '') INFO_DEBUG = '{}\n\n{}\n\n{}'.format(sys.version, platform.platform(), '\n'.join(sys.path)) + IS_WIN = OS == 'Windows' LOG_NAME = 'ZAZ' CLIPBOARD_FORMAT_TEXT = 'text/plain;charset=utf-16' + CALC = 'calc' WRITER = 'writer' OBJ_CELL = 'ScCellObj' @@ -92,6 +89,46 @@ OBJ_RANGE = 'ScCellRangeObj' OBJ_RANGES = 'ScCellRangesObj' OBJ_TYPE_RANGES = (OBJ_CELL, OBJ_RANGE, OBJ_RANGES) +TYPE_DOC = { + 'calc': 'com.sun.star.sheet.SpreadsheetDocument', + 'writer': 'com.sun.star.text.TextDocument', + 'impress': 'com.sun.star.presentation.PresentationDocument', + 'draw': 'com.sun.star.drawing.DrawingDocument', + 'base': 'com.sun.star.sdb.OfficeDatabaseDocument', + 'math': 'com.sun.star.formula.FormulaProperties', + 'basic': 'com.sun.star.script.BasicIDE', +} + +NODE_MENUBAR = 'private:resource/menubar/menubar' +MENUS_CALC = { + 'file': '.uno:PickList', + 'edit': '.uno:EditMenu', + 'view': '.uno:ViewMenu', + 'insert': '.uno:InsertMenu', + 'format': '.uno:FormatMenu', + 'styles': '.uno:FormatStylesMenu', + 'sheet': '.uno:SheetMenu', + 'data': '.uno:DataMenu', + 'tools': '.uno:ToolsMenu', + 'windows': '.uno:WindowList', + 'help': '.uno:HelpMenu', +} + +MENUS_APP = { + 'calc': MENUS_CALC, +} + + +FILE_NAME_DEBUG = 'zaz-debug.log' +FILE_NAME_CONFIG = 'zaz-config.json' +LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' +LOG_DATE = '%d/%m/%Y %H:%M:%S' +logging.addLevelName(logging.ERROR, '\033[1;41mERROR\033[1;0m') +logging.addLevelName(logging.DEBUG, '\x1b[33mDEBUG\033[1;0m') +logging.addLevelName(logging.INFO, '\x1b[32mINFO\033[1;0m') +logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE) +log = logging.getLogger(__name__) + CTX = uno.getComponentContext() SM = CTX.getServiceManager() @@ -105,7 +142,7 @@ def create_instance(name, with_context=False): return instance -def _get_config(key, node_name): +def _get_app_config(key, node_name): name = 'com.sun.star.configuration.ConfigurationProvider' service = 'com.sun.star.configuration.ConfigurationAccess' cp = create_instance(name, True) @@ -120,10 +157,10 @@ def _get_config(key, node_name): return '' -LANGUAGE = _get_config('ooLocale', 'org.openoffice.Setup/L10N/') +LANGUAGE = _get_app_config('ooLocale', 'org.openoffice.Setup/L10N/') LANG = LANGUAGE.split('-')[0] -NAME = TITLE = _get_config('ooName', 'org.openoffice.Setup/Product') -VERSION = _get_config('ooSetupVersion', 'org.openoffice.Setup/Product') +NAME = TITLE = _get_app_config('ooName', 'org.openoffice.Setup/Product') +VERSION = _get_app_config('ooSetupVersion', 'org.openoffice.Setup/Product') def mri(obj): @@ -202,8 +239,34 @@ def run_in_thread(fn): return run -def sleep(sec): - time.sleep(sec) +def get_config(key=''): + values = {} + path = join(get_config_path('UserConfig'), FILE_NAME_CONFIG) + if not exists_path(path): + return values + + with open(path, 'r', encoding='utf-8') as fh: + data = fh.read() + if data: + values = json.loads(data) + + if key: + return values.get(key, None) + + return values + + +def set_config(key, value): + path = join(get_config_path('UserConfig'), FILE_NAME_CONFIG) + values = get_config() + values[key] = value + with open(path, 'w', encoding='utf-8') as fh: + json.dump(values, fh, ensure_ascii=False, sort_keys=True, indent=4) + return True + + +def sleep(seconds): + time.sleep(seconds) return @@ -278,30 +341,116 @@ def exists_app(name): return False return True - +# ~ Delete def exists(path): return Path(path).exists() +def exists_path(path): + return Path(path).exists() def get_type_doc(obj): - services = { - 'calc': 'com.sun.star.sheet.SpreadsheetDocument', - 'writer': 'com.sun.star.text.TextDocument', - 'impress': 'com.sun.star.presentation.PresentationDocument', - 'draw': 'com.sun.star.drawing.DrawingDocument', - 'base': 'com.sun.star.sdb.OfficeDatabaseDocument', - 'math': 'com.sun.star.formula.FormulaProperties', - 'basic': 'com.sun.star.script.BasicIDE', - } - for k, v in services.items(): + # ~ services = { + # ~ 'calc': 'com.sun.star.sheet.SpreadsheetDocument', + # ~ 'writer': 'com.sun.star.text.TextDocument', + # ~ 'impress': 'com.sun.star.presentation.PresentationDocument', + # ~ 'draw': 'com.sun.star.drawing.DrawingDocument', + # ~ 'base': 'com.sun.star.sdb.OfficeDatabaseDocument', + # ~ 'math': 'com.sun.star.formula.FormulaProperties', + # ~ 'basic': 'com.sun.star.script.BasicIDE', + # ~ } + for k, v in TYPE_DOC.items(): if obj.supportsService(v): return k return '' -def _properties(values): - p = [PropertyValue(Name=n, Value=v) for n, v in values.items()] - return tuple(p) +# ~ def _properties(values): + # ~ p = [PropertyValue(Name=n, Value=v) for n, v in values.items()] + # ~ return tuple(p) + +def dict_to_property(values, uno_any=False): + ps = tuple([PropertyValue(Name=n, Value=v) for n, v in values.items()]) + if uno_any: + ps = uno.Any('[]com.sun.star.beans.PropertyValue', ps) + return ps + + +def property_to_dict(values): + d = {i.Name: i.Value for i in values} + return d + + +# ~ Third classes + + +# ~ https://github.com/psf/requests/blob/v2.22.0/requests/structures.py +class CaseInsensitiveDict(MutableMapping): + """A case-insensitive ``dict``-like object. + Implements all methods and operations of + ``MutableMapping`` as well as dict's ``copy``. Also + provides ``lower_items``. + All keys are expected to be strings. The structure remembers the + case of the last key to be set, and ``iter(instance)``, + ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()`` + will contain case-sensitive keys. However, querying and contains + testing is case insensitive:: + cid = CaseInsensitiveDict() + cid['Accept'] = 'application/json' + cid['aCCEPT'] == 'application/json' # True + list(cid) == ['Accept'] # True + For example, ``headers['content-encoding']`` will return the + value of a ``'Content-Encoding'`` response header, regardless + of how the header name was originally stored. + If the constructor, ``.update``, or equality comparison + operations are given keys that have equal ``.lower()``s, the + behavior is undefined. + """ + + def __init__(self, data=None, **kwargs): + self._store = OrderedDict() + if data is None: + data = {} + self.update(data, **kwargs) + + def __setitem__(self, key, value): + # Use the lowercased key for lookups, but store the actual + # key alongside the value. + self._store[key.lower()] = (key, value) + + def __getitem__(self, key): + return self._store[key.lower()][1] + + def __delitem__(self, key): + del self._store[key.lower()] + + def __iter__(self): + return (casedkey for casedkey, mappedvalue in self._store.values()) + + def __len__(self): + return len(self._store) + + def lower_items(self): + """Like iteritems(), but with all lowercase keys.""" + return ( + (lowerkey, keyval[1]) + for (lowerkey, keyval) + in self._store.items() + ) + + def __eq__(self, other): + if isinstance(other, Mapping): + other = CaseInsensitiveDict(other) + else: + return NotImplemented + # Compare insensitively + return dict(self.lower_items()) == dict(other.lower_items()) + + # Copy is required + def copy(self): + return CaseInsensitiveDict(self._store.values()) + + def __repr__(self): + return str(dict(self.items())) # ~ Custom classes @@ -371,7 +520,8 @@ class LODocument(object): return obj def save(self, path='', **kwargs): - opt = _properties(kwargs) + # ~ opt = _properties(kwargs) + opt = dict_to_property(kwargs) if path: self._obj.storeAsURL(_path_url(path), opt) else: @@ -1127,9 +1277,30 @@ def set_properties(model, properties): return -def get_file(filters=(), multiple=False): +def get_config_path(name='Work'): + """ + Return de path name in config + http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1util_1_1XPathSettings.html + """ + path = create_instance('com.sun.star.util.PathSettings') + return _path_system(getattr(path, name)) + + +def get_file(init_dir='', multiple=False, filters=()): + """ + init_folder: folder default open + multiple: True for multiple selected + filters: Example + ( + ('XML', '*.xml'), + ('TXT', '*.txt'), + ) + """ + if not init_dir: + init_dir = get_config_path() file_picker = create_instance('com.sun.star.ui.dialogs.FilePicker') file_picker.setTitle(_('Select file')) + file_picker.setDisplayDirectory(init_dir) file_picker.setMultiSelectionMode(multiple) if filters: @@ -1242,7 +1413,8 @@ def open_doc(path, **kwargs): http://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1document_1_1MediaDescriptor.html """ path = _path_url(path) - opt = _properties(kwargs) + # ~ opt = _properties(kwargs) + opt = dict_to_property(kwargs) doc = get_desktop().loadComponentFromURL(path, '_blank', 0, opt) if doc is None: return @@ -1278,9 +1450,9 @@ def is_created(path): return is_file(path) and bool(get_file_size(path)) -def replace_ext(path, ext): +def replace_ext(path, extension): path, _, name, _ = get_info_path(path) - return '{}/{}.{}'.format(path, name, ext) + return '{}/{}.{}'.format(path, name, extension) def zip_names(path): @@ -1477,8 +1649,8 @@ class TextTransferable(unohelper.Base, XTransferable): return False -def set_clipboard(text): - ts = TextTransferable(text) +def set_clipboard(value): + ts = TextTransferable(value) sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') sc.setContents(ts, None) return @@ -1499,3 +1671,142 @@ def copy(doc=None): def get_epoch(): now = datetime.datetime.now() return int(time.mktime(now.timetuple())) + + +def file_copy(source, target='', name=''): + p, f, n, e = get_info_path(source) + if target: + p = target + if name: + n = name + path_new = join(p, '{}{}'.format(n, e)) + shutil.copy(source, path_new) + return + + +def get_files(path, ext='*'): + docs = [] + for folder, _, files in os.walk(path): + pattern = re.compile(r'\.{}'.format(ext), re.IGNORECASE) + docs += [join(folder, f) for f in files if pattern.search(f)] + return docs + + +def _get_menu(type_doc, name_menu): + instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' + service = TYPE_DOC[type_doc] + manager = create_instance(instance, True) + ui = manager.getUIConfigurationManager(service) + menus = ui.getSettings(NODE_MENUBAR, True) + command = MENUS_APP[type_doc][name_menu] + for menu in menus: + data = property_to_dict(menu) + if data.get('CommandURL', '') == command: + idc = data.get('ItemDescriptorContainer', None) + return ui, menus, idc + return None, None, None + + +def _get_index_menu(menu, command): + for i, m in enumerate(menu): + data = property_to_dict(m) + cmd = data.get('CommandURL', '') + if cmd == command: + return i + # ~ submenu = data.get('ItemDescriptorContainer', None) + # ~ if not submenu is None: + # ~ get_index_menu(submenu, command, count + 1) + return 0 + + +def _store_menu(ui, menus, menu, index, data=(), remove=False): + if remove: + uno.invoke(menu, 'removeByIndex', (index,)) + else: + properties = dict_to_property(data, True) + uno.invoke(menu, 'insertByIndex', (index + 1, properties)) + ui.replaceSettings(NODE_MENUBAR, menus) + ui.store() + return + + +def insert_menu(type_doc, name_menu, **kwargs): + ui, menus, menu = _get_menu(type_doc, name_menu.lower()) + if menu is None: + return 0 + + label = kwargs.get('Label', '-') + separator = False + if label == '-': + separator = True + command = kwargs.get('Command', '') + index = kwargs.get('Index', 0) + if not index: + index = _get_index_menu(menu, kwargs['After']) + if separator: + data = {'Type': 1} + _store_menu(ui, menus, menu, index, data) + return index + 1 + + index_menu = _get_index_menu(menu, command) + if index_menu: + msg = 'Exists: %s' % command + debug(msg) + return 0 + + sub_menu = kwargs.get('Submenu', ()) + idc = None + if sub_menu: + idc = ui.createSettings() + + data = { + 'CommandURL': command, + 'Label': label, + 'Style': 0, + 'Type': 0, + 'ItemDescriptorContainer': idc + } + _store_menu(ui, menus, menu, index, data) + if sub_menu: + _add_sub_menus(ui, menus, idc, sub_menu) + return True + + +def _add_sub_menus(ui, menus, menu, sub_menu): + for i, sm in enumerate(sub_menu): + submenu = sm.pop('Submenu', ()) + sm['Type'] = 0 + if submenu: + idc = ui.createSettings() + sm['ItemDescriptorContainer'] = idc + _store_menu(ui, menus, menu, i - 1, sm) + if submenu: + _add_sub_menus(ui, menus, idc, submenu) + return + + +def remove_menu(type_doc, name_menu, command): + ui, menus, menu = _get_menu(type_doc, name_menu.lower()) + if menu is None: + return False + + index = _get_index_menu(menu, command) + if not index: + debug('Not exists: %s' % command) + return False + + _store_menu(ui, menus, menu, index, remove=True) + return True + + +# ~ name = 'com.sun.star.configuration.ConfigurationProvider' +# ~ cp = create_instance(name, True) +# ~ node = PropertyValue(Name='nodepath', Value=NODE_SETTING) +# ~ try: + # ~ cua = cp.createInstanceWithArguments( + # ~ 'com.sun.star.configuration.ConfigurationUpdateAccess', (node,)) + # ~ cua.setPropertyValue(key, json.dumps(value)) + # ~ cua.commitChanges() +# ~ except Exception as e: + # ~ log.error(e, exc_info=True) + # ~ return False