diff --git a/CHANGELOG b/CHANGELOG index 607d777..2332ccf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,22 +1,31 @@ +v 0.7.0 [27-sep-2019] + - Add support for InputBox and documents + + v 0.6.0 [18-sep-2019] - Add support for modify menus + v 0.5.0 [15-sep-2019] --------------------- - Add support for shortcuts + v 0.4.0 [14-sep-2019] --------------------- - Add support for locales + v 0.3.0 [10-sep-2019] --------------------- - Add support for dialogs + v 0.2.0 [09-sep-2019] --------------------- - Add support for context in menus + v 0.1.0 [06-sep-2019] --------------------- - Initial version diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 45e2060..25ce94a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,3 +15,4 @@ **ETH**: `0x61a4f614a30ff686445751ed8328b82b77ecfc69` +PayPal :( donate ATT elmau DOT net diff --git a/README.md b/README.md index 33a6766..6735a4b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Develop in pure Python, not need any dependence. For Python 3.6+ -* See [documentation](https://gitlab.com/mauriciobaeza/zaz/wikis/home) +* Look [documentation](https://gitlab.com/mauriciobaeza/zaz/wikis/home) * Ver [documentación](https://gitlab.com/mauriciobaeza/zaz/wikis/inicio) @@ -17,6 +17,8 @@ BCH: `1RPLWHJW34p7pMQV1ft4x7eWhAYw69Dsb` BTC: `3Fe4JuADrAK8Qs7GDAxbSXR8E54avwZJLW` -## Extension develop with ZAZ +## Extensions develop with ZAZ * https://gitlab.com/mauriciobaeza/zaz-barcode +* https://gitlab.com/mauriciobaeza/zaz-favorite +* https://gitlab.com/mauriciobaeza/zaz-easymacro diff --git a/TODO.md b/TODO.md index 3d28553..1db343e 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,4 @@ -* Automatic update * Help -* Configuration * Option panel * Sub-menus -* Panel lateral +* Lateral panel diff --git a/VERSION b/VERSION index a918a2a..faef31a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.0 +0.7.0 diff --git a/source/conf.py.example b/source/conf.py.example index 5daa760..9e4f591 100644 --- a/source/conf.py.example +++ b/source/conf.py.example @@ -393,7 +393,7 @@ NODE_MENUS = '' if TYPE_EXTENSION == 1: if PARENT == 'AddonMenu': NODE_MENUS = '\n'.join(menus) - else: + elif PARENT == 'OfficeMenuBar': tmp = ' {}' titles = '\n'.join([tmp.format(k, v) for k, v in MENU_MAIN.items()]) SUBMENUS = '\n ' + '\n'.join(menus) + '\n ' diff --git a/source/easymacro.py b/source/easymacro.py index 8b41c24..0375c5b 100644 --- a/source/easymacro.py +++ b/source/easymacro.py @@ -17,11 +17,12 @@ # ~ You should have received a copy of the GNU General Public License # ~ along with ZAZ. If not, see . - +import base64 import ctypes import datetime import errno import getpass +import hashlib import json import logging import os @@ -34,18 +35,30 @@ import sys import tempfile import threading import time +import traceback import zipfile from collections import OrderedDict from collections.abc import MutableMapping -from datetime import datetime from functools import wraps +from operator import itemgetter from pathlib import Path, PurePath from pprint import pprint +from string import Template from subprocess import PIPE +import smtplib +from smtplib import SMTPException, SMTPAuthenticationError +from email.mime.multipart import MIMEMultipart +from email.mime.base import MIMEBase +from email.mime.text import MIMEText +from email.utils import formatdate +from email import encoders +import mailbox + import uno import unohelper +from com.sun.star.util import Time, Date, DateTime from com.sun.star.beans import PropertyValue from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS from com.sun.star.awt.MessageBoxResults import YES @@ -60,12 +73,20 @@ from com.sun.star.lang import XEventListener from com.sun.star.awt import XActionListener from com.sun.star.awt import XMouseListener +try: + from fernet import Fernet, InvalidToken + CRYPTO = True +except ImportError: + CRYPTO = False + MSG_LANG = { 'es': { 'OK': 'Aceptar', 'Cancel': 'Cancelar', 'Select file': 'Seleccionar archivo', + 'Incorrect user or password': 'Nombre de usuario o contraseña inválidos', + 'Allow less secure apps in GMail': 'Activa: Permitir aplicaciones menos segura en GMail', } } @@ -94,7 +115,8 @@ TYPE_DOC = { 'writer': 'com.sun.star.text.TextDocument', 'impress': 'com.sun.star.presentation.PresentationDocument', 'draw': 'com.sun.star.drawing.DrawingDocument', - 'base': 'com.sun.star.sdb.OfficeDatabaseDocument', + # ~ 'base': 'com.sun.star.sdb.OfficeDatabaseDocument', + 'base': 'com.sun.star.sdb.DocumentDataSource', 'math': 'com.sun.star.formula.FormulaProperties', 'basic': 'com.sun.star.script.BasicIDE', } @@ -113,14 +135,33 @@ MENUS_CALC = { 'windows': '.uno:WindowList', 'help': '.uno:HelpMenu', } +MENUS_WRITER = { + 'file': '.uno:PickList', + 'edit': '.uno:EditMenu', + 'view': '.uno:ViewMenu', + 'insert': '.uno:InsertMenu', + 'format': '.uno:FormatMenu', + 'styles': '.uno:FormatStylesMenu', + 'sheet': '.uno:TableMenu', + 'data': '.uno:FormatFormMenu', + 'tools': '.uno:ToolsMenu', + 'windows': '.uno:WindowList', + 'help': '.uno:HelpMenu', +} MENUS_APP = { 'calc': MENUS_CALC, + 'writer': MENUS_WRITER, } -FILE_NAME_DEBUG = 'zaz-debug.log' -FILE_NAME_CONFIG = 'zaz-config.json' +EXT = { + 'pdf': 'pdf', +} + + +FILE_NAME_DEBUG = 'debug.odt' +FILE_NAME_CONFIG = 'zaz-{}.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') @@ -130,10 +171,16 @@ logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE) log = logging.getLogger(__name__) +_start = 0 +_stop_thread = {} +TIMEOUT = 10 + + CTX = uno.getComponentContext() SM = CTX.getServiceManager() +# ~ Export ok def create_instance(name, with_context=False): if with_context: instance = SM.createInstanceWithContext(name, CTX) @@ -163,6 +210,7 @@ NAME = TITLE = _get_app_config('ooName', 'org.openoffice.Setup/Product') VERSION = _get_app_config('ooSetupVersion', 'org.openoffice.Setup/Product') +# ~ Export ok def mri(obj): m = create_instance('mytools.Mri') if m is None: @@ -180,7 +228,10 @@ def catch_exception(f): try: return f(*args, **kwargs) except Exception as e: - log.error(f.__name__, exc_info=True) + name = f.__name__ + if IS_WIN: + debug(traceback.format_exc()) + log.error(name, exc_info=True) return func @@ -188,30 +239,30 @@ class LogWin(object): def __init__(self, doc): self.doc = doc + self.doc.Title = FILE_NAME_DEBUG def write(self, info): text = self.doc.Text cursor = text.createTextCursor() cursor.gotoEnd(False) - text.insertString(cursor, str(info), 0) + text.insertString(cursor, str(info) + '\n\n', 0) return +# ~ Export ok def info(data): log.info(data) return +# ~ Export ok def debug(info): if IS_WIN: - # ~ app = LOApp(self.ctx, self.sm, self.desktop, self.toolkit) - # ~ doc = app.getDoc(FILE_NAME_DEBUG) - # ~ if not doc: - # ~ doc = app.newDoc(WRITER) - # ~ out = OutputDoc(doc) - # ~ sys.stdout = out - # ~ pprint(info) - doc = LogWin(new_doc('writer').obj) + doc = get_document(FILE_NAME_DEBUG) + if doc is None: + # ~ doc = new_doc('writer') + return + doc = LogWin(doc.obj) doc.write(info) return @@ -219,14 +270,16 @@ def debug(info): return +# ~ Export ok def error(info): log.error(info) return +# ~ Export ok def save_log(path, data): with open(path, 'a') as out: - out.write('{} -{}- '.format(str(datetime.now())[:19], LOG_NAME)) + out.write('{} -{}- '.format(str(now())[:19], LOG_NAME)) pprint(data, stream=out) return @@ -239,32 +292,40 @@ def run_in_thread(fn): return run -def get_config(key=''): - values = {} - path = join(get_config_path('UserConfig'), FILE_NAME_CONFIG) +def now(): + return datetime.datetime.now() + + +# ~ Export ok +def get_config(key='', default=None, prefix='config'): + path_json = FILE_NAME_CONFIG.format(prefix) + values = None + path = join(get_config_path('UserConfig'), path_json) if not exists_path(path): - return values + return default with open(path, 'r', encoding='utf-8') as fh: data = fh.read() - if data: - values = json.loads(data) + values = json.loads(data) if key: - return values.get(key, None) + return values.get(key, default) return values -def set_config(key, value): - path = join(get_config_path('UserConfig'), FILE_NAME_CONFIG) - values = get_config() +# ~ Export ok +def set_config(key, value, prefix='config'): + path_json = FILE_NAME_CONFIG.format(prefix) + path = join(get_config_path('UserConfig'), path_json) + values = get_config(default={}, prefix=prefix) 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 + return +# ~ Export ok def sleep(seconds): time.sleep(seconds) return @@ -281,6 +342,7 @@ def _(msg): return MSG_LANG[L][msg] +# ~ Export ok def msgbox(message, title=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, type_msg='infobox'): """ Create message box type_msg: infobox, warningbox, errorbox, querybox, messbox @@ -292,15 +354,18 @@ def msgbox(message, title=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, type_msg='infob return mb.execute() +# ~ Export ok def question(message, title=TITLE): res = msgbox(message, title, MSG_BUTTONS.BUTTONS_YES_NO, 'querybox') return res == YES +# ~ Export ok def warning(message, title=TITLE): return msgbox(message, title, type_msg='warningbox') +# ~ Export ok def errorbox(message, title=TITLE): return msgbox(message, title, type_msg='errorbox') @@ -309,10 +374,20 @@ def get_desktop(): return create_instance('com.sun.star.frame.Desktop', True) +# ~ Export ok def get_dispatch(): return create_instance('com.sun.star.frame.DispatchHelper') +# ~ Export ok +def call_dispatch(url, args=()): + frame = get_document().frame + dispatch = get_dispatch() + dispatch.executeDispatch(frame, url, '', 0, args) + return + + +# ~ Export ok def get_temp_file(): delete = True if IS_WIN: @@ -332,6 +407,7 @@ def _path_system(path): return path +# ~ Export ok def exists_app(name): try: dn = subprocess.DEVNULL @@ -341,33 +417,20 @@ def exists_app(name): return False return True -# ~ Delete -def exists(path): - return Path(path).exists() + +# ~ Export ok def exists_path(path): return Path(path).exists() +# ~ Export ok 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 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 dict_to_property(values, uno_any=False): ps = tuple([PropertyValue(Name=n, Value=v) for n, v in values.items()]) if uno_any: @@ -380,82 +443,12 @@ def property_to_dict(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())) +def array_to_dict(values): + d = {r[0]: r[1] for r in values} + return d # ~ Custom classes - - class LODocument(object): def __init__(self, obj): @@ -464,7 +457,10 @@ class LODocument(object): def _init_values(self): self._type_doc = get_type_doc(self.obj) - self._cc = self.obj.getCurrentController() + if self._type_doc == 'base': + self._cc = self.obj.DatabaseDocument.getCurrentController() + else: + self._cc = self.obj.getCurrentController() return @property @@ -499,6 +495,10 @@ class LODocument(object): def path(self): return _path_system(self.obj.getURL()) + @property + def statusbar(self): + return self._cc.getStatusIndicator() + @property def visible(self): w = self._cc.getFrame().getContainerWindow() @@ -543,6 +543,31 @@ class LODocument(object): self._cc.insertTransferable(transferable) return self.obj.getCurrentSelection() + @catch_exception + def to_pdf(self, path, **kwargs): + path_pdf = path + if path: + if is_dir(path): + _, _, n, _ = get_info_path(self.path) + path_pdf = join(path, '{}.{}'.format(n, EXT['pdf'])) + else: + path_pdf = replace_ext(self.path, EXT['pdf']) + + filter_name = '{}_pdf_Export'.format(self.type) + filter_data = dict_to_property(kwargs, True) + args = { + 'FilterName': filter_name, + 'FilterData': filter_data, + } + args = dict_to_property(args) + try: + self.obj.storeToURL(_path_url(path_pdf), args) + except Exception as e: + error(e) + path_pdf = '' + + return path_pdf + class LOCalc(LODocument): @@ -913,6 +938,34 @@ class EventsMouse(EventsListenerBase, XMouseListener): pass +class EventsMouseGrid(EventsMouse): + selected = False + + def mousePressed(self, event): + super().mousePressed(event) + # ~ obj = event.Source + # ~ col = obj.getColumnAtPoint(event.X, event.Y) + # ~ row = obj.getRowAtPoint(event.X, event.Y) + # ~ print(col, row) + # ~ if col == -1 and row == -1: + # ~ if self.selected: + # ~ obj.deselectAllRows() + # ~ else: + # ~ obj.selectAllRows() + # ~ self.selected = not self.selected + 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) + return + + class UnoBaseObject(object): def __init__(self, obj): @@ -1020,6 +1073,10 @@ class UnoLabel(UnoBaseObject): def __init__(self, obj): super().__init__(obj) + @property + def type(self): + return 'label' + @property def value(self): return self.model.Label @@ -1032,16 +1089,10 @@ class UnoButton(UnoBaseObject): def __init__(self, obj): super().__init__(obj) - # ~ self._set_icon() - def _set_icon(self): - icon_name = self.tag.strip() - if icon_name: - path_icon = _file_url('{}/img/{}'.format(CURRENT_PATH, icon_name)) - self._model.ImageURL = path_icon - if self.value: - self._model.ImageAlign = 0 - return + @property + def type(self): + return 'button' @property def value(self): @@ -1056,6 +1107,10 @@ class UnoText(UnoBaseObject): def __init__(self, obj): super().__init__(obj) + @property + def type(self): + return 'text' + @property def value(self): return self.model.Text @@ -1088,6 +1143,118 @@ class UnoListBox(UnoBaseObject): return +class UnoGrid(UnoBaseObject): + + def __init__(self, obj): + super().__init__(obj) + self._gdm = self._model.GridDataModel + # ~ self._data = [] + self._columns = {} + # ~ self._format_columns = () + + def __getitem__(self, index): + value = self._gdm.getCellData(index[0], index[1]) + return value + + @property + def type(self): + return 'grid' + + def _format_cols(self): + rows = tuple(tuple( + self._format_columns[i].format(r) for i, r in enumerate(row)) for row in self._data + ) + return rows + + # ~ @property + # ~ def format_columns(self): + # ~ return self._format_columns + # ~ @format_columns.setter + # ~ def format_columns(self, value): + # ~ self._format_columns = value + + @property + def data(self): + return self._data + @data.setter + def data(self, values): + # ~ self._data = values + self._gdm.removeAllRows() + headings = tuple(range(1, len(values) + 1)) + self._gdm.addRows(headings, values) + + # ~ rows = range(grid_dm.RowCount) + # ~ colors = [COLORS['GRAY'] if r % 2 else COLORS['WHITE'] for r in rows] + # ~ grid.Model.RowBackgroundColors = tuple(colors) + return + + @property + def row(self): + return self.obj.CurrentRow + + @property + def rows(self): + return self._gdm.RowCount + + @property + def column(self): + return self.obj.CurrentColumn + + @property + def columns(self): + return self._gdm.ColumnCount + + def set_cell_tooltip(self, col, row, value): + self._gdm.updateCellToolTip(col, row, value) + return + + def get_cell_tooltip(self, col, row): + value = self._gdm.getCellToolTip(col, row) + return value + + def _validate_column(self, data): + row = [] + for i, d in enumerate(data): + if i in self._columns: + if 'image' in self._columns[i]: + row.append(self._columns[i]['image']) + else: + row.append(d) + return tuple(row) + + def add_row(self, data): + # ~ self._data.append(data) + data = self._validate_column(data) + self._gdm.addRow(self.rows + 1, data) + return + + def remove_row(self, row): + self._gdm.removeRow(row) + # ~ del self._data[row] + self._update_row_heading() + return + + def _update_row_heading(self): + for i in range(self.rows): + self._gdm.updateRowHeading(i, i + 1) + return + + def sort(self, column, asc=True): + self._gdm.sortByColumn(column, asc) + # ~ self._data.sort(key=itemgetter(column), reverse=not asc) + self._update_row_heading() + return + + def set_column_image(self, column, path): + gp = create_instance('com.sun.star.graphic.GraphicProvider') + data = dict_to_property({'URL': _path_url(path)}) + image = gp.queryGraphic(data) + if not column in self._columns: + self._columns[column] = {} + self._columns[column]['image'] = image + return + + class LODialog(object): def __init__(self, properties): @@ -1160,6 +1327,9 @@ class LODialog(object): } for key, value in listeners.items(): if hasattr(control.obj, key): + if control.type == 'grid' and key == 'addMouseListener': + control.obj.addMouseListener(EventsMouseGrid(self.events)) + continue getattr(control.obj, key)(listeners[key](self.events)) return @@ -1191,6 +1361,7 @@ class LODialog(object): 'button': UnoButton, 'text': UnoText, 'listbox': UnoListBox, + 'grid': UnoGrid, # ~ 'link': UnoLink, # ~ 'tab': UnoTab, # ~ 'roadmap': UnoRoadmap, @@ -1198,13 +1369,36 @@ class LODialog(object): # ~ 'radio': UnoRadio, # ~ 'groupbox': UnoGroupBox, # ~ 'tree': UnoTree, - # ~ 'grid': UnoGrid, } return classes[tipo](obj) + def _set_column_model(self, columns): + #~ https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1grid_1_1XGridColumn.html + column_model = create_instance('com.sun.star.awt.grid.DefaultGridColumnModel', True) + # ~ column_model.setDefaultColumns(len(columns)) + for column in columns: + grid_column = create_instance('com.sun.star.awt.grid.GridColumn', True) + for k, v in column.items(): + setattr(grid_column, k, v) + column_model.addColumn(grid_column) + # ~ mri(grid_column) + return column_model + + def _set_image_url(self, path): + if exists_path(path): + return _path_url(path) + return '' + @catch_exception def add_control(self, properties): tipo = properties.pop('Type').lower() + + columns = properties.pop('Columns', ()) + if tipo == 'grid': + properties['ColumnModel'] = self._set_column_model(columns) + if tipo == 'button' and 'ImageURL' in properties: + properties['ImageURL'] = self._set_image_url(properties['ImageURL']) + model = self.model.createInstance(self._get_control_model(tipo)) set_properties(model, properties) name = properties['Name'] @@ -1233,14 +1427,35 @@ def _get_class_doc(obj): return classes[type_doc](obj) -def get_document(): +# ~ Export ok +def get_document(title=''): doc = None desktop = get_desktop() - try: + if not title: doc = _get_class_doc(desktop.getCurrentComponent()) - except Exception as e: - log.error(e) - return doc + return doc + + for d in desktop.getComponents(): + if d.Title == title: + doc = d + break + + if doc is None: + return + + return _get_class_doc(doc) + + +# ~ Export ok +def get_documents(custom=True): + docs = [] + desktop = get_desktop() + for doc in desktop.getComponents(): + if custom: + docs.append(_get_class_doc(doc)) + else: + docs.append(doc) + return docs def get_selection(): @@ -1277,6 +1492,7 @@ def set_properties(model, properties): return +# ~ Export ok def get_config_path(name='Work'): """ Return de path name in config @@ -1286,6 +1502,7 @@ def get_config_path(name='Work'): return _path_system(getattr(path, name)) +# ~ Export ok def get_file(init_dir='', multiple=False, filters=()): """ init_folder: folder default open @@ -1298,31 +1515,115 @@ def get_file(init_dir='', multiple=False, filters=()): """ if not init_dir: init_dir = get_config_path() + init_dir = _path_url(init_dir) file_picker = create_instance('com.sun.star.ui.dialogs.FilePicker') file_picker.setTitle(_('Select file')) file_picker.setDisplayDirectory(init_dir) file_picker.setMultiSelectionMode(multiple) + path = '' if filters: file_picker.setCurrentFilter(filters[0][0]) for f in filters: file_picker.appendFilter(f[0], f[1]) if file_picker.execute(): + path = _path_system(file_picker.getSelectedFiles()[0]) if multiple: - return [_path_system(f) for f in file_picker.getSelectedFiles()] - return _path_system(file_picker.getSelectedFiles()[0]) + path = [_path_system(f) for f in file_picker.getSelectedFiles()] - return '' + return path +# ~ Export ok +def get_path(init_dir='', filters=()): + """ + Options: http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1ui_1_1dialogs_1_1TemplateDescription.html + filters: Example + ( + ('XML', '*.xml'), + ('TXT', '*.txt'), + ) + """ + if not init_dir: + init_dir = get_config_path() + init_dir = _path_url(init_dir) + file_picker = create_instance('com.sun.star.ui.dialogs.FilePicker') + file_picker.setTitle(_('Select file')) + file_picker.setDisplayDirectory(init_dir) + file_picker.initialize((2,)) + if filters: + file_picker.setCurrentFilter(filters[0][0]) + for f in filters: + file_picker.appendFilter(f[0], f[1]) + + path = '' + if file_picker.execute(): + path = _path_system(file_picker.getSelectedFiles()[0]) + return path + + +# ~ Export ok +def get_dir(init_dir=''): + folder_picker = create_instance('com.sun.star.ui.dialogs.FolderPicker') + if not init_dir: + init_dir = get_config_path() + init_dir = _path_url(init_dir) + folder_picker.setDisplayDirectory(init_dir) + + path = '' + if folder_picker.execute(): + path = _path_system(folder_picker.getDirectory()) + return path + + +# ~ Export ok def get_info_path(path): path, filename = os.path.split(path) name, extension = os.path.splitext(filename) return (path, filename, name, extension) -def inputbox(message, default='', title=TITLE): +# ~ Export ok +def read_file(path, mode='r', array=False): + data = '' + with open(path, mode) as f: + if array: + data = tuple(f.read().splitlines()) + else: + data = f.read() + return data + + +# ~ Export ok +def save_file(path, mode='w', data=None): + with open(path, mode) as f: + f.write(data) + return + + +# ~ Export ok +def to_json(path, data): + with open(path, 'w') as f: + f.write(json.dumps(data, indent=4, sort_keys=True)) + return + + +# ~ Export ok +def from_json(path): + with open(path) as f: + data = json.loads(f.read()) + return data + + +def get_path_extension(id): + pip = CTX.getValueByName('/singletons/com.sun.star.deployment.PackageInformationProvider') + path = _path_system(pip.getPackageLocation(id)) + return path + + +# ~ Export ok +def inputbox(message, default='', title=TITLE, echochar=''): class ControllersInput(object): @@ -1361,6 +1662,8 @@ def inputbox(message, default='', title=TITLE): 'Width': 190, 'Height': 15, } + if echochar: + args['EchoChar'] = ord(echochar[0]) dlg.add_control(args) dlg.txt_value.move(dlg.lbl_msg) @@ -1393,12 +1696,24 @@ def inputbox(message, default='', title=TITLE): return '' -def new_doc(type_doc=CALC): +# ~ Export ok +def new_doc(type_doc=CALC, **kwargs): path = 'private:factory/s{}'.format(type_doc) - doc = get_desktop().loadComponentFromURL(path, '_default', 0, ()) + opt = dict_to_property(kwargs) + doc = get_desktop().loadComponentFromURL(path, '_default', 0, opt) return _get_class_doc(doc) +# ~ Export ok +def new_db(path): + dbc = create_instance('com.sun.star.sdb.DatabaseContext') + db = dbc.createInstance() + db.URL = 'sdbc:embedded:firebird' # hsqldb + db.DatabaseDocument.storeAsURL(_path_url(path), ()) + return _get_class_doc(db) + + +# ~ Export ok def open_doc(path, **kwargs): """ Open document in path Usually options: @@ -1413,7 +1728,6 @@ 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 = dict_to_property(kwargs) doc = get_desktop().loadComponentFromURL(path, '_blank', 0, opt) if doc is None: @@ -1422,6 +1736,7 @@ def open_doc(path, **kwargs): return _get_class_doc(doc) +# ~ Export ok def open_file(path): if IS_WIN: os.startfile(path) @@ -1430,37 +1745,45 @@ def open_file(path): return +# ~ Export ok def join(*paths): return os.path.join(*paths) +# ~ Export ok def is_dir(path): return Path(path).is_dir() +# ~ Export ok def is_file(path): return Path(path).is_file() +# ~ Export ok def get_file_size(path): return Path(path).stat().st_size +# ~ Export ok def is_created(path): return is_file(path) and bool(get_file_size(path)) +# ~ Export ok def replace_ext(path, extension): path, _, name, _ = get_info_path(path) return '{}/{}.{}'.format(path, name, extension) -def zip_names(path): +# ~ Export ok +def zip_content(path): with zipfile.ZipFile(path) as z: names = z.namelist() return names +# ~ Export ok def run(command, wait=False): # ~ debug(command) # ~ debug(shlex.split(command)) @@ -1515,6 +1838,7 @@ def _zippwd(source, target, pwd): return is_created(target) +# ~ Export ok def zip(source, target='', mode='w', pwd=''): if pwd: return _zippwd(source, target, pwd) @@ -1556,6 +1880,7 @@ def zip(source, target='', mode='w', pwd=''): return is_created(target) +# ~ Export ok def unzip(source, path='', members=None, pwd=None): if not path: path, _, _, _ = get_info_path(source) @@ -1568,6 +1893,7 @@ def unzip(source, path='', members=None, pwd=None): return True +# ~ Export ok def merge_zip(target, zips): try: with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_DEFLATED) as t: @@ -1582,18 +1908,20 @@ def merge_zip(target, zips): return True +# ~ Export ok def kill(path): p = Path(path) - if p.is_file(): - try: + try: + if p.is_file(): p.unlink() - except: - pass - elif p.is_dir(): - p.rmdir() + elif p.is_dir(): + shutil.rmtree(path) + except OSError as e: + log.error(e) return +# ~ Export ok def get_size_screen(): if IS_WIN: user32 = ctypes.windll.user32 @@ -1604,6 +1932,7 @@ def get_size_screen(): return res.strip() +# ~ Export ok def get_clipboard(): df = None text = '' @@ -1649,6 +1978,7 @@ class TextTransferable(unohelper.Base, XTransferable): return False +# ~ Export ok def set_clipboard(value): ts = TextTransferable(value) sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard') @@ -1656,40 +1986,38 @@ def set_clipboard(value): return -def copy(doc=None): - if doc is None: - doc = get_document() - if hasattr(doc, 'frame'): - frame = doc.frame - else: - frame = doc.getCurrentController().getFrame() - dispatch = get_dispatch() - dispatch.executeDispatch(frame, '.uno:Copy', '', 0, ()) +# ~ Todo +def copy(): + call_dispatch('.uno:Copy') return +# ~ Export ok def get_epoch(): - now = datetime.datetime.now() - return int(time.mktime(now.timetuple())) + n = now() + return int(time.mktime(n.timetuple())) +# ~ Export ok def file_copy(source, target='', name=''): p, f, n, e = get_info_path(source) if target: p = target if name: + e = '' n = name path_new = join(p, '{}{}'.format(n, e)) shutil.copy(source, path_new) - return + return path_new -def get_files(path, ext='*'): - docs = [] +# ~ Export ok +def get_path_content(path, filters='*'): + paths = [] 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 + pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE) + paths += [join(folder, f) for f in files if pattern.search(f)] + return paths def _get_menu(type_doc, name_menu): @@ -1739,7 +2067,7 @@ def insert_menu(type_doc, name_menu, **kwargs): separator = False if label == '-': separator = True - command = kwargs.get('Command', '') + command = kwargs.get('CommandURL', '') index = kwargs.get('Index', 0) if not index: index = _get_index_menu(menu, kwargs['After']) @@ -1779,6 +2107,8 @@ def _add_sub_menus(ui, menus, menu, sub_menu): if submenu: idc = ui.createSettings() sm['ItemDescriptorContainer'] = idc + if sm['Label'] == '-': + sm = {'Type': 1} _store_menu(ui, menus, menu, i - 1, sm) if submenu: _add_sub_menus(ui, menus, idc, submenu) @@ -1799,6 +2129,488 @@ def remove_menu(type_doc, name_menu, command): return True +def _get_app_submenus(menus, count=0): + for i, menu in enumerate(menus): + data = property_to_dict(menu) + cmd = data.get('CommandURL', '') + msg = ' ' * count + '├─' + cmd + debug(msg) + submenu = data.get('ItemDescriptorContainer', None) + if not submenu is None: + _get_app_submenus(submenu, count + 1) + return + + +def get_app_menus(name_app, index=-1): + instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier' + service = TYPE_DOC[name_app] + manager = create_instance(instance, True) + ui = manager.getUIConfigurationManager(service) + menus = ui.getSettings(NODE_MENUBAR, True) + if index == -1: + for menu in menus: + data = property_to_dict(menu) + debug(data.get('CommandURL', '')) + else: + menus = property_to_dict(menus[index])['ItemDescriptorContainer'] + _get_app_submenus(menus) + return menus + + +# ~ Export ok +def start(): + global _start + _start = now() + log.info(_start) + return + + +# ~ Export ok +def end(): + global _start + e = now() + return str(e - _start).split('.')[0] + + +# ~ Export ok +# ~ https://en.wikipedia.org/wiki/Web_colors +def get_color(value): + COLORS = { + 'aliceblue': 15792383, + 'antiquewhite': 16444375, + 'aqua': 65535, + 'aquamarine': 8388564, + 'azure': 15794175, + 'beige': 16119260, + 'bisque': 16770244, + 'black': 0, + 'blanchedalmond': 16772045, + 'blue': 255, + 'blueviolet': 9055202, + 'brown': 10824234, + 'burlywood': 14596231, + 'cadetblue': 6266528, + 'chartreuse': 8388352, + 'chocolate': 13789470, + 'coral': 16744272, + 'cornflowerblue': 6591981, + 'cornsilk': 16775388, + 'crimson': 14423100, + 'cyan': 65535, + 'darkblue': 139, + 'darkcyan': 35723, + 'darkgoldenrod': 12092939, + 'darkgray': 11119017, + 'darkgreen': 25600, + 'darkgrey': 11119017, + 'darkkhaki': 12433259, + 'darkmagenta': 9109643, + 'darkolivegreen': 5597999, + 'darkorange': 16747520, + 'darkorchid': 10040012, + 'darkred': 9109504, + 'darksalmon': 15308410, + 'darkseagreen': 9419919, + 'darkslateblue': 4734347, + 'darkslategray': 3100495, + 'darkslategrey': 3100495, + 'darkturquoise': 52945, + 'darkviolet': 9699539, + 'deeppink': 16716947, + 'deepskyblue': 49151, + 'dimgray': 6908265, + 'dimgrey': 6908265, + 'dodgerblue': 2003199, + 'firebrick': 11674146, + 'floralwhite': 16775920, + 'forestgreen': 2263842, + 'fuchsia': 16711935, + 'gainsboro': 14474460, + 'ghostwhite': 16316671, + 'gold': 16766720, + 'goldenrod': 14329120, + 'gray': 8421504, + 'grey': 8421504, + 'green': 32768, + 'greenyellow': 11403055, + 'honeydew': 15794160, + 'hotpink': 16738740, + 'indianred': 13458524, + 'indigo': 4915330, + 'ivory': 16777200, + 'khaki': 15787660, + 'lavender': 15132410, + 'lavenderblush': 16773365, + 'lawngreen': 8190976, + 'lemonchiffon': 16775885, + 'lightblue': 11393254, + 'lightcoral': 15761536, + 'lightcyan': 14745599, + 'lightgoldenrodyellow': 16448210, + 'lightgray': 13882323, + 'lightgreen': 9498256, + 'lightgrey': 13882323, + 'lightpink': 16758465, + 'lightsalmon': 16752762, + 'lightseagreen': 2142890, + 'lightskyblue': 8900346, + 'lightslategray': 7833753, + 'lightslategrey': 7833753, + 'lightsteelblue': 11584734, + 'lightyellow': 16777184, + 'lime': 65280, + 'limegreen': 3329330, + 'linen': 16445670, + 'magenta': 16711935, + 'maroon': 8388608, + 'mediumaquamarine': 6737322, + 'mediumblue': 205, + 'mediumorchid': 12211667, + 'mediumpurple': 9662683, + 'mediumseagreen': 3978097, + 'mediumslateblue': 8087790, + 'mediumspringgreen': 64154, + 'mediumturquoise': 4772300, + 'mediumvioletred': 13047173, + 'midnightblue': 1644912, + 'mintcream': 16121850, + 'mistyrose': 16770273, + 'moccasin': 16770229, + 'navajowhite': 16768685, + 'navy': 128, + 'oldlace': 16643558, + 'olive': 8421376, + 'olivedrab': 7048739, + 'orange': 16753920, + 'orangered': 16729344, + 'orchid': 14315734, + 'palegoldenrod': 15657130, + 'palegreen': 10025880, + 'paleturquoise': 11529966, + 'palevioletred': 14381203, + 'papayawhip': 16773077, + 'peachpuff': 16767673, + 'peru': 13468991, + 'pink': 16761035, + 'plum': 14524637, + 'powderblue': 11591910, + 'purple': 8388736, + 'red': 16711680, + 'rosybrown': 12357519, + 'royalblue': 4286945, + 'saddlebrown': 9127187, + 'salmon': 16416882, + 'sandybrown': 16032864, + 'seagreen': 3050327, + 'seashell': 16774638, + 'sienna': 10506797, + 'silver': 12632256, + 'skyblue': 8900331, + 'slateblue': 6970061, + 'slategray': 7372944, + 'slategrey': 7372944, + 'snow': 16775930, + 'springgreen': 65407, + 'steelblue': 4620980, + 'tan': 13808780, + 'teal': 32896, + 'thistle': 14204888, + 'tomato': 16737095, + 'turquoise': 4251856, + 'violet': 15631086, + 'wheat': 16113331, + 'white': 16777215, + 'whitesmoke': 16119285, + 'yellow': 16776960, + 'yellowgreen': 10145074, + } + + if isinstance(value, tuple): + return (value[0] << 16) + (value[1] << 8) + value[2] + + if isinstance(value, str) and value[0] == '#': + r, g, b = bytes.fromhex(value[1:]) + return (r << 16) + (g << 8) + b + + return COLORS.get(value.lower(), -1) + + +# ~ Export ok +def render(template, data): + s = Template(template) + return s.safe_substitute(**data) + + +def _to_date(value): + new_value = value + if isinstance(value, Time): + new_value = datetime.time(value.Hours, value.Minutes, value.Seconds) + elif isinstance(value, Date): + new_value = datetime.date(value.Year, value.Month, value.Day) + elif isinstance(value, DateTime): + new_value = datetime.datetime( + value.Year, value.Month, value.Day, + value.Hours, value.Minutes, value.Seconds) + return new_value + + +# ~ Export ok +def format(template, data): + """ + https://pyformat.info/ + """ + if isinstance(data, (str, int, float)): + # ~ print(template.format(data)) + return template.format(data) + + if isinstance(data, (Time, Date, DateTime)): + return template.format(_to_date(data)) + + if isinstance(data, tuple) and isinstance(data[0], tuple): + data = {r[0]: _to_date(r[1]) for r in data} + return template.format(**data) + + data = [_to_date(v) for v in data] + result = template.format(*data) + return result + + +def _call_macro(macro): + #~ https://wiki.openoffice.org/wiki/Documentation/DevGuide/Scripting/Scripting_Framework_URI_Specification + name = 'com.sun.star.script.provider.MasterScriptProviderFactory' + factory = create_instance(name, False) + + data = macro.copy() + if macro['language'] == 'Python': + data['module'] = '.py$' + elif macro['language'] == 'Basic': + data['module'] = '.{}.'.format(macro['module']) + if macro['location'] == 'user': + data['location'] = 'application' + else: + data['module'] = '.' + + 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] + + +# ~ Export ok +def call_macro(macro): + in_thread = macro.pop('thread') + if in_thread: + t = threading.Thread(target=_call_macro, args=(macro,)) + t.start() + return + + return _call_macro(macro) + + +class TimerThread(threading.Thread): + + def __init__(self, event, seconds, macro): + threading.Thread.__init__(self) + self.stopped = event + self.seconds = seconds + self.macro = macro + + def run(self): + info('Timer started... {}'.format(self.macro['name'])) + while not self.stopped.wait(self.seconds): + _call_macro(self.macro) + info('Timer stopped... {}'.format(self.macro['name'])) + return + + +# ~ Export ok +def timer(name, seconds, macro): + global _stop_thread + _stop_thread[name] = threading.Event() + thread = TimerThread(_stop_thread[name], seconds, macro) + thread.start() + return + + +# ~ Export ok +def stop_timer(name): + global _stop_thread + _stop_thread[name].set() + del _stop_thread[name] + return + + +def _get_key(password): + digest = hashlib.sha256(password.encode()).digest() + key = base64.urlsafe_b64encode(digest) + return key + + +# ~ Export ok +def encrypt(data, password): + f = Fernet(_get_key(password)) + token = f.encrypt(data).decode() + return token + + +# ~ Export ok +def decrypt(token, password): + data = '' + f = Fernet(_get_key(password)) + try: + data = f.decrypt(token.encode()).decode() + except InvalidToken as e: + error('Invalid Token') + return data + + +class SmtpServer(object): + + def __init__(self, config): + self._server = None + self._error = '' + self._sender = '' + self._is_connect = self._login(config) + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + @property + def is_connect(self): + return self._is_connect + + @property + def error(self): + return self._error + + def _login(self, config): + name = config['server'] + port = config['port'] + is_ssl = config['ssl'] + self._sender = config['user'] + hosts = ('gmail' in name or 'outlook' in name) + try: + if is_ssl and hosts: + self._server = smtplib.SMTP(name, port, timeout=TIMEOUT) + self._server.ehlo() + self._server.starttls() + self._server.ehlo() + elif is_ssl: + self._server = smtplib.SMTP_SSL(name, port, timeout=TIMEOUT) + self._server.ehlo() + else: + self._server = smtplib.SMTP(name, port, timeout=TIMEOUT) + + self._server.login(self._sender, config['pass']) + msg = 'Connect to: {}'.format(name) + debug(msg) + return True + except smtplib.SMTPAuthenticationError as e: + if '535' in str(e): + self._error = _('Incorrect user or password') + return False + if '534' in str(e) and 'gmail' in name: + self._error = _('Allow less secure apps in GMail') + return False + except smtplib.SMTPException as e: + self._error = str(e) + return False + except Exception as e: + self._error = str(e) + return False + return False + + def _body(self, msg): + body = msg.replace('\\n', '
') + return body + + def send(self, message): + file_name = 'attachment; filename={}' + email = MIMEMultipart() + email['From'] = self._sender + email['To'] = message['to'] + email['Cc'] = message.get('cc', '') + email['Subject'] = message['subject'] + email['Date'] = formatdate(localtime=True) + if message.get('confirm', False): + email['Disposition-Notification-To'] = email['From'] + email.attach(MIMEText(self._body(message['body']), 'html')) + + for path in message.get('files', ()): + _, fn, _, _ = get_info_path(path) + part = MIMEBase('application', 'octet-stream') + part.set_payload(read_file(path, 'rb')) + encoders.encode_base64(part) + part.add_header('Content-Disposition', file_name.format(fn)) + email.attach(part) + + receivers = ( + email['To'].split(',') + + email['CC'].split(',') + + message.get('bcc', '').split(',')) + try: + self._server.sendmail(self._sender, receivers, email.as_string()) + msg = 'Email sent...' + debug(msg) + if message.get('path', ''): + self.save_message(email, message['path']) + return True + except Exception as e: + self._error = str(e) + return False + return False + + def save_message(self, email, path): + mbox = mailbox.mbox(path, create=True) + mbox.lock() + try: + msg = mailbox.mboxMessage(email) + mbox.add(msg) + mbox.flush() + finally: + mbox.unlock() + return + + def close(self): + try: + self._server.quit() + msg = 'Close connection...' + debug(msg) + except: + pass + return + + +def _send_email(server, messages): + with SmtpServer(server) as server: + if server.is_connect: + for msg in messages: + server.send(msg) + else: + error(server.error) + return server.error + + +def send_email(server, message): + messages = message + if isinstance(message, dict): + messages = (message,) + t = threading.Thread(target=_send_email, args=(server, messages)) + t.start() + return + + +def server_smtp_test(config): + with SmtpServer(config) as server: + if server.error: + error(server.error) + return server.error + + # ~ name = 'com.sun.star.configuration.ConfigurationProvider' # ~ cp = create_instance(name, True) # ~ node = PropertyValue(Name='nodepath', Value=NODE_SETTING) diff --git a/source/images/add.png b/source/images/add.png new file mode 100644 index 0000000..6cacfc5 Binary files /dev/null and b/source/images/add.png differ diff --git a/source/images/barcode_16.bmp b/source/images/barcode_16.bmp new file mode 100644 index 0000000..10246a3 Binary files /dev/null and b/source/images/barcode_16.bmp differ diff --git a/source/images/delete.png b/source/images/delete.png new file mode 100644 index 0000000..7652e03 Binary files /dev/null and b/source/images/delete.png differ diff --git a/source/images/favorite.png b/source/images/favorite.png new file mode 100644 index 0000000..2e6d2ea Binary files /dev/null and b/source/images/favorite.png differ diff --git a/source/images/favorite_26.bmp b/source/images/favorite_26.bmp new file mode 100644 index 0000000..0da6992 Binary files /dev/null and b/source/images/favorite_26.bmp differ diff --git a/source/images/save.png b/source/images/save.png new file mode 100644 index 0000000..06d84fe Binary files /dev/null and b/source/images/save.png differ diff --git a/source/images/tool_16.bmp b/source/images/tool_16.bmp new file mode 100644 index 0000000..0bd1d9b Binary files /dev/null and b/source/images/tool_16.bmp differ