/
kuzant24
/
libre-macros
Обзор
Документация
Войти
/
kuzant24
/
libre-macros
Код
Запросы
1
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
new_source_processing
macro-lib/param_wizard.py
19 299 строк
816 KB
direct-dev.ru
fixed for home work
04 авг 2026, 16:06
04 авг 2026, 16:06
eb53100
Код
Авторство
О чём код?
# -*- coding: utf-8 -*- from __future__ import print_function, unicode_literals """ Наименование: param_wizard Описание: визард параметров листа «Параметры_Объединения» для макроса collect_workbooks. Назначение: диалог выбора параметра, подсказки, выпадающие списки и запись значений на лист параметров. Точка входа: set_merge_param(). """ MACRO_VERSION = "3.10.359" import ast import glob import json import os import re import sys import time import uno import unohelper try: unicode except NameError: unicode = str try: unichr except NameError: unichr = chr import libre_macros_param_wizard_cfg as _pw_cfg from libre_macros_param_wizard_cfg import * # noqa: F403 — реэкспорт констант def _pw_dbg(msg): if not _pw_cfg.PARAM_WIZARD_DEBUG: return try: print(u'[param_wizard DEBUG] %s' % unicode(msg)) except Exception: try: print('[param_wizard DEBUG] %s' % str(msg)) except Exception: pass def _pw_dbg_kv(title, val): if not _pw_cfg.PARAM_WIZARD_DEBUG: return try: _pw_dbg(u'%s = %s' % (unicode(title), unicode(val))) except Exception: try: _pw_dbg('%s = %r' % (title, val)) except Exception: pass def _pw_time_log(msg): if not (_pw_cfg.PARAM_WIZARD_DEBUG or _pw_cfg.PARAM_WIZARD_TIMING): return try: print(u'[param_wizard TIME] %s' % unicode(msg)) except Exception: try: print('[param_wizard TIME] %s' % str(msg)) except Exception: pass class _PwTimed(object): """Контекстный замер этапа: >> stage … << stage: N ms.""" def __init__(self, stage): self.stage = unicode(stage) if stage is not None else u'?' self._t0 = 0.0 self._depth = 0 def __enter__(self): if _pw_cfg.PARAM_WIZARD_DEBUG or _pw_cfg.PARAM_WIZARD_TIMING: self._t0 = time.time() self._depth = _pw_cfg._PW_TIME_DEPTH _pw_cfg._PW_TIME_DEPTH += 1 _pw_time_log(u'%s>> %s' % (u' ' * self._depth, self.stage)) return self def __exit__(self, exc_type, exc_val, exc_tb): if _pw_cfg.PARAM_WIZARD_DEBUG or _pw_cfg.PARAM_WIZARD_TIMING: ms = (time.time() - self._t0) * 1000.0 if exc_type is not None: _pw_time_log(u'%s<< %s: %.1f ms (ошибка: %s)' % (u' ' * self._depth, self.stage, ms, exc_val)) else: _pw_time_log(u'%s<< %s: %.1f ms' % (u' ' * self._depth, self.stage, ms)) _pw_cfg._PW_TIME_DEPTH = max(0, _pw_cfg._PW_TIME_DEPTH - 1) return False def _pw_timed(stage): return _PwTimed(stage) class _PwTimedNoop(object): def __enter__(self): return self def __exit__(self, *args): return False def _pw_timed_if(stage, enabled=True): if enabled and (_pw_cfg.PARAM_WIZARD_DEBUG or _pw_cfg.PARAM_WIZARD_TIMING): return _PwTimed(stage) return _PwTimedNoop() from com.sun.star.awt import XActionListener, XItemListener, XMouseListener, XTextListener from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS from com.sun.star.awt.MessageBoxResults import YES from com.sun.star.awt.MessageBoxType import MESSAGEBOX, QUERYBOX from com.sun.star.style.VerticalAlignment import MIDDLE as VERT_ALIGN_MIDDLE def _path_from_file_ref(file_ref): """Путь на диске: file:// URL (LibreOffice) или обычный путь.""" s = unicode(file_ref or u'').strip() if s == u'': return u'' if s.lower().startswith(u'file://'): try: return os.path.normpath(uno.fileUrlToSystemPath(s)) except Exception: return u'' try: return os.path.normpath(os.path.abspath(s)) except Exception: return u'' def _macro_dir_from_module_file(module_file): """Каталог .py модуля (param_wizard / collect в LO).""" path = _path_from_file_ref(module_file) if path == u'': return None try: d = os.path.dirname(path) if d and os.path.isdir(d): return d except Exception: pass return None try: _here = _macro_dir_from_module_file(__file__) if not _here: _here = os.path.dirname(os.path.abspath(__file__)) _pp = os.path.join(_here, u'pythonpath') for _p in (_pp, _here): if _p not in sys.path: sys.path.insert(0, _p) except Exception: pass _pw_cfg._FALLBACK_PP_XML_REST_HINTS[u'сводная_таблица'] = (u'В Постобработка_xml — только материализованный отчёт (as_values всегда true, не DataPilot). JSON — кнопка «Параметры…».', u'[{"v":1,"fn":"сводная_таблица","as_values":true,"source_sheet":"Merge","header_row":"1","data_start":"","data_end":"","sheet_name":"Сводная","row_fields":["Отдел"],"column_fields":[],"data_fields":[{"field":"Сумма_руб","function":"SUM"}]}]') def _merge_pp_rest_hints(base, extra): """Объединить словари подсказок (ключи — unicode).""" out = {} for k, v in (base or {}).items(): out[unicode(k)] = v if extra: for k, v in extra.items(): out[unicode(k)] = v return out def _rest_hint_lookup(rest_hints, key): """Найти подсказку по имени функции (точное, без регистра, через normalize_fn_key).""" if not rest_hints or key is None: return None k = unicode(key).strip() if k == u'': return None if k in rest_hints: return rest_hints[k] kl = k.lower() for name, item in rest_hints.items(): if unicode(name).strip().lower() == kl: return item try: from libre_macros_param_codec import normalize_fn_key norm = unicode(normalize_fn_key(k) or u'').strip() except Exception: norm = u'' if norm != u'' and norm != k: item = _rest_hint_lookup(rest_hints, norm) if item is not None: return item if norm != u'': for name, item in rest_hints.items(): if not isinstance(item, (tuple, list)) or len(item) < 1: continue desc = unicode(item[0] or u'') if u'алиас' in desc.lower() and norm in desc: canon = _rest_hint_lookup(rest_hints, unicode(name).strip()) if canon is not None: return canon return None def _try_import_lm(): """libre_macros_lib — разбор Да/Нет без collect_workbooks.""" if _pw_cfg._LM_LIB is False: return None if _pw_cfg._LM_LIB is None: try: import sys here = _macro_dir_from_module_file(__file__) if not here: here = os.path.dirname(os.path.abspath(__file__)) pp = os.path.join(here, u'pythonpath') for p in (pp, here): if p not in sys.path: sys.path.insert(0, p) import libre_macros_lib as lm _pw_cfg._LM_LIB = lm except Exception as err: print(u'param_wizard: не удалось загрузить libre_macros_lib: %s' % err) _pw_cfg._LM_LIB = False return _pw_cfg._LM_LIB if _pw_cfg._LM_LIB is not False else None def _try_import_collect(): if _pw_cfg._COLLECT is False: return None if _pw_cfg._COLLECT is None: try: import sys here = _macro_dir_from_module_file(__file__) if not here: here = os.path.dirname(os.path.abspath(__file__)) if here not in sys.path: sys.path.insert(0, here) import collect_workbooks as cw _pw_cfg._COLLECT = cw except Exception as err: print(u'param_wizard: не удалось загрузить collect_workbooks: %s' % err) _pw_cfg._COLLECT = False return _pw_cfg._COLLECT if _pw_cfg._COLLECT is not False else None def _ensure_collect_pp_maps(cw): """Заполнить MERGE_RESULT_POSTPROCESS_*_MAP перед чтением списков функций.""" if cw is None: return try: if hasattr(cw, '_ensure_pp_from_lib'): cw._ensure_pp_from_lib() except Exception as err: print(u'param_wizard: _ensure_pp_from_lib: %s' % err) def _plugin_macro_script_dirs(): """Каталоги поиска functions_*.py: сначала param_wizard, затем collect_workbooks.""" dirs = [] seen = set() def _add(path): if not path: return try: norm = os.path.normpath(os.path.abspath(path)) except Exception: return if os.path.isdir(norm) and norm not in seen: seen.add(norm) dirs.append(norm) try: _add(_macro_dir_from_module_file(__file__)) except Exception: pass cw = _try_import_collect() if cw is not None and hasattr(cw, '_merge_macro_script_dir'): try: _add(cw._merge_macro_script_dir()) except Exception: pass return dirs def _plugin_macro_script_dir(): """Первый каталог из _plugin_macro_script_dirs (для совместимости).""" dirs = _plugin_macro_script_dirs() if dirs: return dirs[0] d = _macro_dir_from_module_file(__file__) if d: return d try: return os.path.dirname(os.path.abspath(__file__)) except Exception: return os.getcwd() def _glob_functions_plugin_files(): """Все functions_*.py рядом с param_wizard и/или collect_workbooks.""" out = [] seen = set() for script_dir in _plugin_macro_script_dirs(): pattern = os.path.join(script_dir, 'functions_*.py') if _pw_cfg.PARAM_WIZARD_DEBUG: _pw_dbg(u'plugin glob pattern: %s' % pattern) for path in sorted(glob.glob(pattern)): norm = os.path.normpath(path) if os.path.isfile(norm) and norm not in seen: seen.add(norm) out.append(norm) if _pw_cfg.PARAM_WIZARD_DEBUG: _pw_dbg_kv(u'plugin functions_*.py found', out) return out def _parse_py_file_ast(abs_path): try: f = open(abs_path, 'rb') try: raw = f.read() finally: f.close() if raw.startswith(b'\xef\xbb\xbf'): text = raw[3:].decode('utf-8') else: text = raw.decode('utf-8') return ast.parse(text) except Exception as err: if _pw_cfg.PARAM_WIZARD_DEBUG: _pw_dbg(u'plugin AST parse fail %s: %s' % (abs_path, err)) return None def _fn_def_matches_signature(func_def, sig_prefix): if not isinstance(func_def, ast.FunctionDef): return False names = [a.arg for a in func_def.args.args] if len(names) < len(sig_prefix): return False i = 0 while i < len(sig_prefix): if names[i] != sig_prefix[i]: return False i = i + 1 return True def _fn_def_plugin_kind(func_def): """range | row | final | None — по сигнатуре пользовательского плагина.""" if _fn_def_matches_signature(func_def, _pw_cfg._PP_PLUGIN_SIG_FINAL): return 'final' if _fn_def_matches_signature(func_def, _pw_cfg._PP_PLUGIN_SIG_ROW): return 'row' if _fn_def_matches_signature(func_def, _pw_cfg._PP_PLUGIN_SIG_RANGE): return 'range' return None def _pw_dbg_plugin_signature_hint(kind): want = unicode(kind or u'').strip().lower() if want == 'final': sig = _pw_cfg._PP_PLUGIN_SIG_FINAL elif want == 'row': sig = _pw_cfg._PP_PLUGIN_SIG_ROW else: sig = _pw_cfg._PP_PLUGIN_SIG_RANGE _pw_dbg(u'plugin expected signature (%s): (%s, *extra_args)' % (want, u', '.join(sig))) def _scan_functions_plugin_choices(kind): """ functions_*.py#func рядом с макросом; kind = range | row | final. """ want = unicode(kind or u'').strip().lower() if _pw_cfg.PARAM_WIZARD_DEBUG: _pw_dbg(u'--- plugin scan start kind=%s ---' % want) _pw_dbg_kv(u'plugin __file__', __file__) _pw_dbg_kv(u'plugin __file__ dir', _macro_dir_from_module_file(__file__)) _pw_dbg_kv(u'plugin script dirs', _plugin_macro_script_dirs()) _pw_dbg_plugin_signature_hint(want) out = [] for abs_path in _glob_functions_plugin_files(): rel = os.path.basename(abs_path) tree = _parse_py_file_ast(abs_path) if tree is None: continue defs_total = 0 defs_matched = 0 for node in tree.body: if not isinstance(node, ast.FunctionDef): continue defs_total = defs_total + 1 names = [a.arg for a in node.args.args] pk = _fn_def_plugin_kind(node) if pk != want: if _pw_cfg.PARAM_WIZARD_DEBUG: _pw_dbg(u'plugin skip %s#%s args=%s detected=%s want=%s' % (rel, node.name, names, pk, want)) continue defs_matched = defs_matched + 1 out.append(u'%s#%s' % (rel, node.name)) if _pw_cfg.PARAM_WIZARD_DEBUG: _pw_dbg(u'plugin file %s: %d def(s), %d matched kind=%s' % (rel, defs_total, defs_matched, want)) if _pw_cfg.PARAM_WIZARD_DEBUG: _pw_dbg_kv(u'plugin choices (%s)' % want, out) _pw_dbg(u'--- plugin scan end kind=%s count=%d ---' % (want, len(out))) return sorted(out, key=lambda x: unicode(x).casefold()) def _functions_pp_file_paths(): """Пути к functions_pp.py (рядом с визардом или с collect_workbooks).""" paths = [] seen = set() here = _macro_dir_from_module_file(__file__) if not here: here = os.path.dirname(os.path.abspath(__file__)) for cand in (os.path.join(here, u'functions_pp.py'),): norm = os.path.normpath(cand) if os.path.isfile(norm) and norm not in seen: seen.add(norm) paths.append(norm) cw = _try_import_collect() if cw is not None and hasattr(cw, '_merge_macro_script_dir'): try: cand = os.path.join(cw._merge_macro_script_dir(), u'functions_pp.py') norm = os.path.normpath(cand) if os.path.isfile(norm) and norm not in seen: seen.add(norm) paths.append(norm) except Exception: pass return paths def _scan_functions_pp_choices(): """ Ссылки functions_pp.py#имя для выпадающих списков визарда. pp_row_* → постобработка строки; остальные pp_* → диапазон. """ range_choices = [] row_choices = [] for abs_path in _functions_pp_file_paths(): rel = os.path.basename(abs_path) try: f = open(abs_path, 'rb') try: raw = f.read() finally: f.close() if raw.startswith(b'\xef\xbb\xbf'): text = raw[3:].decode('utf-8') else: text = raw.decode('utf-8') tree = ast.parse(text) except Exception: continue for node in tree.body: if not isinstance(node, ast.FunctionDef): continue name = node.name if not name.startswith('pp_'): continue ref = u'%s#%s' % (rel, name) if name.startswith('pp_row_'): row_choices.append(ref) else: range_choices.append(ref) return (range_choices, row_choices) def _functions_final_file_paths(): """Пути к functions_final.py (рядом с визардом или с collect_workbooks).""" paths = [] seen = set() here = _macro_dir_from_module_file(__file__) if not here: here = os.path.dirname(os.path.abspath(__file__)) for cand in (os.path.join(here, u'functions_final.py'),): norm = os.path.normpath(cand) if os.path.isfile(norm) and norm not in seen: seen.add(norm) paths.append(norm) cw = _try_import_collect() if cw is not None and hasattr(cw, '_merge_macro_script_dir'): try: cand = os.path.join(cw._merge_macro_script_dir(), u'functions_final.py') norm = os.path.normpath(cand) if os.path.isfile(norm) and norm not in seen: seen.add(norm) paths.append(norm) except Exception: pass return paths def _scan_functions_final_choices(): """Ссылки functions_final.py#имя для выпадающего списка финальной обработки.""" out = [] for abs_path in _functions_final_file_paths(): rel = os.path.basename(abs_path) try: f = open(abs_path, 'rb') try: raw = f.read() finally: f.close() if raw.startswith(b'\xef\xbb\xbf'): text = raw[3:].decode('utf-8') else: text = raw.decode('utf-8') tree = ast.parse(text) except Exception: continue for node in tree.body: if not isinstance(node, ast.FunctionDef): continue name = node.name if not name.startswith('pp_'): continue out.append(u'%s#%s' % (rel, name)) return out def _pp_allows_freeform_b(text): """Колонка B/C: ссылка на .py#func или inline lambda/def (не из выпадающего списка).""" t = unicode(text).strip() if t == u'': return False cw = _try_import_collect() if cw is not None: try: if cw._merge_pp_looks_like_file_function_ref(t): return True if cw._merge_pp_looks_like_python_code(t): return True except Exception: pass if u'.py#' in t and t.count(u'#') == 1: path_part, fn_name = t.split(u'#', 1) if path_part.strip() != u'' and fn_name.strip() != u'': return True if t.startswith(u'lambda') or t.startswith(u'def '): return True if u'\n' in t and (u'def ' in t or u'lambda ' in t): return True return False def _pp_plugin_key(): cw = _try_import_collect() if cw is not None: return unicode(getattr(cw, 'MERGE_PLUGIN_FUNCTION_KEY', _pw_cfg.PLUGIN_FUNCTION_KEY)) return _pw_cfg.PLUGIN_FUNCTION_KEY def _pp_is_plugin_primary(text): key = unicode(text or u'').strip().casefold() return key in (_pw_cfg.PLUGIN_FUNCTION_KEY.casefold(), u'plugin_function', u'plugin', _pp_plugin_key().casefold()) def _pp_allows_freeform_c(text): return _pp_allows_freeform_b(text) def _pp_is_plugin_json_c(text): """Колонка C: JSON-массив с блоком функция_плагин (из диалога «Сохранять как JSON»).""" t = unicode(text or u'').strip() if not (t.startswith(u'[{') and t.endswith(u'}]')): return False try: parsed = json.loads(t) except Exception: return False if not isinstance(parsed, list) or len(parsed) == 0: return False block = parsed[0] if not isinstance(block, dict): return False fn = unicode(block.get('fn') or u'').strip().casefold() return fn in (_pw_cfg.PLUGIN_FUNCTION_KEY.casefold(), u'plugin_function', u'plugin', _pp_plugin_key().casefold()) def _pp_catalog_choices_with_plugin(choices): """Убрать file# из combo B; добавить «функция_плагин».""" out = [] seen = set() pk = _pp_plugin_key() for c in choices or []: cs = unicode(c) if u'.py#' in cs: continue k = cs.casefold() if k in seen: continue seen.add(k) out.append(cs) if pk.casefold() not in seen: out.append(pk) return sorted(out, key=lambda x: unicode(x).casefold()) def _plugin_scan_for_spec(spec): """Список functions_*.py#func для диалога плагина (по сигнатуре).""" if _pw_cfg.PARAM_WIZARD_DEBUG: spec_name = spec.get('name') if spec is not None else None spec_mode = spec.get('mode') if spec is not None else None _pw_dbg(u'plugin_scan_for_spec name=%s mode=%s' % (spec_name, spec_mode)) if spec is not None and _param_match(spec.get('name', u''), u'Финальная_обработка'): return _scan_functions_plugin_choices('final') mode = spec.get('mode') if spec is not None else u'' if mode == u'postprocess_row': return _scan_functions_plugin_choices('row') return _scan_functions_plugin_choices('range') def _plugin_block_list_label(block): """Подпись блока в списке: лист + ref/extra.""" sheet = unicode(block.get('sheet') or u'').strip() if block.get('code'): tail = u'Python' else: ref = unicode(block.get('ref') or block.get('c') or u'').strip() if u'#' in ref: tail = ref.split(u'#', 1)[-1] else: tail = ref[:24] + (u'…' if len(ref) > 24 else u'') extra = unicode(block.get('extra') or block.get('d') or u'').strip() if extra: xs = extra[:16] + (u'…' if len(extra) > 16 else u'') tail = u'%s · %s' % (tail, xs) return _inner_block_list_label(sheet, tail) def _plugin_blocks_from_initial(initial_ref, initial_extra): """Разбор C/D или legacy ref в список блоков плагина.""" init_ref = unicode(initial_ref or u'').strip() init_extra = unicode(initial_extra or u'').strip() blocks = [] if init_ref.startswith(u'[{') and init_ref.endswith(u'}]'): try: parsed = json.loads(init_ref) if isinstance(parsed, list): for item in parsed: if not isinstance(item, dict): continue fn = unicode(item.get('fn') or u'').strip() if fn != _pw_cfg.PLUGIN_FUNCTION_KEY: continue blocks.append(dict(item)) except Exception: pass if len(blocks) == 0 and init_ref != u'': b = {'v': 1, 'fn': _pw_cfg.PLUGIN_FUNCTION_KEY} if _pp_allows_freeform_c(init_ref) and (u'def ' in init_ref or u'lambda' in init_ref.lower()): b['code'] = init_ref else: b['ref'] = init_ref if init_extra != u'': b['extra'] = init_extra blocks = [b] if len(blocks) == 0: blocks = [{'v': 1, 'fn': _pw_cfg.PLUGIN_FUNCTION_KEY}] for b in blocks: b['v'] = 1 b['fn'] = _pw_cfg.PLUGIN_FUNCTION_KEY return blocks def _show_plugin_function_dialog(parent_dialog, spec, initial_ref=u'', initial_extra=u'', doc=None): """ Диалог пользовательской функции (блоки по листам, как у встроенных функций). C = JSON-массив [{ref|code, extra, sheet}, …]; D пусто. Возвращает (c_ref, d_extra) или None. """ choices = _plugin_scan_for_spec(spec) if _pw_cfg.PARAM_WIZARD_DEBUG: _pw_dbg_kv(u'plugin dialog RefCombo items', choices) norm_blocks = _plugin_blocks_from_initial(initial_ref, initial_extra) same_all_init = len(norm_blocks) <= 1 and all((not unicode(b.get('sheet') or u'').strip() for b in norm_blocks)) ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) dm = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialogModel', ctx) dw = 560 dh = _inner_dialog_height(_pw_cfg._INNER_GRID_DH) m = 10 dm.PositionX = 120 dm.PositionY = 80 dm.Width = dw dm.Height = dh dm.Title = u'Пользовательская функция' _inner_dialog_set_sizeable(dm) y = m _wizard_dlg_add_fixed(dm, 'HintLbl', u'B = «функция_плагин». Слева — лист; справа — ссылка functions_*.py#имя или Python-код и доп. аргументы (extra).', m, y, dw - m * 2, 28, multiline=True) y += 34 _wizard_dlg_add_checkbox(dm, 'SameAllChk', u'Одинаково на всех листах', m, y, dw - m * 2, 16) y += 22 col_gap = 10 left_w = 190 right_w = dw - m * 2 - left_w - col_gap form_h = 16 + 18 + 20 + 14 + 72 + 24 + 14 + 18 list_top = y + 16 list_h = _inner_grid_list_height(dh, list_top, m, min_h=72) list_h = min(list_h, max(72, dh - list_top - _pw_cfg._INNER_FOOTER_RESERVE - form_h - _pw_cfg._INNER_GRID_CONTROLS_H - _pw_cfg._INNER_GRID_LIST_GAP)) _wizard_dlg_add_fixed(dm, 'BlocksLbl', u'Блоки:', m, y, left_w, 14) _wizard_dlg_add_fixed(dm, 'ParamsLbl', u'Параметры:', m + left_w + col_gap, y, right_w, 14) y += 16 _wizard_dlg_add_listbox(dm, 'BlocksLB', m, y, left_w, list_h, multiselect=False) rx = m + left_w + col_gap fy = y _wizard_dlg_add_fixed(dm, 'RefLbl', u'Ссылка на функцию:', rx, fy, right_w, 12) fy += 14 _wizard_dlg_add_combo(dm, 'RefCombo', rx, fy, right_w, 18) fy += 22 _wizard_dlg_add_checkbox(dm, 'CodeChk', u'Свой Python-код вместо file#func', rx, fy, right_w, 16) fy += 20 _wizard_dlg_add_fixed(dm, 'CodeLbl', u'Код (lambda или def …):', rx, fy, right_w, 12) fy += 14 _wizard_dlg_add_edit(dm, 'CodeEd', rx, fy, right_w, 72, multiline=True) fy += 78 _wizard_dlg_add_fixed(dm, 'ExtraLbl', u'Доп. аргументы (extra):', rx, fy, right_w, 12) fy += 14 _wizard_dlg_add_edit(dm, 'ExtraEd', rx, fy, right_w, 18) y2 = y + list_h + 8 _wizard_dlg_add_fixed(dm, 'SheetLbl', u'Лист (пусто = все листы):', m, y2, left_w, 14) _wizard_dlg_add_combo(dm, 'SheetEd', m, y2 + 16, left_w, 18) _wizard_dlg_add_button(dm, 'AddBlockBtn', u'+ Блок', m, y2 + 40, 80, 16) _wizard_dlg_add_button(dm, 'DelBlockBtn', u'Удалить', m + 90, y2 + 40, 80, 16) _inner_dialog_add_footer(dm, m, dh) dlg = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialog', ctx) dlg.setModel(dm) _nested_subdialog_create_peer( dlg, toolkit, doc, parent_dialog, center_in_main_wizard=bool(parent_dialog) ) blocks_lb = dlg.getControl('BlocksLB') sheet_ed = dlg.getControl('SheetEd') ref_combo = dlg.getControl('RefCombo') code_chk = dlg.getControl('CodeChk') code_ed = dlg.getControl('CodeEd') extra_ed = dlg.getControl('ExtraEd') same_all_chk = dlg.getControl('SameAllChk') combo_items = [u''] + list(choices) _set_combo_items(ref_combo, combo_items, u'') doc_sheet_names = _pivot_list_doc_sheet_names(doc) if len(doc_sheet_names) > 0: try: _set_combo_items(sheet_ed, doc_sheet_names, select_text=u'') except Exception: pass try: same_all_chk.setState(1 if same_all_init else 0) except Exception: pass try: dlg.getControl('JsonChk').setState(1) except Exception: pass form_block_index = [-1] sheet_field_guard = [False] form_loading = [False] def _sync_code_mode(): code_on = False try: code_on = bool(code_chk.getState()) except Exception: pass for ctl_name in ('RefCombo', 'RefLbl'): try: dlg.getControl(ctl_name).Model.Enabled = not code_on except Exception: pass for ctl_name in ('CodeEd', 'CodeLbl'): try: dlg.getControl(ctl_name).Model.Enabled = code_on except Exception: pass def _refresh_blocks(selected_index=0): form_loading[0] = True items = [_plugin_block_list_label(b) for b in norm_blocks] try: blocks_lb.Model.StringItemList = tuple(items) except Exception: pass try: if len(items) > 0: blocks_lb.selectItemPos(max(0, min(selected_index, len(items) - 1)), True) except Exception: pass form_loading[0] = False def _block_index_selected(): try: pos = blocks_lb.getSelectedItemPos() return int(pos) if pos is not None else -1 except Exception: return -1 def _load_block_to_form(block_index): form_loading[0] = True form_block_index[0] = block_index if block_index < 0 or block_index >= len(norm_blocks): _inner_dialog_set_sheet_field_text(sheet_ed, u'', sheet_field_guard) try: ref_combo.setText(u'') except Exception: pass try: code_chk.setState(0) except Exception: pass try: code_ed.setText(u'') except Exception: pass try: extra_ed.setText(u'') except Exception: pass form_loading[0] = False _sync_code_mode() return b = norm_blocks[block_index] _inner_dialog_set_sheet_field_text(sheet_ed, unicode(b.get('sheet') or u'').strip(), sheet_field_guard) use_code = bool(b.get('code')) ref_val = unicode(b.get('ref') or b.get('c') or u'').strip() code_val = unicode(b.get('code') or u'').strip() extra_val = unicode(b.get('extra') or b.get('d') or u'').strip() try: code_chk.setState(1 if use_code else 0) except Exception: pass try: _set_combo_items(ref_combo, combo_items, ref_val if not use_code else u'') except Exception: pass try: code_ed.setText(code_val if use_code else u'') except Exception: pass try: extra_ed.setText(extra_val) except Exception: pass form_loading[0] = False _sync_code_mode() def _apply_form_to_block(block_index): if block_index < 0 or block_index >= len(norm_blocks): return False b = norm_blocks[block_index] _inner_dialog_set_block_sheet(b, _inner_dialog_sheet_text(sheet_ed)) code_on = False try: code_on = bool(code_chk.getState()) except Exception: pass if code_on: try: c_val = unicode(code_ed.getText() or u'').strip() except Exception: c_val = u'' if c_val == u'': return False b['code'] = c_val b.pop('ref', None) b.pop('c', None) else: try: c_val = unicode(ref_combo.getText() or u'').strip() except Exception: c_val = u'' if c_val == u'': return False b['ref'] = c_val b.pop('code', None) try: ex = unicode(extra_ed.getText() or u'').strip() except Exception: ex = u'' if ex != u'': b['extra'] = ex else: b.pop('extra', None) b.pop('d', None) b['v'] = 1 b['fn'] = _pw_cfg.PLUGIN_FUNCTION_KEY return True def _sync_sheet_field_to_block(): if sheet_field_guard[0] or form_loading[0]: return bi = form_block_index[0] if bi < 0 or bi >= len(norm_blocks): return _inner_dialog_set_block_sheet(norm_blocks[bi], _inner_dialog_sheet_text(sheet_ed)) _inner_dialog_update_blocks_list_label(blocks_lb, bi, _plugin_block_list_label(norm_blocks[bi])) def _switch_block_form(block_index): prev = form_block_index[0] if prev >= 0 and prev != block_index: if _apply_form_to_block(prev): _inner_dialog_update_blocks_list_label(blocks_lb, prev, _plugin_block_list_label(norm_blocks[prev])) _load_block_to_form(block_index) def _add_block_empty(): prev = form_block_index[0] if prev >= 0: if _apply_form_to_block(prev): _inner_dialog_update_blocks_list_label(blocks_lb, prev, _plugin_block_list_label(norm_blocks[prev])) norm_blocks.append({'v': 1, 'fn': _pw_cfg.PLUGIN_FUNCTION_KEY}) new_idx = len(norm_blocks) - 1 _refresh_blocks(selected_index=new_idx) _load_block_to_form(new_idx) def _sync_same_all_ui(): same_on = False try: same_on = bool(same_all_chk.getState()) except Exception: pass for ctl_name in ('BlocksLB', 'BlocksLbl', 'SheetLbl', 'SheetEd', 'AddBlockBtn', 'DelBlockBtn'): try: dlg.getControl(ctl_name).Model.Enabled = not same_on except Exception: pass if same_on and len(norm_blocks) > 1: while len(norm_blocks) > 1: norm_blocks.pop() _refresh_blocks(0) _load_block_to_form(0) class _PluginDlgHandler(unohelper.Base, XActionListener, XItemListener, XTextListener): def disposing(self, ev): pass def _sync_params_to_block(self): if form_loading[0]: return bi = form_block_index[0] if bi < 0: return if _apply_form_to_block(bi): _inner_dialog_update_blocks_list_label(blocks_lb, bi, _plugin_block_list_label(norm_blocks[bi])) def textChanged(self, ev): try: src = ev.Source except Exception: return if _wizard_dlg_is_named_control(src, sheet_ed, u'SheetEd'): _sync_sheet_field_to_block() elif src in (ref_combo, code_ed, extra_ed): self._sync_params_to_block() def itemStateChanged(self, ev): try: src = ev.Source except Exception: return if src == blocks_lb: if form_loading[0]: return _switch_block_form(_block_index_selected()) elif src == same_all_chk: if form_loading[0]: return _sync_same_all_ui() elif src == code_chk: _sync_code_mode() self._sync_params_to_block() def actionPerformed(self, ev): cmd = u'' try: cmd = unicode(ev.ActionCommand) except Exception: pass if cmd == u'AddBlockBtn': _add_block_empty() return if cmd == u'DelBlockBtn': bi = _block_index_selected() if 0 <= bi < len(norm_blocks): del norm_blocks[bi] form_block_index[0] = -1 _refresh_blocks(selected_index=max(0, bi - 1)) _switch_block_form(_block_index_selected()) return h = _PluginDlgHandler() try: blocks_lb.addItemListener(h) sheet_ed.addTextListener(h) ref_combo.addTextListener(h) code_ed.addTextListener(h) extra_ed.addTextListener(h) code_chk.addItemListener(h) same_all_chk.addItemListener(h) except Exception: pass for btn in ('AddBlockBtn', 'DelBlockBtn'): try: ctl = dlg.getControl(btn) ctl.addActionListener(h) ctl.setActionCommand(btn) except Exception: pass _refresh_blocks(0) _load_block_to_form(0 if len(norm_blocks) > 0 else -1) _sync_same_all_ui() if dlg.execute() != 1: return None bi = form_block_index[0] if bi < 0: bi = _block_index_selected() if bi < 0 and len(norm_blocks) > 0: bi = 0 if bi >= 0: if not _apply_form_to_block(bi): _show_message(u'Укажите ссылку functions_*.py#имя или Python-код.', dialog=dlg, doc=doc) return None same_on = False try: same_on = bool(same_all_chk.getState()) except Exception: pass out_blocks = [] if same_on: if len(norm_blocks) > 0: b = dict(norm_blocks[0]) b.pop('sheet', None) out_blocks.append(b) else: out_blocks = [{'v': 1, 'fn': _pw_cfg.PLUGIN_FUNCTION_KEY}] else: out_blocks = [dict(b) for b in norm_blocks] for b in out_blocks: ref_or_code = b.get('code') or b.get('ref') or b.get('c') or u'' if unicode(ref_or_code).strip() == u'': _show_message(u'В каждом блоке нужна ссылка или Python-код.', dialog=dlg, doc=doc) return None val = unicode(ref_or_code).strip() if not _pp_allows_freeform_c(val): _show_message(u'Некорректная ссылка или код в блоке.', dialog=dlg, doc=doc) return None use_json = True try: use_json = bool(dlg.getControl('JsonChk').getState()) except Exception: pass if use_json: try: c_out = json.dumps(out_blocks, ensure_ascii=False, separators=(',', ':')) except Exception: c_out = u'' return (c_out, u'') b0 = out_blocks[0] if len(out_blocks) > 0 else {} if b0.get('code'): return (unicode(b0.get('code') or u''), unicode(b0.get('extra') or u'')) return (unicode(b0.get('ref') or u''), unicode(b0.get('extra') or u'')) def _build_wizard_catalog(): """Каталог параметров визарда: имя, режим ввода, повторяемость.""" cw = _try_import_collect() p_dt_legacy = u'Дата_источника_во_второй__колонке' if cw is not None: _ensure_collect_pp_maps(cw) p_files = cw.P_MERGE_FILES p_source_extra = getattr(cw, 'P_MERGE_SOURCE_EXTRA', u'Доп_Параметры_Источника') p_mode = cw.P_MERGE_MODE p_sheets = cw.P_MERGE_SHEETS p_columns = cw.P_MERGE_COLUMNS p_start_row = cw.P_MERGE_START_ROW p_start_col = cw.P_MERGE_START_COL p_row_count = cw.P_MERGE_ROW_COUNT p_col_count = cw.P_MERGE_COL_COUNT p_header = cw.P_MERGE_HEADER_ROW p_path = cw.P_MERGE_PATH_DISPLAY p_dt = cw.P_MERGE_DATETIME_DISPLAY p_dt_legacy = getattr(cw, 'P_MERGE_DATETIME_DISPLAY_LEGACY', u'Дата_источника_во_второй__колонке') p_transfer = getattr(cw, 'P_MERGE_DATA_TRANSFER', u'Способ_переноса') p_xml_convert = getattr(cw, 'P_MERGE_XML_CONVERT_TO_NUMBERS', u'MERGE_XML_CONVERT_TO_NUMBERS') p_xml_dt_formats = getattr(cw, 'P_MERGE_XML_DATETIME_FORMATS', u'MERGE_XML_DATETIME_FORMATS') p_src_subfolders = getattr(cw, 'P_MERGE_SOURCE_VIEW_SUBFOLDERS', u'MERGE_SOURCE_VIEW_SUBFOLDERS') p_src_exclude = getattr(cw, 'P_MERGE_SOURCE_EXCLUDE_MASK', u'MERGE_SOURCE_EXCLUDE_MASK') p_debug = getattr(cw, 'P_MERGE_DEBUG', u'MERGE_DEBUG') p_debug_console = getattr(cw, 'P_MERGE_DEBUG_CONSOLE_LOG', u'MERGE_DEBUG_CONSOLE_LOG_TO_WORKSHEET') p_undelete = cw.P_MERGE_SHEETS_UNDELETE p_force_delete = getattr(cw, 'P_MERGE_SHEETS_FORCE_DELETE', u'Листы_удалить') p_skip_rows = cw.P_MERGE_SKIP_SOURCE_ROWS p_autofilter = getattr(cw, 'P_MERGE_RESULT_AUTOFILTER', u'Автофильтр_результата') p_freeze = getattr(cw, 'P_MERGE_FREEZE_HEADER', u'Закрепить_заголовок') p_progress = getattr(cw, 'P_MERGE_PROGRESS_UI', u'Отображение_прогресса') p_pp_range = cw.P_MERGE_POSTPROCESS_RANGE p_pp_row = cw.P_MERGE_POSTPROCESS_ROW p_pp_xml = getattr(cw, 'P_MERGE_POSTPROCESS_XML', u'Постобработка_xml') p_final = getattr(cw, 'P_MERGE_FINAL_PROCESSING', u'Финальная_обработка') p_vlookup = getattr(cw, 'P_MERGE_VLOOKUP', u'ВПР') p_merge_sheets = getattr(cw, 'P_MERGE_SHEETS_INTO_ONE', u'объединить_листы_в_один') p_split_sheets = getattr(cw, 'P_SPLIT_SHEETS', u'разделить_листы') p_cell = cw.P_MERGE_CELL_TO_COLUMN p_var_map = getattr(cw, 'P_MERGE_ADD_TO_VARIABLES_MAP', u'Добавить_в_карту_переменных') p_delete_top = getattr(cw, 'P_MERGE_DELETE_TOP_ROWS', u'удаление_верхних_строк') p_copy_auto_trim = getattr(cw, 'P_MERGE_COPY_AUTO_TRIM', u'Автообрезка_листов') p_paste_formats = getattr(cw, 'P_MERGE_PASTE_FORMATS', u'MERGE_PASTE_FORMATS') try: pp_range_choices = sorted(list(cw.MERGE_RESULT_POSTPROCESS_RANGE_MAP.keys())) except Exception: pp_range_choices = sorted(_pw_cfg._FALLBACK_PP_RANGE_CHOICES) if len(pp_range_choices) == 0: pp_range_choices = sorted(_pw_cfg._FALLBACK_PP_RANGE_CHOICES) try: pp_row_choices = sorted(list(cw.MERGE_RESULT_POSTPROCESS_ROW_MAP.keys())) except Exception: pp_row_choices = sorted(_pw_cfg._FALLBACK_PP_ROW_CHOICES) if len(pp_row_choices) == 0: pp_row_choices = sorted(_pw_cfg._FALLBACK_PP_ROW_CHOICES) try: pp_xml_choices = sorted(list(cw.MERGE_RESULT_POSTPROCESS_XML_MAP.keys())) except Exception: pp_xml_choices = [] if len(pp_xml_choices) == 0: pp_xml_choices = sorted(_pw_cfg._FALLBACK_PP_XML_CHOICES) try: final_choices = sorted(list(cw.MERGE_FINAL_PROCESSING_MAP.keys())) except Exception: final_choices = sorted(_pw_cfg._FALLBACK_FINAL_CHOICES) if len(final_choices) == 0: final_choices = sorted(_pw_cfg._FALLBACK_FINAL_CHOICES) file_range, file_row = _scan_functions_pp_choices() file_final = _scan_functions_final_choices() pp_range_choices = _pp_catalog_choices_with_plugin(pp_range_choices) pp_row_choices = _pp_catalog_choices_with_plugin(pp_row_choices) final_choices = _pp_catalog_choices_with_plugin(final_choices) pp_range_rest = _merge_pp_rest_hints(_pw_cfg._FALLBACK_PP_RANGE_REST_HINTS, getattr(cw, 'MERGE_POSTPROCESS_RANGE_REST_HINTS', None)) pp_row_rest = _merge_pp_rest_hints(_pw_cfg._FALLBACK_PP_ROW_REST_HINTS, getattr(cw, 'MERGE_POSTPROCESS_ROW_REST_HINTS', None)) pp_xml_rest = _merge_pp_rest_hints(_pw_cfg._FALLBACK_PP_XML_REST_HINTS, getattr(cw, 'MERGE_POSTPROCESS_XML_REST_HINTS', None)) final_rest = _merge_pp_rest_hints(_pw_cfg._FALLBACK_FINAL_REST_HINTS, getattr(cw, 'MERGE_FINAL_PROCESSING_REST_HINTS', None)) advanced = list(cw.MERGE_ADVANCED_PARAM_NAMES) advanced_defaults = cw.MERGE_ADVANCED_DEFAULTS else: p_files = u'Файлы-Источники' p_source_extra = u'Доп_Параметры_Источника' p_mode = u'Режим' p_sheets = u'Листы' p_columns = u'Столбцы' p_start_row = u'Начало_Строка' p_start_col = u'Начало_Столбец' p_row_count = u'Число_Строк' p_col_count = u'Число_Столбцов' p_header = u'Строка_Заголовков' p_path = u'Источник_в_первой_колонке' p_dt = getattr(cw, 'P_MERGE_DATETIME_DISPLAY', u'Дата_источника_во_второй_колонке') p_dt_legacy = getattr(cw, 'P_MERGE_DATETIME_DISPLAY_LEGACY', u'Дата_источника_во_второй__колонке') p_transfer = u'Способ_переноса' p_xml_convert = u'MERGE_XML_CONVERT_TO_NUMBERS' p_xml_dt_formats = u'MERGE_XML_DATETIME_FORMATS' p_src_subfolders = u'MERGE_SOURCE_VIEW_SUBFOLDERS' p_src_exclude = u'MERGE_SOURCE_EXCLUDE_MASK' p_debug = u'MERGE_DEBUG' p_debug_console = u'MERGE_DEBUG_CONSOLE_LOG_TO_WORKSHEET' p_undelete = u'Листы_не_удалять' p_force_delete = u'Листы_удалить' p_skip_rows = u'Пропуск_строк_источника' p_autofilter = u'Автофильтр_результата' p_freeze = u'Закрепить_заголовок' p_progress = u'Отображение_прогресса' p_pp_range = u'Постобработка_Диапазон' p_pp_row = u'Постобработка_Строка' p_pp_xml = u'Постобработка_xml' p_final = u'Финальная_обработка' p_vlookup = u'ВПР' p_merge_sheets = u'объединить_листы_в_один' p_split_sheets = u'разделить_листы' p_cell = u'Ячейка_В_Столбец' p_var_map = u'Добавить_в_карту_переменных' p_delete_top = u'удаление_верхних_строк' p_copy_auto_trim = u'Автообрезка_листов' p_paste_formats = u'MERGE_PASTE_FORMATS' pp_range_choices = sorted(_pw_cfg._FALLBACK_PP_RANGE_CHOICES) pp_row_choices = sorted(_pw_cfg._FALLBACK_PP_ROW_CHOICES) pp_xml_choices = sorted(_pw_cfg._FALLBACK_PP_XML_CHOICES) final_choices = sorted(_pw_cfg._FALLBACK_FINAL_CHOICES) file_range, file_row = _scan_functions_pp_choices() file_final = _scan_functions_final_choices() pp_range_choices = _pp_catalog_choices_with_plugin(pp_range_choices) pp_row_choices = _pp_catalog_choices_with_plugin(pp_row_choices) final_choices = _pp_catalog_choices_with_plugin(final_choices) pp_range_rest = dict(_pw_cfg._FALLBACK_PP_RANGE_REST_HINTS) pp_row_rest = dict(_pw_cfg._FALLBACK_PP_ROW_REST_HINTS) pp_xml_rest = dict(_pw_cfg._FALLBACK_PP_XML_REST_HINTS) final_rest = dict(_pw_cfg._FALLBACK_FINAL_REST_HINTS) advanced = [] advanced_defaults = {} choice_mode = [u'На один лист', u'На разные листы', u'Копирование листов', u'Текущие листы'] choice_path = [u'Не_выводить', u'Коротко', u'Длинно'] choice_bool = [u'Да', u'Нет'] choice_progress = [u'Статусная_строка', u'Диалог'] choice_dt = [u'Не_выводить', u'Коротко', u'Длинно'] choice_transfer = [u'По_API', u'Буфер_по_заголовкам', u'Буфер_по_позиции', u'xml_excel_ods'] def _hint(name, extra=u''): text = _pw_cfg.WIZARD_HINTS.get(name, extra) if cw is not None and name in advanced: default = advanced_defaults.get(name, u'') return u'Расширенный параметр. Целое число в B.\nПустое B — значение по умолчанию из кода: %s.' % default return text catalog = [{'name': _pw_cfg.P_MERGE_COMMENT, 'mode': 'text', 'multi_row': False, 'hint': _hint(_pw_cfg.P_MERGE_COMMENT)}, {'name': p_files, 'mode': 'files', 'multi_row': False, 'multi_column': True, 'hint': _hint(p_files)}, {'name': p_source_extra, 'mode': 'source_extra', 'multi_row': False, 'multi_column': True, 'hint': _hint(p_source_extra)}, {'name': p_mode, 'mode': 'choice', 'choices': choice_mode, 'multi_row': False, 'hint': _hint(p_mode), 'inline_transfer': True, 'transfer_choices': choice_transfer}, {'name': p_sheets, 'mode': 'text', 'multi_row': False, 'hint': _hint(p_sheets), 'col_c_subdialog': True, 'col_c_subdialog_kind': 'sheets_include_filter', 'col_c_keep_b': True}, {'name': p_columns, 'mode': 'text', 'multi_row': False, 'hint': _hint(p_columns)}, {'name': p_start_row, 'mode': 'number', 'multi_row': False, 'hint': _hint(p_start_row)}, {'name': p_start_col, 'mode': 'text', 'multi_row': False, 'hint': _hint(p_start_col)}, {'name': p_row_count, 'mode': 'number', 'multi_row': False, 'hint': _hint(p_row_count)}, {'name': p_col_count, 'mode': 'number', 'multi_row': False, 'hint': _hint(p_col_count)}, {'name': p_header, 'mode': 'number', 'multi_row': False, 'hint': _hint(p_header)}, {'name': p_path, 'mode': 'choice', 'choices': choice_path, 'multi_row': False, 'hint': _hint(p_path)}, {'name': p_dt, 'mode': 'choice', 'choices': choice_dt, 'multi_row': False, 'hint': _hint(p_dt)}, {'name': p_dt_legacy, 'mode': 'choice', 'choices': choice_dt, 'multi_row': False, 'hint': _hint(p_dt_legacy)}, {'name': p_transfer, 'mode': 'choice', 'choices': choice_transfer, 'multi_row': False, 'hint': _hint(p_transfer)}, {'name': p_xml_convert, 'mode': 'choice', 'choices': choice_bool, 'multi_row': False, 'hint': _hint(p_xml_convert)}, {'name': p_xml_dt_formats, 'mode': 'text', 'multi_row': False, 'hint': _hint(p_xml_dt_formats)}, {'name': p_src_subfolders, 'mode': 'choice', 'choices': choice_bool, 'multi_row': False, 'hint': _hint(p_src_subfolders)}, {'name': p_src_exclude, 'mode': 'text', 'multi_row': False, 'hint': _hint(p_src_exclude)}, {'name': p_debug, 'mode': 'choice', 'choices': choice_bool, 'multi_row': False, 'hint': _hint(p_debug)}, {'name': p_debug_console, 'mode': 'choice', 'choices': choice_bool, 'multi_row': False, 'hint': _hint(p_debug_console)}, {'name': p_undelete, 'mode': 'text', 'multi_row': False, 'hint': _hint(p_undelete), 'col_c_subdialog': True, 'col_c_subdialog_kind': 'sheets_undelete_filter', 'col_c_keep_b': True}, {'name': p_force_delete, 'mode': 'text', 'multi_row': False, 'hint': _hint(p_force_delete), 'col_c_subdialog': True, 'col_c_subdialog_kind': 'sheets_force_delete_filter', 'col_c_keep_b': True}, {'name': p_skip_rows, 'mode': 'text', 'multi_row': False, 'hint': _hint(p_skip_rows), 'col_c_subdialog': True, 'col_c_subdialog_kind': 'skip_source_rows'}, {'name': p_delete_top, 'mode': 'text', 'multi_row': False, 'hint': _hint(p_delete_top), 'col_c_subdialog': True, 'col_c_subdialog_kind': 'delete_top_rows'}, {'name': p_copy_auto_trim, 'mode': 'choice', 'choices': choice_bool, 'multi_row': False, 'hint': _hint(p_copy_auto_trim)}, {'name': p_paste_formats, 'mode': 'choice', 'choices': choice_bool, 'multi_row': False, 'hint': _hint(p_paste_formats)}, {'name': p_autofilter, 'mode': 'choice', 'choices': choice_bool, 'multi_row': False, 'hint': _hint(p_autofilter), 'col_c_subdialog': True, 'col_c_subdialog_kind': 'sheet_list_fn', 'col_c_fn_key': u'автофильтр'}, {'name': p_freeze, 'mode': 'choice', 'choices': choice_bool, 'multi_row': False, 'hint': _hint(p_freeze), 'col_c_subdialog': True, 'col_c_subdialog_kind': 'sheet_list_fn', 'col_c_fn_key': u'закрепить_заголовок'}, {'name': p_progress, 'mode': 'choice', 'choices': choice_progress, 'multi_row': False, 'hint': _hint(p_progress)}, {'name': p_pp_range, 'mode': 'postprocess_range', 'choices': pp_range_choices, 'rest_hints': pp_range_rest, 'multi_row': True, 'hint': _hint(p_pp_range)}, {'name': p_pp_row, 'mode': 'postprocess_row', 'choices': pp_row_choices, 'rest_hints': pp_row_rest, 'multi_row': True, 'hint': _hint(p_pp_row)}, {'name': p_pp_xml, 'mode': 'postprocess_range', 'choices': pp_xml_choices, 'rest_hints': pp_xml_rest, 'multi_row': True, 'hint': _hint(p_pp_xml)}, {'name': p_final, 'mode': 'postprocess_range', 'choices': final_choices, 'rest_hints': final_rest, 'multi_row': True, 'hint': _hint(p_final)}, {'name': p_vlookup, 'mode': 'vlookup', 'multi_row': True, 'hint': _hint(p_vlookup)}, {'name': p_merge_sheets, 'mode': 'merge_sheets', 'multi_row': True, 'hint': u'Объединить несколько листов в один. JSON в колонке C (sheets, dest_sheet, header_row, with_formatting).'}, {'name': p_split_sheets, 'mode': 'split_sheets', 'multi_row': True, 'hint': u'Разделить листы (заглушка). JSON в C или пусто.'}, {'name': p_cell, 'mode': 'cell_to_column', 'multi_row': True, 'hint': _hint(p_cell), 'col_c_subdialog': True, 'col_c_subdialog_kind': 'cell_to_column'}, {'name': p_var_map, 'mode': 'cell_to_column', 'multi_row': True, 'hint': _hint(p_var_map), 'col_c_subdialog': True, 'col_c_subdialog_kind': 'add_to_variables_map'}] for adv in advanced: catalog.append({'name': adv, 'mode': 'number', 'multi_row': False, 'hint': _hint(adv)}) return catalog def _cell_string(cell): if cell is None: return u'' s = cell.String if s is None: return u'' return unicode(s).strip() def _param_match(name, key): return unicode(name).strip().lower() == unicode(key).strip().lower() def _param_sheet_name_is_disabled(name): cw = _try_import_collect() if cw is not None and hasattr(cw, 'param_sheet_name_is_disabled'): return cw.param_sheet_name_is_disabled(name) return unicode(name or u'').strip().startswith(u'__') def _param_canonical_sheet_name(name): cw = _try_import_collect() if cw is not None and hasattr(cw, 'param_canonical_sheet_name'): return unicode(cw.param_canonical_sheet_name(name)) s = unicode(name or u'').strip() if _param_sheet_name_is_disabled(s): return s[2:] return s def _param_sheet_name_for_save(canonical_name, disabled=False): cw = _try_import_collect() nm = unicode(canonical_name or u'').strip() force_delete_top_case = nm.casefold() == u'удаление_верхних_строк'.casefold() if force_delete_top_case: nm = u'Удаление_верхних_строк' if cw is not None and hasattr(cw, 'param_sheet_name_for_save'): out = unicode(cw.param_sheet_name_for_save(canonical_name, disabled)) else: out = nm if nm.casefold() != u'удаление_верхних_строк'.casefold(): if disabled: if _param_sheet_name_is_disabled(nm): out = nm else: out = u'__' + nm elif _param_sheet_name_is_disabled(nm): out = nm[2:] if force_delete_top_case: if disabled: out_u = unicode(out or u'').strip() if out_u.startswith(u'__'): return out_u[:2] + u'Удаление_верхних_строк' return u'__' + u'Удаление_верхних_строк' return u'Удаление_верхних_строк' return out def _sheet_name_matches_param(sheet_name, param_name): if unicode(sheet_name or u'').strip() == u'': return False if _param_match(sheet_name, param_name): return True return _param_match(_param_canonical_sheet_name(sheet_name), param_name) def _param_row_matches_name(rname, canonical_name): if unicode(rname or u'').strip() == u'': return True return _sheet_name_matches_param(rname, canonical_name) def _write_param_row_sheet_name(sheet, row, canonical_name, disabled, catalog=None): if sheet is None or row is None or int(row) < 0: return row = int(row) with _pw_timed(u'write_row_sheet_name A=%r disabled=%s' % (canonical_name, disabled)): with _pw_timed(u'cell A String'): sheet.getCellByPosition(0, row).String = _param_sheet_name_for_save(canonical_name, disabled) with _pw_timed(u'apply_row_disabled_style'): _param_apply_row_disabled_style(sheet, row, disabled, catalog=catalog) def _param_is_value_column_header(text): return unicode(text or u'').strip().lower().startswith(u'значение') def _param_sheet_header_value_max_col(sheet, max_scan=40, fallback=1): """Правый индекс столбца (B=1) по подряд идущим «Значение N» в строке 0.""" if sheet is None: return int(fallback) last = 0 col = 1 while col < max_scan: hdr = _cell_string(sheet.getCellByPosition(col, 0)) if _param_is_value_column_header(hdr): last = col elif last > 0: break col = col + 1 if last <= 0: return int(fallback) return last def _param_row_max_col(sheet, row, catalog=None): row = int(row) name = _cell_string(sheet.getCellByPosition(0, row)) max_col = _param_sheet_header_value_max_col(sheet) if catalog is not None: spec = _find_catalog_spec(catalog, name) if spec is not None and spec.get('mode') == 'vlookup': max_col = max(max_col, 5) if spec is not None and spec.get('mode') == 'files': row_last = _param_sheet_row_last_col(sheet, row, max_cols=40) if row_last > max_col: max_col = row_last return max_col def _param_row_disabled_style_max_col(sheet, row, catalog=None): """Столбцы A + «Значение N» для заливки отключённой строки (без лишних пустых).""" row = int(row) name = _cell_string(sheet.getCellByPosition(0, row)) max_col = _param_sheet_header_value_max_col(sheet) if catalog is not None: spec = _find_catalog_spec(catalog, name) if spec is not None and spec.get('mode') == 'vlookup': max_col = max(max_col, 5) return max_col def _param_reset_cell_text_color(cell): if cell is None: return try: cell.CharAutoColor = True except Exception: pass try: cell.CharColor2 = 0 except Exception: pass try: cell.CharColor = 0 except Exception: pass def _param_set_cell_disabled_fill(cell): if cell is None: return cell.CellBackColor = _pw_cfg._PARAM_FILL_DISABLED cell.IsCellBackgroundTransparent = False _param_reset_cell_text_color(cell) def _param_clear_cell_background(cell): if cell is None: return try: cell.IsCellBackgroundTransparent = True except Exception: pass def _param_apply_row_disabled_style(sheet, row, disabled, catalog=None, bulk=False): """Тёмно-серая заливка всей строки параметра при отключении (__ в колонке A).""" if sheet is None or row is None or int(row) < 0: return row = int(row) detail = not bulk with _pw_timed_if(u'apply_row_disabled_style row=%d disabled=%s' % (row, disabled), detail): with _pw_timed_if(u'read name col A', detail): name = _cell_string(sheet.getCellByPosition(0, row)) with _pw_timed_if(u'row_disabled_style_max_col', detail): max_col = _param_row_disabled_style_max_col(sheet, row, catalog) comment_row = -1 if not disabled: with _pw_timed_if(u'scan comment_row 0..last_row', detail): try: last_row = _sheet_last_used_row(sheet, row) row_idx = 0 while row_idx <= last_row: if _param_match(_cell_string(sheet.getCellByPosition(0, row_idx)), _pw_cfg.P_MERGE_COMMENT): comment_row = row_idx row_idx = row_idx + 1 except Exception: pass with _pw_timed_if(u'style cells 0..%d' % max_col, detail): col = 0 while col <= max_col: cell = sheet.getCellByPosition(col, row) if disabled: _param_set_cell_disabled_fill(cell) else: if row == 0 and _param_match(name, u'Параметр'): _param_style_header_cell(cell) elif col == 0 and name != u'': _param_style_name_cell(cell) _param_write_hint_annotation(cell, _param_sheet_hint(name)) elif comment_row >= 0 and row == comment_row and (col >= 1): _param_style_value_cell(cell, wrap_top=True) _param_clear_cell_background(cell) else: _param_style_value_cell(cell, wrap_top=False) if col >= 1: _param_clear_cell_background(cell) _param_reset_cell_text_color(cell) col = col + 1 def _param_apply_disabled_rows_styles(sheet, catalog=None): """Подсветить все отключённые строки параметров на листе.""" if sheet is None: return with _pw_timed(u'apply_disabled_rows_styles (весь лист)'): with _pw_timed(u'last_used_row'): last_row = _sheet_last_used_row(sheet, _pw_cfg.MERGE_PARAM_SHEET_MAX_ROWS - 1) row_idx = 1 styled = 0 while row_idx <= last_row: name = _cell_string(sheet.getCellByPosition(0, row_idx)) if name != u'' and (not _param_match(name, u'Параметр')): _param_apply_row_disabled_style(sheet, row_idx, _param_sheet_name_is_disabled(name), catalog, bulk=True) styled += 1 row_idx = row_idx + 1 if _pw_cfg.PARAM_WIZARD_TIMING: _pw_time_log(u' styled rows: %d / %d' % (styled, last_row)) def _find_param_sheet(doc): """Лист параметров: активный, единственный или ошибка при нескольких.""" sheet, _err = _resolve_param_sheet_for_ui(doc, allow_create=False) return sheet def _list_param_sheet_names_for_wizard(doc): """Имена листов параметров в книге (отсортированы).""" if doc is None: return [] cw = _try_import_collect() if cw is not None: try: names = cw.list_param_sheet_names(doc) if len(names) > 0: return [unicode(n) for n in names] except Exception: pass out = [] try: sheets = doc.Sheets i = 0 while i < sheets.Count: sh = sheets.getByIndex(i) if unicode(sh.Name).startswith(_pw_cfg.MERGE_PARAM_SHEET_NAME): out.append(unicode(sh.Name)) i = i + 1 except Exception: pass out.sort() return out def _get_sheet_by_name(doc, name): if doc is None or name is None or unicode(name).strip() == u'': return None cw = _try_import_collect() if cw is not None: try: return cw.get_sheet(doc, unicode(name)) except Exception: pass try: return doc.Sheets.getByName(unicode(name)) except Exception: return None def _resolve_wizard_param_sheet(doc): """ Лист параметров для визарда: активный лист параметров или первый по счёту. Возвращает (sheet, names, default_name). """ names = _list_param_sheet_names_for_wizard(doc) if len(names) == 0: return (None, [], u'') active = u'' cw = _try_import_collect() if cw is not None: try: active = unicode(cw.get_active_param_sheet_name(doc) or u'') except Exception: pass pick = active if active in names else names[0] return (_get_sheet_by_name(doc, pick), names, pick) def _resolve_param_sheet_for_ui(doc, allow_create=False): """ Выбор листа параметров для визарда. Один лист — без выбора; несколько — активный лист параметров или первый. Возвращает (sheet, error_text). """ if doc is None: return (None, u'Не открыта книга Calc.') cw = _try_import_collect() if cw is not None: try: names = cw.list_param_sheet_names(doc) if len(names) == 0: if not allow_create: return (None, None) elif len(names) == 1: return (cw.get_sheet(doc, names[0]), None) else: _sh, _names, pick = _resolve_wizard_param_sheet(doc) if _sh is not None: return (_sh, None) except Exception: pass try: ctrl = doc.getCurrentController() if ctrl is not None: active = ctrl.getActiveSheet() if active is not None and unicode(active.Name).startswith(_pw_cfg.MERGE_PARAM_SHEET_NAME): return (active, None) except Exception: pass try: sheets = doc.Sheets found = [] i = 0 while i < sheets.Count: sh = sheets.getByIndex(i) if unicode(sh.Name).startswith(_pw_cfg.MERGE_PARAM_SHEET_NAME): found.append(sh) i = i + 1 if len(found) == 1: return (found[0], None) if len(found) > 1: found.sort(key=lambda sh: unicode(sh.Name)) return (found[0], None) except Exception: pass if allow_create: return (None, None) return (None, u'Лист «%s» не найден.' % _pw_cfg.MERGE_PARAM_SHEET_NAME) def _default_wizard_vlookup_source_path(): """Путь к демо-источнику для шаблона первого создания листа параметров.""" here = os.path.dirname(os.path.abspath(__file__)) root = os.path.dirname(here) return os.path.join(root, u'test', u'collect_workbooks', u'sources', u'vlookup', u'vlookup_data_low_old.xlsx') def _default_wizard_json(*blocks): """Компактный JSON-массив блоков для колонки C шаблона.""" import json return json.dumps(list(blocks), ensure_ascii=False, separators=(',', ':')) def _default_wizard_param_rows(macro_version): """ Шаблон при первом создании «Параметры_Объединения»: Копирование листов + xml_excel_ods + ВПР + Постобработка_xml (как нагрузочный тест low / vlookup_data_low_old.xlsx). """ see_c = _pw_cfg._PARAM_B_SEE_COLUMN_C src = _default_wizard_vlookup_source_path() j_skip_src = _default_wizard_json({u'v': 1, u'fn': u'пропуск_строк_источника', u'columns': [u'1'], u'mode': u'columns'}) j_del_top = _default_wizard_json({u'v': 1, u'fn': u'удаление_верхних_строк', u'n': 2, u'sheet': u'1_Заказы'}) j_rename = _default_wizard_json({u'v': 1, u'fn': u'переименовать_лист', u'new_name': u'Заказы', u'sheet': u'1_Заказы'}) j_rename_cols = _default_wizard_json({u'v': 1, u'fn': u'переименовать_столбцы', u'sheet': u'Заказы', u'mappings': [{u'old': u'Сумма_руб', u'new': u'Сумма'}]}) j_reorder_cols = _default_wizard_json({u'v': 1, u'fn': u'переставить_столбцы', u'sheet': u'Заказы', u'columns': [u'C', u'Сумма_руб'], u'position': u'_перед_', u'relative_column': u'Итого'}) j_vlookup = _default_wizard_json({u'v': 1, u'fn': u'впр', u'left': {u'sheet': u'Заказы', u'start_row': 4, u'start_col': 1, u'header_row': 3}, u'right': {u'sheet': u'1_Справочник_цен', u'start_row': 2, u'start_col': 1, u'header_row': 1}, u'keys_same': True, u'keys_left': [u'Код_позиции'], u'keys_right': [u'Код_позиции'], u'extract_columns': [u'B', u'C', u'D'], u'highlight_color': u'#FFF2CC', u'fill_duplicates': False, u'not_found_fill': u'#Н/Д', u'multi_match_one_cell': False, u'trim_keys': False, u'column_suffix': True}) j_skip_empty = _default_wizard_json({u'v': 1, u'fn': u'удаление_строк', u'mode': u'columns', u'columns': [u'1'], u'sheet': u'Заказы'}) j_freeze = _default_wizard_json({u'v': 1, u'fn': u'закрепить_заголовок', u'sheets': [u'Заказы']}) j_header = _default_wizard_json({u'v': 1, u'fn': u'заголовок_плюс_высота', u'height_mm': 15.0, u'h_align': u'center', u'v_align': u'center', u'fill_color': u'orange', u'font_color': u'белый', u'bold': False, u'font_size': 14.0, u'sheet': u'Заказы'}) j_auto_w = _default_wizard_json({u'v': 1, u'fn': u'авто_ширина', u'sheets': [u'Заказы']}) j_auto_h = _default_wizard_json({u'v': 1, u'fn': u'авто_высота', u'sheets': [u'Заказы']}) j_filter = _default_wizard_json({u'v': 1, u'fn': u'автофильтр', u'sheets': [u'Заказы']}) j_del_cols = _default_wizard_json({u'v': 1, u'fn': u'удалить_столбцы', u'markers': [u'Комментарий'], u'sheet': u'Заказы'}) j_row_h = _default_wizard_json({u'v': 1, u'fn': u'высота_строки', u'rows': u'all', u'height_mm': 15.0, u'sheet': u'Заказы'}, {u'v': 1, u'fn': u'высота_строки', u'rows': u'all', u'height_mm': 20.0}) return [(u'Параметр', [u'Значение 1', u'Значение 2', u'Значение 3', u'Значение 4', u'Значение 5']), (_pw_cfg.P_MERGE_COMMENT, [_pw_cfg._SCENARIO_01_COMMENT]), (_pw_cfg.P_MERGE_VERSION, [unicode(macro_version)]), (u'Отображение_прогресса', [u'Диалог']), (u'Файлы-Источники', [src]), (u'MERGE_DEBUG', [u'Да']), (u'MERGE_XML_CONVERT_TO_NUMBERS', [u'Да']), (u'Листы', [u'Заказы', u'Справочник_цен']), (u'Начало_Строка', [u'4', u'2']), (u'Начало_Столбец', [u'1', u'1']), (u'Число_Строк', [u'', u'']), (u'Число_Столбцов', [u'', u'']), (u'Строка_Заголовков', [u'3', u'1']), (u'Режим', [u'Копирование листов', u'xml_excel_ods']), (u'Пропуск_строк_источника', [see_c, j_skip_src]), (u'Удаление_верхних_строк', [see_c, j_del_top]), (_pw_cfg.P_MERGE_POSTPROCESS_XML, [u'переименовать_лист', j_rename]), (u'ВПР', [u'', j_vlookup]), (_pw_cfg.P_MERGE_POSTPROCESS_XML, [u'пропуск_пустых_строк', j_skip_empty]), (_pw_cfg.P_MERGE_POSTPROCESS_XML, [u'закрепить_заголовок', j_freeze]), (_pw_cfg.P_MERGE_POSTPROCESS_XML, [u'заголовок_плюс_высота', j_header]), (_pw_cfg.P_MERGE_POSTPROCESS_XML, [u'авто_ширина', j_auto_w]), (_pw_cfg.P_MERGE_POSTPROCESS_XML, [u'авто_высота', j_auto_h]), (_pw_cfg.P_MERGE_POSTPROCESS_XML, [u'автофильтр', j_filter]), (_pw_cfg.P_MERGE_POSTPROCESS_XML, [u'удалить_столбцы', j_del_cols]), (u'__' + _pw_cfg.P_MERGE_POSTPROCESS_XML, [u'высота_строки', j_row_h]), (u'', []), (u'', []), (u'', [])] def _ensure_macro_scripts_path(): here = os.path.dirname(os.path.abspath(__file__)) if here not in sys.path: sys.path.insert(0, here) def _wp_import_global_settings_lib(): try: from libre_macros_global_settings_lib import CUSTOM_NUMBER_FORMAT_LABEL, DATE_FORMAT_CHOICES, NUMBER_FORMAT_CHOICES, calc_number_format_choices, default_global_settings, global_settings_path, is_calc_friendly_number_format, load_global_settings, merge_flags_from_settings, normalize_global_settings, resolve_number_format_choice, save_global_settings, apply_runtime_font_settings, apply_runtime_param_sheet_prefix, DEFAULT_FONT_NAME, DEFAULT_FONT_SIZE, DEFAULT_PARAM_SHEET_PREFIX return {'CUSTOM_NUMBER_FORMAT_LABEL': CUSTOM_NUMBER_FORMAT_LABEL, 'DATE_FORMAT_CHOICES': DATE_FORMAT_CHOICES, 'NUMBER_FORMAT_CHOICES': NUMBER_FORMAT_CHOICES, 'calc_number_format_choices': calc_number_format_choices, 'is_calc_friendly_number_format': is_calc_friendly_number_format, 'default_global_settings': default_global_settings, 'global_settings_path': global_settings_path, 'load_global_settings': load_global_settings, 'merge_flags_from_settings': merge_flags_from_settings, 'normalize_global_settings': normalize_global_settings, 'resolve_number_format_choice': resolve_number_format_choice, 'save_global_settings': save_global_settings, 'apply_runtime_font_settings': apply_runtime_font_settings, 'apply_runtime_param_sheet_prefix': apply_runtime_param_sheet_prefix, 'DEFAULT_FONT_NAME': DEFAULT_FONT_NAME, 'DEFAULT_FONT_SIZE': DEFAULT_FONT_SIZE, 'DEFAULT_PARAM_SHEET_PREFIX': DEFAULT_PARAM_SHEET_PREFIX} except Exception as err: print(u'param_wizard: global_settings lib: %s' % err) return None def _wp_config_base(): if sys.platform == 'win32': base = os.environ.get('APPDATA', os.path.expanduser('~')) else: base = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config')) return os.path.join(base, 'libre-macros') def _wp_presets_dir(): path = os.path.join(_wp_config_base(), _pw_cfg._WP_PRESETS_SUBDIR) try: os.makedirs(path, exist_ok=True) except TypeError: if not os.path.isdir(path): try: os.makedirs(path) except OSError: pass except OSError: pass return path def _wp_presets_path(): return os.path.join(_wp_presets_dir(), _pw_cfg._WP_PRESETS_FILE) def _wp_legacy_presets_path(): return os.path.join(_wp_config_base(), _pw_cfg._WP_PRESETS_FILE) def _wp_resolve_read_path(): new_path = _wp_presets_path() legacy = _wp_legacy_presets_path() if os.path.isfile(new_path) and os.path.isfile(legacy): try: if os.path.getmtime(legacy) > os.path.getmtime(new_path): with open(legacy, 'rb') as f: data = f.read() with open(new_path, 'wb') as f: f.write(data) try: os.remove(legacy) except OSError: pass except Exception: pass return new_path if os.path.isfile(new_path): return new_path if os.path.isfile(legacy): try: _wp_presets_dir() with open(legacy, 'rb') as f: data = f.read() with open(new_path, 'wb') as f: f.write(data) try: os.remove(legacy) except OSError: pass except Exception: pass return new_path def _wp_resolve_write_path(): new_path = _wp_presets_path() legacy = _wp_legacy_presets_path() if os.path.isfile(legacy): try: os.remove(legacy) except OSError: pass return new_path def _wp_normalize_preset(obj): if obj is None or not isinstance(obj, dict): return None name = unicode(obj.get('name', u'')).strip() rows = obj.get('rows') if rows is None: rows = obj.get('parameters') if name == u'' or rows is None: return None out_rows = [] i = 0 while i < len(rows): r = rows[i] if not isinstance(r, dict): i = i + 1 continue out_rows.append({'name': unicode(r.get('name', u'')), 'values': [unicode(v) for v in r.get('values') or []]}) i = i + 1 if len(out_rows) == 0: return None return {'name': name, 'rows': out_rows} def _wp_load_presets_list(): path = _wp_resolve_read_path() if not os.path.isfile(path): return [] try: with open(path, 'rb') as f: raw = f.read() text = raw.decode('utf-8') data = json.loads(text) except Exception as err: print(u'param_wizard presets: read %s: %s' % (path, err)) return [] presets = [] if isinstance(data, list): i = 0 while i < len(data): p = _wp_normalize_preset(data[i]) if p is not None: presets.append(p) i = i + 1 elif isinstance(data, dict): items = data.get('presets') if isinstance(items, list): i = 0 while i < len(items): p = _wp_normalize_preset(items[i]) if p is not None: presets.append(p) i = i + 1 else: p = _wp_normalize_preset(data) if p is not None: presets.append(p) return presets def _wp_save_presets_list(presets): path = _wp_resolve_write_path() payload = {'version': _pw_cfg._WP_PRESETS_JSON_VER, 'presets': presets or []} text = json.dumps(payload, ensure_ascii=False, indent=2) try: _wp_presets_dir() with open(path, 'wb') as f: f.write(text.encode('utf-8')) legacy = _wp_legacy_presets_path() if legacy != path and os.path.isfile(legacy): try: os.remove(legacy) except OSError: pass if not os.path.isfile(path): return u'файл не создан: %s' % path return None except Exception as err: return unicode(err) def _wp_preset_names(presets): names = [] i = 0 while i < len(presets): names.append(presets[i]['name']) i = i + 1 return names def _wp_find_preset_by_name(presets, name): key = unicode(name).strip().lower() if key == u'': return None i = 0 while i < len(presets): if unicode(presets[i]['name']).strip().lower() == key: return presets[i] i = i + 1 return None def _wp_upsert_preset(presets, name, rows): name = unicode(name).strip() if name == u'': return (presets, u'Укажите имя пресета.') preset = {'name': name, 'rows': rows} out = [] replaced = False i = 0 while i < len(presets): if unicode(presets[i]['name']).strip().lower() == name.lower(): if not replaced: out.append(preset) replaced = True else: out.append(presets[i]) i = i + 1 if not replaced: out.append(preset) return (out, None) def _wp_delete_preset(presets, name): key = unicode(name).strip().lower() if key == u'': return (presets, u'Выберите пресет для удаления.') out = [] removed = False i = 0 while i < len(presets): if unicode(presets[i]['name']).strip().lower() == key: removed = True else: out.append(presets[i]) i = i + 1 if not removed: return (presets, u'Пресет не найден.') return (out, None) def _wp_delete_preset_and_save(presets, name): new_list, err = _wp_delete_preset(presets, name) if err: return (None, err) presets[:] = new_list path = _wp_resolve_write_path() err = _wp_save_presets_list(presets) if err: return (None, err) return (path, None) def _wp_read_row_values(sheet, row, max_col=30): vals = [] col = 1 last_nonempty = -1 while col < max_col: v = _cell_string(sheet.getCellByPosition(col, row)) vals.append(v) if v != u'': last_nonempty = len(vals) - 1 col = col + 1 if last_nonempty < 0: return [] return vals[:last_nonempty + 1] def _is_vlookup_param_col_header(text): s = unicode(text or u'').strip().casefold().replace(u' ', u'_') i = 0 while i < len(_pw_cfg._VLOOKUP_COL_HEADERS): h = _pw_cfg._VLOOKUP_COL_HEADERS[i] if s == unicode(h).strip().casefold().replace(u' ', u'_'): return True i = i + 1 return False def _generic_param_header_values(max_col): max_col = max(int(max_col or 0), _pw_cfg._PARAM_VALUE_COLUMNS) vals = [] c = 1 while c <= max_col: vals.append(u'Значение %d' % c) c = c + 1 return vals def _sanitize_param_header_values(vals): """В строке «Параметр» всегда «Значение N», не метки B–F от ВПР.""" if not vals: return vals i = 0 while i < len(vals): if _is_vlookup_param_col_header(vals[i]): return _generic_param_header_values(len(vals)) i = i + 1 return vals def _restore_param_sheet_header_row(sheet, max_col=None): """Восстановить строку 0: A=Параметр, B…=Значение 1, 2, …""" if sheet is None: return if max_col is None: max_col = _param_sheet_max_value_col(sheet) max_col = max(int(max_col or 0), _pw_cfg._PARAM_VALUE_COLUMNS) try: cell_a = sheet.getCellByPosition(0, 0) if not _param_match(_cell_string(cell_a), u'Параметр'): cell_a.String = u'Параметр' _param_style_header_cell(cell_a) except Exception: pass col = 1 while col <= max_col: try: cell = sheet.getCellByPosition(col, 0) cell.String = u'Значение %d' % col _param_style_header_cell(cell) except Exception: pass col = col + 1 def _wp_read_param_sheet_rows(sheet): if sheet is None: return [] rows = [] row = 0 h0 = _cell_string(sheet.getCellByPosition(0, 0)) h1 = _cell_string(sheet.getCellByPosition(0, 1)) if _param_match(h0, u'Параметр'): rows.append({'name': u'Параметр', 'values': _sanitize_param_header_values(_wp_read_row_values(sheet, 0))}) row = 1 elif _param_match(h1, u'Параметр'): rows.append({'name': u'Параметр', 'values': _sanitize_param_header_values(_wp_read_row_values(sheet, 1))}) row = 2 consecutive_empty = 0 while row < _pw_cfg.MERGE_PARAM_SHEET_MAX_ROWS: name = _cell_string(sheet.getCellByPosition(0, row)) if name == u'': consecutive_empty = consecutive_empty + 1 if consecutive_empty >= 8: break rows.append({'name': u'', 'values': []}) row = row + 1 continue consecutive_empty = 0 rows.append({'name': name, 'values': _wp_read_row_values(sheet, row)}) row = row + 1 return rows def _wp_meaningful_rows_count(rows): count = 0 i = 0 while i < len(rows or []): name = unicode(rows[i].get('name', u'')).strip() if name != u'' and (not _param_match(name, u'Параметр')): count = count + 1 i = i + 1 return count def _wp_clear_param_sheet(sheet, max_row=None, max_col=40): if sheet is None: return if max_row is None: max_row = _pw_cfg.MERGE_PARAM_SHEET_MAX_ROWS try: rng = sheet.getCellRangeByPosition(0, 0, int(max_col) - 1, int(max_row) - 1) rng.clearContents(7) except Exception: row = 0 while row < max_row: col = 0 while col < max_col: try: sheet.getCellByPosition(col, row).setString(u'') except Exception: pass col = col + 1 row = row + 1 def _wp_write_param_sheet_rows(sheet, rows): if sheet is None: return False _wp_clear_param_sheet(sheet) if rows is None or len(rows) == 0: return True max_vals = 3 i = 0 while i < len(rows): vals = rows[i].get('values') or [] if len(vals) > max_vals: max_vals = len(vals) i = i + 1 out_rows = list(rows) if len(out_rows) > 0 and _param_match(out_rows[0].get('name', u''), u'Параметр'): out_rows[0] = {'name': u'Параметр', 'values': _generic_param_header_values(max_vals)} elif len(out_rows) > 0: out_rows.insert(0, {'name': u'Параметр', 'values': _generic_param_header_values(max_vals)}) row_idx = 0 i = 0 while i < len(out_rows): item = out_rows[i] name = item.get('name', u'') vals = item.get('values') or [] sheet.getCellByPosition(0, row_idx).String = unicode(name) col = 1 vi = 0 while vi < max_vals: cell = sheet.getCellByPosition(col, row_idx) if vi < len(vals): cell.String = unicode(vals[vi]) else: cell.String = u'' vi = vi + 1 col = col + 1 row_idx = row_idx + 1 i = i + 1 _restore_param_sheet_header_row(sheet, max_vals) return True def _wp_save_preset_from_doc(doc, presets, name, sheet): name = unicode(name).strip() if name == u'': return (None, u'Укажите имя пресета (поле «Имя для сохранения»).') if sheet is None: sheet = _find_param_sheet(doc) if sheet is None: return (None, u'Лист «%s» не найден.' % _pw_cfg.MERGE_PARAM_SHEET_NAME) try: rows = _wp_read_param_sheet_rows(sheet) except Exception as err: return (None, u'Не удалось прочитать лист: %s' % err) if _wp_meaningful_rows_count(rows) < 1: return (None, u'На листе параметров нет данных для пресета.\nПрочитано строк: %d.' % len(rows)) new_list, uerr = _wp_upsert_preset(presets, name, rows) if uerr: return (None, uerr) presets[:] = new_list path = _wp_resolve_write_path() err = _wp_save_presets_list(presets) if err: return (None, u'Не удалось сохранить:\n%s' % err) if not os.path.isfile(path): return (None, u'Файл не создан: %s' % path) return (path, None) def _wp_apply_preset_rows(doc, sheet, rows, catalog): if sheet is None: sheet = _find_param_sheet(doc) if sheet is None: return u'Лист «%s» не найден.' % _pw_cfg.MERGE_PARAM_SHEET_NAME _wp_write_param_sheet_rows(sheet, rows) _restore_param_sheet_header_row(sheet) reformat_param_sheet_table(doc, sheet, catalog) return None def _wp_export_preset_to_file(preset, path): payload = {'name': preset['name'], 'rows': preset['rows']} text = json.dumps(payload, ensure_ascii=False, indent=2) try: with open(path, 'wb') as f: f.write(text.encode('utf-8')) return None except Exception as err: return unicode(err) def _wp_import_preset_from_file(path): try: with open(path, 'rb') as f: raw = f.read() data = json.loads(raw.decode('utf-8')) except Exception as err: return (None, unicode(err)) if isinstance(data, dict) and 'presets' in data: items = data.get('presets') or [] if len(items) == 0: return (None, u'В файле нет пресетов.') p = _wp_normalize_preset(items[0]) else: p = _wp_normalize_preset(data) if p is None: return (None, u'Некорректный формат файла пресета.') return (p, None) def _wp_dialog_parent(): try: frame = XSCRIPTCONTEXT.getDesktop().getCurrentFrame() if frame is not None: return frame.getContainerWindow() except Exception: pass return None def _wp_dialog_picker_parent(dialog=None): if dialog is not None: try: peer = dialog.getPeer() if peer is not None: return peer except Exception: pass return _wp_dialog_parent() def _wp_pick_json_file(ctx, save_as=False, suggested_name=u'preset.json', parent=None): try: sm = ctx.getServiceManager() picker = sm.createInstanceWithContext('com.sun.star.ui.dialogs.FilePicker', ctx) if save_as: try: from com.sun.star.ui.dialogs.TemplateDescription import FILESAVE_SIMPLE picker.initialize((FILESAVE_SIMPLE,)) except Exception: pass picker.setTitle(u'Сохранить пресет') else: try: from com.sun.star.ui.dialogs.TemplateDescription import FILEOPEN_SIMPLE picker.initialize((FILEOPEN_SIMPLE,)) except Exception: pass picker.setTitle(u'Выбрать файл пресета') try: picker.appendFilter('JSON', '*.json') picker.setCurrentFilter('JSON') except Exception: pass start_dir = _wp_presets_dir() if os.path.isdir(start_dir): try: picker.setDisplayDirectory(uno.systemPathToFileUrl(start_dir)) except Exception: pass if save_as and suggested_name: try: picker.setDefaultName(unicode(suggested_name)) except Exception: pass if parent is None: parent = _wp_dialog_parent() try: if parent is not None and hasattr(picker, 'setParent'): picker.setParent(parent) except Exception: pass if picker.execute() != 1: return None urls = picker.getFiles() if urls is None or len(urls) == 0: return None path = os.path.abspath(uno.fileUrlToSystemPath(urls[0])) if save_as and (not path.lower().endswith('.json')): path = path + '.json' return path except Exception as err: print(u'param_wizard pick_json: %s' % err) return None def _wp_doc_parent_window(doc=None): if doc is None: try: doc = XSCRIPTCONTEXT.getDocument() except Exception: doc = None if doc is not None: try: return doc.getCurrentController().getFrame().getContainerWindow() except Exception: pass return _wp_dialog_parent() def _wp_message_box_execute(doc, dialog, box_type, buttons, title, text): """MessageBox с перебором родителей (окно документа — первым: работает поверх модального визарда).""" toolkit = None try: toolkit = XSCRIPTCONTEXT.getDesktop().getToolkit() except Exception: pass if toolkit is None: try: ctx = XSCRIPTCONTEXT.getComponentContext() toolkit = ctx.ServiceManager.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) except Exception: toolkit = None if toolkit is None: return None parents = [] parent_doc = _wp_doc_parent_window(doc) if parent_doc is not None: parents.append(parent_doc) parent_desk = _wp_dialog_parent() if parent_desk is not None and parent_desk not in parents: parents.append(parent_desk) if dialog is not None: try: peer = dialog.getPeer() if peer is not None and peer not in parents: parents.append(peer) except Exception: pass title_u = unicode(title) text_u = unicode(text) i = 0 while i < len(parents): try: box = toolkit.createMessageBox(parents[i], box_type, buttons, title_u, text_u) return box.execute() except Exception: pass i = i + 1 try: box = toolkit.createMessageBox(None, box_type, buttons, title_u, text_u) return box.execute() except Exception: return None def _wp_suppress_param_action_messages(): """Глобальный флаг: не показывать короткий тост после сохранения/удаления параметра.""" gs_lib = _wp_import_global_settings_lib() if gs_lib is None: return False try: settings = gs_lib['load_global_settings']() return bool(settings.get('suppress_param_action_messages')) except Exception: return False def _wp_soft_gray_info_box(message, title=u'Параметры', parent_dialog=None, doc=None): """Короткое контрастное сообщение в стиле soft-gray (как «применить_формулу»).""" try: ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) dm = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialogModel', ctx) text = unicode(message or u'') caption = unicode(title or u'Параметры') w = 300 m = 12 btn_h = 22 btn_w = 80 lines = max(1, text.count(u'\n') + 1) wrap_extra = max(0, (len(text.replace(u'\n', u'')) // 44) - lines + 1) msg_h = max(40, min(180, (lines + wrap_extra) * 15 + 10)) dm.PositionX = 120 dm.PositionY = 100 dm.Width = w dm.Height = m + msg_h + 10 + btn_h + m dm.Title = caption msg = dm.createInstance('com.sun.star.awt.UnoControlFixedTextModel') msg.Name = 'SoftGrayMsgLabel' msg.Label = text msg.PositionX = m msg.PositionY = m msg.Width = w - 2 * m msg.Height = msg_h try: msg.MultiLine = True except Exception: pass try: msg.Align = 0 except Exception: pass try: msg.Border = 0 except Exception: pass dm.insertByName('SoftGrayMsgLabel', msg) ok = dm.createInstance('com.sun.star.awt.UnoControlButtonModel') ok.Name = 'SoftGrayOkBtn' ok.Label = u'OK' ok.PositionX = (w - btn_w) // 2 ok.PositionY = m + msg_h + 10 ok.Width = btn_w ok.Height = btn_h try: ok.DefaultButton = True except Exception: pass try: ok.PushButtonType = 1 # OK — закрывает диалог except Exception: pass dm.insertByName('SoftGrayOkBtn', ok) _wizard_apply_soft_gray_theme(dm, title_text=caption) dlg = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialog', ctx) dlg.setModel(dm) if not _nested_subdialog_create_peer(dlg, toolkit, doc=doc, parent_dialog=parent_dialog): dlg.createPeer(toolkit, None) _wizard_try_paint_titlebar(dlg) class _SoftGrayOkHandler(unohelper.Base, XActionListener): def disposing(self, ev): pass def actionPerformed(self, ev): try: dlg.endExecute() except Exception: pass handler = _SoftGrayOkHandler() try: ctl = dlg.getControl('SoftGrayOkBtn') if ctl is not None: ctl.addActionListener(handler) except Exception: pass dlg.execute() try: dlg.dispose() except Exception: pass return True except Exception as err: print(u'param_wizard: soft_gray_info_box: %s' % err) return False def _wp_notify(message, doc=None, dialog=None, title=u'Пресеты'): text = unicode(message) if _wp_soft_gray_info_box(text, title=title, parent_dialog=dialog, doc=doc): return if _wp_message_box_execute(doc, dialog, MESSAGEBOX, MSG_BUTTONS.BUTTONS_OK, title, text) is not None: return try: cw = _try_import_collect() if cw is not None: if cw.show_message_uno_infobox(doc, text): return cw.show_message(text, doc) return except Exception: pass print(text) def _wp_notify_param_action(message, doc=None, dialog=None, title=u'Параметры'): """Короткий тост после действия с параметром; уважает глобальный флаг отключения.""" if _wp_suppress_param_action_messages(): return _wp_notify(message, doc=doc, dialog=dialog, title=title) def _wp_confirm_overwrite_preset(name, doc=None, dialog=None): text = u'Пресет «%s» уже существует.\n\nПерезаписать его текущими параметрами листа?' % unicode(name) title = u'Перезапись пресета' answer = _wp_message_box_execute(doc, dialog, QUERYBOX, MSG_BUTTONS.BUTTONS_YES_NO, title, text) if answer is not None: return answer == YES try: cw = _try_import_collect() if cw is not None: return cw.ask_yes_no(text, doc) except Exception: pass print(text) return False def _wp_confirm_delete_preset(name, doc=None, dialog=None): text = u'Удалить пресет «%s»?' % unicode(name) title = u'Удаление пресета' answer = _wp_message_box_execute(doc, dialog, QUERYBOX, MSG_BUTTONS.BUTTONS_YES_NO, title, text) if answer is not None: return answer == YES try: cw = _try_import_collect() if cw is not None: return cw.ask_yes_no(text, doc) except Exception: pass print(text) return False def _wp_confirm_delete_param_row(param_name, row, doc=None, dialog=None): text = u'Удалить параметр «%s» (строка %d)?' % (unicode(param_name), int(row) + 1) title = u'Удаление параметра' answer = _wp_message_box_execute(doc, dialog, QUERYBOX, MSG_BUTTONS.BUTTONS_YES_NO, title, text) if answer is not None: return answer == YES try: cw = _try_import_collect() if cw is not None: return cw.ask_yes_no(text, doc) except Exception: pass print(text) return False def _wp_msg_saved(name, path): return u'Пресет «%s» сохранён в файл:\n%s' % (unicode(name), unicode(path)) def _wp_msg_exported(name, path): return u'Пресет «%s» экспортирован в файл:\n%s' % (unicode(name), unicode(path)) def _wp_msg_imported(name, path): return u'Пресет «%s» загружен из файла:\n%s' % (unicode(name), unicode(path)) def _wp_msg_deleted(name, path): return u'Пресет «%s» удалён.\nСписок пресетов: %s' % (unicode(name), unicode(path)) def _wp_msg_applied(name): return u'Пресет «%s» применён к листу «%s».' % (unicode(name), _pw_cfg.MERGE_PARAM_SHEET_NAME) def _wp_rows_first_value(rows, param_name, default=u''): i = 0 while i < len(rows or []): name = unicode(rows[i].get('name', u'')).strip() if _param_match(name, param_name): vals = rows[i].get('values') or [] if len(vals) > 0: v = unicode(vals[0]).strip() if v != u'': return v i = i + 1 return default def _wp_rows_values_nonempty(rows, param_name): out = [] i = 0 while i < len(rows or []): name = unicode(rows[i].get('name', u'')).strip() if _param_match(name, param_name): vals = rows[i].get('values') or [] j = 0 while j < len(vals): v = unicode(vals[j]).strip() if v != u'': out.append(v) j = j + 1 i = i + 1 return out def _wp_count_param_rows(rows, param_name): count = 0 i = 0 while i < len(rows or []): name = unicode(rows[i].get('name', u'')).strip() if _param_match(name, param_name): vals = rows[i].get('values') or [] if len(vals) > 0 and unicode(vals[0]).strip() != u'': count = count + 1 i = i + 1 return count def _wp_extract_comment_from_rows(rows): i = 0 while i < len(rows or []): name = unicode(rows[i].get('name', u'')).strip() if _param_match(name, _pw_cfg.P_MERGE_COMMENT): parts = [] vals = rows[i].get('values') or [] j = 0 while j < len(vals): t = unicode(vals[j]).strip() if t != u'': parts.append(t) j = j + 1 return u'\n'.join(parts) i = i + 1 return u'' def _wp_mode_label(mode): m = unicode(mode or u'').strip() if m == u'': return u'—' cw = _try_import_collect() if cw is not None: try: return cw._merge_mode_label(m) except Exception: pass return m def _wp_build_preset_summary(rows): mode = _wp_rows_first_value(rows, u'Режим', u'—') files = _wp_rows_values_nonempty(rows, u'Файлы-Источники') sheets = _wp_rows_first_value(rows, u'Листы', u'') cols = _wp_rows_first_value(rows, u'Столбцы', u'') start_row = _wp_rows_first_value(rows, u'Начало_Строка', u'') header_row = _wp_rows_first_value(rows, u'Строка_Заголовков', u'') path_disp = _wp_rows_first_value(rows, u'Источник_в_первой_колонке', u'') dt_disp = _wp_rows_first_value(rows, u'Дата_источника_во_второй_колонке', u'') if dt_disp == u'': dt_disp = _wp_rows_first_value(rows, u'Дата_источника_во_второй__колонке', u'') pp_range = _wp_count_param_rows(rows, _pw_cfg.P_MERGE_POSTPROCESS_RANGE) pp_row = _wp_count_param_rows(rows, _pw_cfg.P_MERGE_POSTPROCESS_ROW) pp_xml = _wp_count_param_rows(rows, _pw_cfg.P_MERGE_POSTPROCESS_XML) ctc = _wp_count_param_rows(rows, _pw_cfg.P_MERGE_CELL_TO_COLUMN) dtr = _wp_rows_first_value(rows, u'удаление_верхних_строк', u'') lines = [u'Режим сбора: %s' % _wp_mode_label(mode)] if len(files) == 0: lines.append(u'Источники: не заданы (заполните «Файлы-Источники» на листе)') else: lines.append(u'Файлов/масок в параметрах: %d' % len(files)) max_show = 5 fi = 0 while fi < len(files) and fi < max_show: lines.append(u' • %s' % files[fi]) fi = fi + 1 if len(files) > max_show: lines.append(u' … и ещё %d' % (len(files) - max_show)) if sheets != u'': lines.append(u'Листы: %s' % sheets) if cols != u'': lines.append(u'Столбцы: %s' % cols) if start_row != u'': lines.append(u'Данные с строки %s; строка заголовков: %s' % (start_row, header_row if header_row != u'' else u'—')) if path_disp != u'': lines.append(u'Колонка #Путь: %s' % path_disp) if dt_disp != u'': lines.append(u'Колонка #ДатаВремяФайла: %s' % dt_disp) extras = [] if pp_range > 0: extras.append(u'постобработка диапазона (%d шаг.)' % pp_range) if pp_row > 0: extras.append(u'постобработка строк (%d шаг.)' % pp_row) if pp_xml > 0: extras.append(u'Постобработка_xml (%d шаг.)' % pp_xml) if ctc > 0: extras.append(u'«Ячейка_В_Столбец» (%d)' % ctc) if dtr != u'': extras.append(u'«удаление_верхних_строк»') if len(extras) > 0: lines.append(u'Дополнительно: ' + u', '.join(extras)) return u'\n'.join(lines) def _wp_preset_info_texts(presets, preset_name): name = unicode(preset_name or u'').strip() preset = _wp_find_preset_by_name(presets, name) if name != u'' else None if preset is None: if name != u'': return (u'(новый пресет — комментарий будет взят с листа при сохранении)', u'Будет сохранён текущий лист «%s» под именем «%s».\nПосле сохранения здесь появится описание набора.' % (_pw_cfg.MERGE_PARAM_SHEET_NAME, name)) return (u'', u'Выберите пресет из списка или укажите имя для нового.\n«Применить к листу» — подставить параметры пресета на лист.\n«Сохранить» — записать текущий лист в JSON-пресеты.') rows = preset.get('rows') or [] comment = _wp_extract_comment_from_rows(rows) if comment == u'': comment = u'(комментарий не задан в параметре «Комментарий»)' return (comment, _wp_build_preset_summary(rows)) def _wp_example_preset_name(idx, kind_label, mode_label): return u'Я%02d — %s · %s' % (int(idx), kind_label, mode_label) def _wp_example_vlookup_json_tier(tier): """JSON ВПР для vlookup_{tier}.xlsx (Заказы + Справочник_цен, заголовок в строке 1).""" tiers = {u'1to1_low': ([u'Код_позиции'], False), u'1to1_middle': ([u'Регион', u'Код_товара'], False), u'1to1_high': ([u'Регион', u'Код_товара', u'Склад'], False), u'1toN_low': ([u'Код_позиции'], True), u'1toN_middle': ([u'Регион', u'Код_товара'], True), u'1toN_high': ([u'Регион', u'Код_товара', u'Склад'], True)} keys, fill_dup = tiers.get(unicode(tier), ([u'Код_позиции'], False)) return _default_wizard_json({u'v': 1, u'fn': u'впр', u'left': {u'sheet': u'Заказы', u'start_row': 2, u'start_col': 1, u'header_row': 1}, u'right': {u'sheet': u'Справочник_цен', u'start_row': 2, u'start_col': 1, u'header_row': 1}, u'keys_same': True, u'keys_left': list(keys), u'keys_right': list(keys), u'extract_columns': [u'B', u'C', u'D'], u'highlight_color': u'#FFF2CC', u'fill_duplicates': bool(fill_dup), u'not_found_fill': u'#Н/Д', u'multi_match_one_cell': False, u'trim_keys': False, u'column_suffix': True}) def _wp_macro_version_for_presets(): cw = _try_import_collect() if cw is not None: try: return unicode(cw.MACRO_VERSION) except Exception: pass return u'3.9.2' def _wp_row(name, *values): return {'name': unicode(name), 'values': [unicode(v) for v in values]} def _wp_example_src_preset(*parts): """Путь к sources: repo/test/… если есть, иначе bundled.""" try: from libre_macros_bundle_paths import repo_dev_sources_dir dev = repo_dev_sources_dir(__file__) if dev: out = dev i = 0 while i < len(parts): out = os.path.join(out, unicode(parts[i])) i = i + 1 return os.path.normpath(out) except Exception: pass return _wp_example_src(*parts) def _wp_example_sources_root(): try: from libre_macros_bundle_paths import resolve_test_sources_dir return resolve_test_sources_dir(__file__) except Exception: home = os.path.expanduser('~') return os.path.join(home, 'Documents', 'alter_office_macros', 'test', 'collect_workbooks', 'sources') def _wp_example_src(*parts): try: from libre_macros_bundle_paths import resolve_example_source_path return resolve_example_source_path(__file__, *parts) except Exception: return os.path.join(_wp_example_sources_root(), *parts) def _wp_block_range_rows(start_row=u'2', start_col=u'1', row_count=u'', col_count=u'', header_row=u'2'): return [_wp_row(u'Начало_Строка', start_row), _wp_row(u'Начало_Столбец', start_col), _wp_row(u'Число_Строк', row_count), _wp_row(u'Число_Столбцов', col_count), _wp_row(u'Строка_Заголовков', header_row)] def _wp_pp_range_rows(*specs): rows = [] i = 0 while i < len(specs): spec = specs[i] if isinstance(spec, tuple): fn = spec[0] rest = spec[1] if len(spec) > 1 else u'' else: fn = spec rest = u'' rows.append(_wp_row(_pw_cfg.P_MERGE_POSTPROCESS_RANGE, fn, rest)) i = i + 1 return rows def _wp_pp_xml_rows(*specs): rows = [] i = 0 while i < len(specs): spec = specs[i] if isinstance(spec, tuple): fn = spec[0] rest = spec[1] if len(spec) > 1 else u'' else: fn = spec rest = u'' rows.append(_wp_row(_pw_cfg.P_MERGE_POSTPROCESS_XML, fn, rest)) i = i + 1 return rows def _wp_pp_xml_rows_disabled(*specs): rows = [] pname = u'__' + _pw_cfg.P_MERGE_POSTPROCESS_XML i = 0 while i < len(specs): spec = specs[i] if isinstance(spec, tuple): fn = spec[0] rest = spec[1] if len(spec) > 1 else u'' else: fn = spec rest = u'' rows.append(_wp_row(pname, fn, rest)) i = i + 1 return rows def _wp_pp_range_rows_disabled(*specs): rows = [] pname = u'__' + _pw_cfg.P_MERGE_POSTPROCESS_RANGE i = 0 while i < len(specs): spec = specs[i] if isinstance(spec, tuple): fn = spec[0] rest = spec[1] if len(spec) > 1 else u'' else: fn = spec rest = u'' rows.append(_wp_row(pname, fn, rest)) i = i + 1 return rows def _wp_pp_row_rows(*specs): rows = [] i = 0 while i < len(specs): spec = specs[i] if isinstance(spec, tuple): fn = spec[0] rest = spec[1] if len(spec) > 1 else u'' else: fn = spec rest = u'' rows.append(_wp_row(_pw_cfg.P_MERGE_POSTPROCESS_ROW, fn, rest)) i = i + 1 return rows def _wp_parse_ctc_legacy(text): """Разбор legacy «A:1 -> Филиал» для пресетов.""" s = unicode(text or u'').strip() if s == u'' or u'->' not in s: return None left, right = s.split(u'->', 1) left = left.strip() right = right.strip() if left == u'' or right == u'' or u':' not in left: return None col_s, row_s = left.split(u':', 1) col_s = col_s.strip() row_s = row_s.strip() if col_s == u'' or row_s == u'': return None try: row_n = int(row_s) except Exception: return None if row_n < 1: return None out = {u'col': col_s, u'row': row_n, u'column': right} return out def _wp_ctc_json(col, row, column, sheet=None): block = {u'v': 1, u'fn': u'ячейка_в_столбец', u'col': unicode(col), u'row': int(row), u'column': unicode(column)} if sheet: block[u'sheet'] = unicode(sheet) return _default_wizard_json(block) def _wp_ctc_rows(*specs): rows = [] i = 0 while i < len(specs): spec = specs[i] parsed = None if isinstance(spec, dict): parsed = spec elif isinstance(spec, (list, tuple)): j = 0 while j < len(spec): parsed = _wp_parse_ctc_legacy(spec[j]) if parsed is not None: break j = j + 1 else: parsed = _wp_parse_ctc_legacy(spec) if parsed is not None: rows.append(_wp_row(_pw_cfg.P_MERGE_CELL_TO_COLUMN, _pw_cfg._PARAM_B_SEE_COLUMN_C, _wp_ctc_json(parsed[u'col'], parsed[u'row'], parsed[u'column'], parsed.get(u'sheet')))) i = i + 1 return rows def _wp_var_map_json(file_mask, sheet, cell, name): block = { u'v': 1, u'fn': u'добавить_в_карту_переменных', u'file': unicode(file_mask or u'*'), u'sheet': unicode(sheet or u'*'), u'cell': unicode(cell or u'A1'), u'name': unicode(name or u''), } return _default_wizard_json(block) def _wp_var_map_rows(*specs): rows = [] i = 0 while i < len(specs): spec = specs[i] i = i + 1 if not isinstance(spec, dict): continue name = unicode(spec.get(u'name') or u'').strip() cell = unicode(spec.get(u'cell') or u'').strip() if name == u'' or cell == u'': continue rows.append( _wp_row( _pw_cfg.P_MERGE_ADD_TO_VARIABLES_MAP, _pw_cfg._PARAM_B_SEE_COLUMN_C, _wp_var_map_json(spec.get(u'file') or u'*', spec.get(u'sheet') or u'*', cell, name), ) ) return rows def _wp_j(fn, **fields): block = {u'v': 1, u'fn': fn} for key in fields: block[key] = fields[key] return _default_wizard_json(block) def _wp_body_collect_header(sources, sheets, columns=None): srcs = tuple((_wp_example_src(s) for s in sources)) sheet_vals = sheets if isinstance(sheets, (list, tuple)) else (sheets,) body = [_wp_row(u'Отображение_прогресса', u'Диалог'), _wp_row(u'Файлы-Источники', *srcs), _wp_row(u'MERGE_DEBUG', u'Да'), _wp_row(u'Листы', *sheet_vals)] if columns: body.append(_wp_row(u'Столбцы', columns)) return body def _wp_body_finish_collect(body, xml_mode, merge_mode, pp_steps, path=u'Коротко', datetime=u'Не_выводить', ctc=None, var_map=None): body.extend(_wp_block_range_rows()) transfer = u'xml_excel_ods' if xml_mode else u'По_API' body.append(_wp_row(u'Режим', merge_mode, transfer)) if merge_mode in (u'На один лист', u'На разные листы'): body.append(_wp_row(u'Источник_в_первой_колонке', path)) body.append(_wp_row(u'Дата_источника_во_второй_колонке', datetime)) if ctc: body.extend(_wp_ctc_rows(*ctc)) if var_map: body.extend(_wp_var_map_rows(*var_map)) if xml_mode: body.append(_wp_row(u'MERGE_XML_CONVERT_TO_NUMBERS', u'Да')) if xml_mode: body.extend(_wp_pp_xml_rows(*pp_steps)) else: body.extend(_wp_pp_range_rows(*pp_steps)) return body def _build_wp_scenarios(): if _pw_cfg._WP_SCENARIOS is None: _pw_cfg._WP_SCENARIOS = ( ( u"На один лист", u"Сбор на один лист: три источника, CTC + карта переменных (Филиал/Дата_отчета), " u"сетка + заголовок + автоширина + автофильтр + закрепление.", _wp_scenario_body_01, ), ( u"На разные листы", u"Каждый лист отдельно: путь (коротко) и дата (длинно), перенос, авто-высота, сетка.", _wp_scenario_body_02, ), ( u"Копирование листов", u"Копирование всех листов source_01+02 без объединения: сетка, автоширина, фильтр, закрепление.", _wp_scenario_body_03, ), ( u"ВПР", u"Копирование + ВПР 1:1 low (vlookup_1to1_low.xlsx), зебра после подстановки.", _wp_scenario_body_04, ), ( u"Сводная", u"На один лист + сводная по Отдел (source_pivot.xlsx), CTC и карта переменных для формул, диапазон Merge: заголовок 2, данные с 3.", _wp_scenario_body_05, ), ( u"Структура данных", u"На один лист: сортировка, удалить_дубликаты, удалить_столбцы, замена_значений, конкатенация_столбцов.", _wp_scenario_body_06, ), ( u"Визуал", u"На один лист: градиент, раскрасить_блоки, ширина_столбцов, формат_деньги, зебра.", _wp_scenario_body_07, ), ( u"ВПР legacy", u"ВПР legacy (vlookup_data_low_old): пропуск строк, удаление верхних, переименование листа.", _wp_scenario_body_08, ), ( u"Смешанные форматы", u"На разные листы: .xlsm/.xls/.ods + товары; подсветка_по_порогу, количество_значений, серые_служебные, полосы_по_пути.", _wp_scenario_body_09, ), ) return _pw_cfg._WP_SCENARIOS def _wp_scenario_body_01(xml_mode): """На один лист: фильтр столбцов, CTC, оформление и автофильтр.""" body = _wp_body_collect_header((u'source_01.xlsx', u'source_02.xlsx', u'source_03.xlsx'), u'Отчет*', columns=u'ФИО;Отдел;Сумма_руб') pp = [(u'тонкая_сетка', _wp_j(u'тонкая_сетка')), (u'заголовок_плюс_высота', _wp_j(u'заголовок_плюс_высота', height_mm=15.0, h_align=u'center', v_align=u'center')), (u'авто_ширина', _wp_j(u'авто_ширина')), (u'автофильтр', _wp_j(u'автофильтр')), (u'закрепить_заголовок', _wp_j(u'закрепить_заголовок'))] vm = ({u'file': u'*source_01*', u'sheet': u'Заказы', u'cell': u'A1', u'name': u'Филиал'}, {u'file': u'*', u'sheet': u'*', u'cell': u'B2', u'name': u'Дата_отчета'}) return _wp_body_finish_collect(body, xml_mode, u'На один лист', pp, ctc=(u'A:1 -> Филиал',), var_map=vm) def _wp_scenario_body_02(xml_mode): """На разные листы: путь и дата в служебных колонках.""" body = _wp_body_collect_header((u'source_01.xlsx', u'source_02.xlsx'), u'Все') pp = [(u'перенос', _wp_j(u'перенос', wrap=True)), (u'авто_высота', _wp_j(u'авто_высота')), (u'тонкая_сетка', _wp_j(u'тонкая_сетка'))] return _wp_body_finish_collect(body, xml_mode, u'На разные листы', pp, path=u'Коротко', datetime=u'Длинно') def _wp_scenario_body_03(xml_mode): """Копирование листов целиком.""" return _wp_example_body_copy(xml_mode=xml_mode, variant={u'sources': (u'source_01.xlsx', u'source_02.xlsx'), u'sheets': u'Все', u'pp': (u'тонкая_сетка', u'авто_ширина', u'автофильтр', u'закрепить_заголовок')}) def _wp_scenario_body_04(xml_mode): """ВПР 1:1 low + зебра после подстановки.""" return _wp_example_body_vlookup(xml_mode=xml_mode, variant={u'src': (u'vlookup', u'vlookup_1to1_low.xlsx'), u'tier': u'1to1_low', u'extra_pp': (u'зебра_диапазон',)}) def _wp_scenario_body_05(xml_mode): """Сводная по Отдел из source_pivot.xlsx (эталон Я05).""" pivot_sheet = u'Сводная_Отдел' pp_tail = ((u'тонкая_сетка', _wp_j(u'тонкая_сетка', sheets=[u'Merge'])), (u'авто_ширина', _wp_j(u'авто_ширина', sheets=[u'Merge'])), (u'сетка', _wp_j(u'сетка', sheets=[pivot_sheet], width=u'тонкая', sheet=pivot_sheet, color=u'синий', include_header=True)), (u'авто_ширина', _wp_j(u'авто_ширина', sheets=[pivot_sheet]))) return _wp_example_body_pivot(xml_mode=xml_mode, variant={u'sources': (u'source_pivot.xlsx',), u'sources_preset': True, u'sheets': u'Отчет*', u'row_fields': [u'Отдел'], u'data_fields': [{u'field': u'Сумма_руб', u'function': u'SUM'}], u'pivot_sheet': pivot_sheet, u'ctc': u'A:1 -> Квартал', u'var_map': ({u'file': u'*', u'sheet': u'Отчет*', u'cell': u'A1', u'name': u'Квартал'},), u'pivot_header_row': u'1', u'pivot_data_start': u'', u'pivot_data_end': u'', u'pp_tail': pp_tail}) def _wp_scenario_body_06(xml_mode): """Структура данных: сортировка, удаление, замена, конкатенация.""" body = _wp_body_collect_header((u'source_03.xlsx',), u'Отчет_март') pp = [(u'сортировка', _wp_j(u'сортировка', keys=[{u'column': u'Отдел', u'desc': False}, {u'column': u'ФИО', u'desc': False}])), (u'удалить_дубликаты', _wp_j(u'удалить_дубликаты', key_columns=[u'ФИО'], keep=u'first')), (u'удалить_столбцы', _wp_j(u'удалить_столбцы', markers=[u'Комментарий'])), (u'замена_значений', _wp_j(u'замена_значений', columns=[u'ФИО'], find=[u'ООО'], replace=[u'_ПУСТО_'], case_insensitive=True)), (u'конкатенация_столбцов', _wp_j(u'конкатенация_столбцов', new_column=u'ФИО_Отдел', separator=u' / ', columns=[1, 3]))] return _wp_body_finish_collect(body, xml_mode, u'На один лист', pp, ctc=(u'A:1 -> Филиал',)) def _wp_scenario_body_07(xml_mode): """Визуал: градиент, блоки, ширина, деньги, зебра.""" body = _wp_body_collect_header((u'source_01.xlsx',), u'Отчет_январь') pp = [(u'градиент', _wp_j(u'градиент', marker=u'Сумма_руб', color_min=u'зеленый', color_max=u'красный', whole_row=True, sort=True)), (u'раскрасить_блоки', _wp_j(u'раскрасить_блоки', key_columns=[u'ФИО'], colors=[u'голубой', u'желтый'], sort=u'asc')), (u'ширина_столбцов', _wp_j(u'ширина_столбцов', width_mm=30, columns=u'all')), (u'формат_деньги', _wp_j(u'формат_деньги')), (u'зебра_диапазон', _wp_j(u'зебра_диапазон'))] return _wp_body_finish_collect(body, xml_mode, u'На один лист', pp) def _wp_scenario_body_08(xml_mode): """ВПР legacy: удаление строк, переименование листа, полная цепочка.""" return _wp_example_body_vlookup(xml_mode=xml_mode, variant={u'src': (u'vlookup', u'vlookup_data_low_old.xlsx'), u'legacy': True}) def _wp_scenario_body_09(xml_mode): """Смешанные форматы и каталог товаров: аналитика и подсветка.""" body = _wp_body_collect_header((u'source_01.xlsm', u'source_02.xls', u'source_03.ods', u'source_products.xlsx'), (u'Отчет*', u'Электроника', u'Одежда')) pp = [(u'подсветка_по_порогу', _wp_j(u'подсветка_по_порогу', marker=u'Цена', threshold=5000, color=u'светло_желтый')), (u'количество_значений', _wp_j(u'количество_значений', new_column=u'Кол_во', key_columns=[u'Товар'], trim=True)), (u'серые_служебные', _wp_j(u'серые_служебные')), (u'полосы_по_пути', _wp_j(u'полосы_по_пути')), (u'тонкая_сетка', _wp_j(u'тонкая_сетка'))] return _wp_body_finish_collect(body, xml_mode, u'На разные листы', pp, path=u'Коротко', datetime=u'Коротко') def _wp_scenario_preset(display_name, comment, body_rows, value_cols=5): rows = [_wp_row(u'Параметр', *[u'Значение %d' % (c + 1) for c in range(value_cols)]), _wp_row(_pw_cfg.P_MERGE_COMMENT, comment), _wp_row(_pw_cfg.P_MERGE_VERSION, _wp_macro_version_for_presets())] rows.extend(body_rows) i = 0 while i < 3: rows.append(_wp_row(u'')) i = i + 1 return {'name': display_name, 'rows': rows} def _wp_example_vlookup_json(): return _default_wizard_json({u'v': 1, u'fn': u'впр', u'left': {u'sheet': u'Заказы', u'start_row': 4, u'start_col': 1, u'header_row': 3}, u'right': {u'sheet': u'1_Справочник_цен', u'start_row': 2, u'start_col': 1, u'header_row': 1}, u'keys_same': True, u'keys_left': [u'Код_позиции'], u'keys_right': [u'Код_позиции'], u'extract_columns': [u'B', u'C', u'D'], u'highlight_color': u'#FFF2CC', u'fill_duplicates': False, u'not_found_fill': u'#Н/Д', u'multi_match_one_cell': False, u'trim_keys': False, u'column_suffix': True}) def _wp_example_pivot_json(as_values=True, source_sheet=u'Merge', sheet_name=u'Сводная', row_fields=None, data_fields=None, header_row=None, data_start=None, data_end=None): if row_fields is None: row_fields = [u'Отдел'] if data_fields is None: data_fields = [{u'field': u'Сумма_руб', u'function': u'SUM'}] block = {u'v': 1, u'fn': u'сводная_таблица', u'source_sheet': source_sheet, u'sheet_name': sheet_name, u'as_values': bool(as_values), u'row_fields': list(row_fields), u'column_fields': [], u'filter_fields': [], u'data_fields': list(data_fields)} if header_row is not None: block[u'header_row'] = unicode(header_row) if data_start is not None: block[u'data_start'] = unicode(data_start) if data_end is not None: block[u'data_end'] = unicode(data_end) return _default_wizard_json(block) def _wp_example_body_vlookup(xml_mode=True, variant=None): """Копирование листов + ВПР.""" v = variant if isinstance(variant, dict) else {} see_c = _pw_cfg._PARAM_B_SEE_COLUMN_C src_parts = v.get(u'src') or (u'vlookup', u'vlookup_data_low_old.xlsx') src = _wp_example_src(*src_parts) legacy = bool(v.get(u'legacy')) j_skip = _default_wizard_json({u'v': 1, u'fn': u'пропуск_строк_источника', u'columns': [u'1'], u'mode': u'columns'}) j_del_top = _default_wizard_json({u'v': 1, u'fn': u'удаление_верхних_строк', u'n': 2, u'sheet': u'1_Заказы'}) j_rename = _default_wizard_json({u'v': 1, u'fn': u'переименовать_лист', u'new_name': u'Заказы', u'sheet': u'1_Заказы'}) tier = v.get(u'tier') if tier: j_vl = _wp_example_vlookup_json_tier(tier) else: j_vl = _wp_example_vlookup_json() j_grid = _default_wizard_json({u'v': 1, u'fn': u'тонкая_сетка'}) j_aw = _default_wizard_json({u'v': 1, u'fn': u'авто_ширина', u'sheets': [u'Заказы']}) j_header = _default_wizard_json({u'v': 1, u'fn': u'заголовок_плюс_высота', u'height_mm': 15.0, u'h_align': u'center', u'v_align': u'center', u'sheet': u'Заказы'}) j_zebra = _default_wizard_json({u'v': 1, u'fn': u'зебра_диапазон', u'sheet': u'Заказы'}) transfer = u'xml_excel_ods' if xml_mode else u'По_API' start_rows = (u'4', u'2') if legacy else (u'2', u'2') header_rows = (u'3', u'1') if legacy else (u'1', u'1') body = [_wp_row(u'Отображение_прогресса', u'Диалог'), _wp_row(u'Файлы-Источники', src), _wp_row(u'MERGE_DEBUG', u'Да'), _wp_row(u'Листы', u'Заказы', u'Справочник_цен'), _wp_row(u'Начало_Строка', start_rows[0], start_rows[1]), _wp_row(u'Начало_Столбец', u'1', u'1'), _wp_row(u'Число_Строк', u'', u''), _wp_row(u'Число_Столбцов', u'', u''), _wp_row(u'Строка_Заголовков', header_rows[0], header_rows[1]), _wp_row(u'Режим', u'Копирование листов', transfer), _wp_row(u'Пропуск_строк_источника', see_c, j_skip)] if legacy: body.append(_wp_row(u'Удаление_верхних_строк', see_c, j_del_top)) if xml_mode: body.append(_wp_row(u'MERGE_XML_CONVERT_TO_NUMBERS', u'Да')) pp_tail = [] if legacy: pp_tail.append((u'переименовать_лист', j_rename)) body.extend(_wp_pp_xml_rows(*pp_tail)) body.append(_wp_row(u'ВПР', u'', j_vl)) pp_tail = [(u'тонкая_сетка', j_grid), (u'авто_ширина', j_aw)] else: pp_tail = [(u'тонкая_сетка', j_grid), (u'авто_ширина', j_aw)] body.extend(_wp_pp_xml_rows(*pp_tail)) body.append(_wp_row(u'ВПР', u'', j_vl)) pp_tail = [] extra_pp = v.get(u'extra_pp') or () i = 0 while i < len(extra_pp): fn = extra_pp[i] if fn == u'заголовок_плюс_высота': pp_tail.append((fn, j_header)) elif fn == u'зебра_диапазон': pp_tail.append((fn, j_zebra)) else: pp_tail.append((fn, _default_wizard_json({u'v': 1, u'fn': fn}))) i = i + 1 if len(pp_tail) > 0: body.extend(_wp_pp_xml_rows(*pp_tail)) else: pp_tail = [] if legacy: pp_tail.append((u'переименовать_лист', j_rename)) pp_tail.extend([(u'тонкая_сетка', j_grid), (u'авто_ширина', j_aw)]) extra_pp = v.get(u'extra_pp') or () i = 0 while i < len(extra_pp): fn = extra_pp[i] if fn == u'заголовок_плюс_высота': pp_tail.append((fn, j_header)) elif fn == u'зебра_диапазон': pp_tail.append((fn, j_zebra)) else: pp_tail.append((fn, _default_wizard_json({u'v': 1, u'fn': fn}))) i = i + 1 if legacy: body.extend(_wp_pp_range_rows(*pp_tail[:1])) body.append(_wp_row(u'ВПР', u'', j_vl)) if len(pp_tail) > 1: body.extend(_wp_pp_range_rows(*pp_tail[1:])) else: body.extend(_wp_pp_range_rows(*pp_tail)) body.append(_wp_row(u'ВПР', u'', j_vl)) extra_only = [] i = 0 while i < len(extra_pp): fn = extra_pp[i] if fn == u'заголовок_плюс_высота': extra_only.append((fn, j_header)) elif fn == u'зебра_диапазон': extra_only.append((fn, j_zebra)) i = i + 1 if len(extra_only) > 0: body.extend(_wp_pp_range_rows(*extra_only)) return body def _wp_example_body_pivot(xml_mode=True, variant=None): """На один лист + сводная.""" v = variant if isinstance(variant, dict) else {} sources = v.get(u'sources') or (u'source_01.xlsx',) if v.get(u'sources_preset'): srcs = tuple((_wp_example_src_preset(s) for s in sources)) else: srcs = tuple((_wp_example_src(s) for s in sources)) sheets = v.get(u'sheets') or u'Отчет*' sheet_vals = sheets if isinstance(sheets, (list, tuple)) else (sheets,) merge_mode = v.get(u'merge_mode') or u'На один лист' pivot_kw = {} if u'pivot_header_row' in v: pivot_kw[u'header_row'] = v[u'pivot_header_row'] if u'pivot_data_start' in v: pivot_kw[u'data_start'] = v[u'pivot_data_start'] if u'pivot_data_end' in v: pivot_kw[u'data_end'] = v[u'pivot_data_end'] j_pivot = _wp_example_pivot_json(as_values=bool(xml_mode), source_sheet=v.get(u'pivot_source_sheet') or u'Merge', sheet_name=v.get(u'pivot_sheet') or u'Сводная', row_fields=v.get(u'row_fields'), data_fields=v.get(u'data_fields'), **pivot_kw) j_grid = _default_wizard_json({u'v': 1, u'fn': u'тонкая_сетка'}) j_aw = _default_wizard_json({u'v': 1, u'fn': u'авто_ширина'}) pp_tail = v.get(u'pp_tail') pp_tail_disabled = v.get(u'pp_tail_disabled') transfer = u'xml_excel_ods' if xml_mode else u'По_API' body = [_wp_row(u'Отображение_прогресса', u'Диалог'), _wp_row(u'Файлы-Источники', *srcs), _wp_row(u'MERGE_DEBUG', u'Да'), _wp_row(u'Листы', *sheet_vals)] body.extend(_wp_block_range_rows()) body.extend([_wp_row(u'Режим', merge_mode, transfer), _wp_row(u'Источник_в_первой_колонке', u'Коротко'), _wp_row(u'Дата_источника_во_второй_колонке', u'Не_выводить')]) if not v.get(u'skip_ctc'): ctc_spec = v.get(u'ctc') or u'A:1 -> Филиал' body.extend(_wp_ctc_rows(ctc_spec)) if v.get(u'var_map'): body.extend(_wp_var_map_rows(*v.get(u'var_map'))) if xml_mode: body.append(_wp_row(u'MERGE_XML_CONVERT_TO_NUMBERS', u'Да')) body.extend(_wp_pp_xml_rows((u'сводная_таблица', j_pivot))) if pp_tail: body.extend(_wp_pp_xml_rows(*pp_tail)) elif pp_tail_disabled: body.extend(_wp_pp_xml_rows_disabled(*pp_tail_disabled)) else: body.extend(_wp_pp_xml_rows((u'тонкая_сетка', j_grid), (u'авто_ширина', j_aw))) else: body.extend(_wp_pp_range_rows((u'сводная_таблица', j_pivot))) if pp_tail: body.extend(_wp_pp_range_rows(*pp_tail)) elif pp_tail_disabled: body.extend(_wp_pp_range_rows_disabled(*pp_tail_disabled)) else: body.extend(_wp_pp_range_rows((u'тонкая_сетка', j_grid), (u'авто_ширина', j_aw))) return body def _wp_example_body_copy(xml_mode=True, variant=None): """Копирование листов целиком.""" v = variant if isinstance(variant, dict) else {} sources = v.get(u'sources') or (u'source_01.xlsx', u'source_02.xlsx') srcs = tuple((_wp_example_src(s) for s in sources)) sheets = v.get(u'sheets') or u'Все' sheet_vals = sheets if isinstance(sheets, (list, tuple)) else (sheets,) pp_fns = v.get(u'pp') or (u'тонкая_сетка', u'авто_ширина', u'автофильтр') transfer = u'xml_excel_ods' if xml_mode else u'По_API' body = [_wp_row(u'Отображение_прогресса', u'Диалог'), _wp_row(u'Файлы-Источники', *srcs), _wp_row(u'MERGE_DEBUG', u'Да'), _wp_row(u'Листы', *sheet_vals)] body.extend(_wp_block_range_rows()) body.append(_wp_row(u'Режим', u'Копирование листов', transfer)) pp_specs = [] i = 0 while i < len(pp_fns): fn = pp_fns[i] j = _default_wizard_json({u'v': 1, u'fn': fn}) pp_specs.append((fn, j)) i = i + 1 if xml_mode: body.append(_wp_row(u'MERGE_XML_CONVERT_TO_NUMBERS', u'Да')) body.extend(_wp_pp_xml_rows(*pp_specs)) else: body.extend(_wp_pp_range_rows(*pp_specs)) return body def _wp_default_example_presets(): """ Примеры при пустом файле пресетов: 9 сценариев × xml_excel_ods (Я01…Я09) + UNO-клоны (Я10…Я18). """ out = [] scenarios = _build_wp_scenarios() i = 0 while i < len(scenarios): title, comment, builder = scenarios[i] out.append(_wp_scenario_preset(_wp_example_preset_name(i + 1, title, u'xml_excel_ods'), comment, builder(True))) i = i + 1 i = 0 while i < len(scenarios): title, comment, builder = scenarios[i] uno_comment = comment + u' UNO: По_API и Постобработка_Диапазон.' if title == u'Сводная': uno_comment = comment + u' UNO: as_values=false.' out.append(_wp_scenario_preset(_wp_example_preset_name(i + 1 + _pw_cfg._WP_SCENARIO_COUNT, title, u'UNO'), uno_comment, builder(False))) i = i + 1 return out def _wp_presets_file_missing_or_empty(): path = _wp_presets_path() if not os.path.isfile(path): legacy = _wp_legacy_presets_path() if not os.path.isfile(legacy): return True path = legacy try: items = _wp_load_presets_list() return len(items) == 0 except Exception: return True def _wp_create_default_presets_file(): default_list = _wp_default_example_presets() err = _wp_save_presets_list(default_list) if err: return (None, err) return (default_list, None) def _wp_ensure_presets_file(presets): """ Если JSON нет или пуст — создать примеры Я01…Я18 (9 сценариев × xml + UNO). Возвращает True, если файл был создан заново. """ if not _wp_presets_file_missing_or_empty(): presets[:] = _wp_load_presets_list() return False new_list, err = _wp_create_default_presets_file() if err: print(u'param_wizard: не удалось создать пресеты: %s' % err) return False presets[:] = new_list return True def _wp_delete_presets_file(): err = None for path in (_wp_presets_path(), _wp_legacy_presets_path()): if os.path.isfile(path): try: os.remove(path) except OSError as exc: err = unicode(exc) return err def _wp_confirm_clear_all_presets(doc=None, dialog=None): text = u'Удалить файл со всеми пресетами?\n\nПри следующем открытии вкладки «Пресеты» будут созданы примеры Я01…Я18 (9 сценариев × xml_excel_ods + UNO).' title = u'Очистить все пресеты' answer = _wp_message_box_execute(doc, dialog, QUERYBOX, MSG_BUTTONS.BUTTONS_YES_NO, title, text) if answer is not None: return answer == YES try: cw = _try_import_collect() if cw is not None: return cw.ask_yes_no(text, doc) except Exception: pass return False def _show_choose_preset_on_create_dialog(doc, presets): """ Диалог выбора пресета при первом создании листа параметров. В списке всегда первый пункт — встроенный шаблон; далее сохранённые пресеты. Возвращает: ("default", None) — шаблон по умолчанию; ("preset", name) — загрузить сохранённый пресет; None — отмена (тоже шаблон по умолчанию). """ saved_names = _wp_preset_names(presets or []) list_names = [_pw_cfg._WP_CREATE_DEFAULT_PRESET_NAME] + list(saved_names) try: ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) dm = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialogModel', ctx) except Exception as err: _show_message(u'Не удалось открыть выбор пресета: %s\nБудет шаблон по умолчанию.' % err, doc=doc) return (u'default', None) dw, dh, m = (420, 300, 10) dm.PositionX = 100 dm.PositionY = 80 dm.Width = dw dm.Height = dh dm.Title = u'Выбор пресета параметров' try: dm.Sizeable = True except Exception: pass y = m _wizard_dlg_add_fixed(dm, u'HintLbl', u'Выберите набор для нового листа «Параметры_Объединения».\nПервый пункт — встроенный шаблон xml+ВПР; ниже — сохранённые пресеты.', m, y, dw - m * 2, 36, multiline=True) y += 40 _wizard_dlg_add_fixed(dm, u'ListLbl', u'Пресеты:', m, y, dw - m * 2, 14) y += 16 list_h = 110 _wizard_dlg_add_listbox(dm, u'PresetLB', m, y, dw - m * 2, list_h, multiselect=False) y += list_h + 8 _wizard_dlg_add_fixed(dm, u'CommentLbl', u'Комментарий:', m, y, dw - m * 2, 14) y += 16 comment_h = 48 _wizard_dlg_add_edit(dm, u'CommentEd', m, y, dw - m * 2, comment_h, multiline=True, readonly=True) y += comment_h + 10 btn_w, btn_h = (120, 22) load_btn = dm.createInstance('com.sun.star.awt.UnoControlButtonModel') load_btn.Name = u'LoadBtn' load_btn.Label = u'Загрузить' try: load_btn.PushButtonType = 1 except Exception: pass load_btn.PositionX = m load_btn.PositionY = y load_btn.Width = btn_w load_btn.Height = btn_h load_btn.DefaultButton = True dm.insertByName(u'LoadBtn', load_btn) cancel_btn = dm.createInstance('com.sun.star.awt.UnoControlButtonModel') cancel_btn.Name = u'CancelBtn' cancel_btn.Label = u'Отмена' try: cancel_btn.PushButtonType = 2 except Exception: pass cancel_btn.PositionX = dw - m - 80 cancel_btn.PositionY = y cancel_btn.Width = 80 cancel_btn.Height = btn_h dm.insertByName(u'CancelBtn', cancel_btn) dlg = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialog', ctx) dlg.setModel(dm) if not _nested_subdialog_create_peer(dlg, toolkit, doc, None): return (u'default', None) lb = dlg.getControl(u'PresetLB') comment_ed = dlg.getControl(u'CommentEd') try: lb.Model.StringItemList = tuple(list_names) sel = 1 if len(saved_names) > 0 else 0 lb.selectItemPos(sel, True) except Exception: pass chosen = {u'kind': None, u'name': None} def _selected_list_name(): try: pos = lb.getSelectedItemPos() if pos is not None and int(pos) >= 0 and (int(pos) < len(list_names)): return list_names[int(pos)] except Exception: pass try: t = lb.getSelectedItem() if t is not None and unicode(t).strip() != u'': return unicode(t).strip() except Exception: pass return u'' def _refresh_comment(): name = _selected_list_name() if name == u'' or name == _pw_cfg._WP_CREATE_DEFAULT_PRESET_NAME: text = _pw_cfg._SCENARIO_01_COMMENT else: comment, _summary = _wp_preset_info_texts(presets, name) text = unicode(comment or u'') try: comment_ed.setText(text) except Exception: pass def _accept_selection(): name = _selected_list_name() if name == u'' or name == _pw_cfg._WP_CREATE_DEFAULT_PRESET_NAME: chosen[u'kind'] = u'default' chosen[u'name'] = None else: chosen[u'kind'] = u'preset' chosen[u'name'] = name try: dlg.endExecute() except Exception: pass class _ChooseHandler(unohelper.Base, XActionListener, XItemListener): def disposing(self, ev): pass def itemStateChanged(self, ev): _refresh_comment() def actionPerformed(self, ev): cmd = u'' try: cmd = unicode(ev.ActionCommand) except Exception: pass if cmd == u'LoadBtn': _accept_selection() h = _ChooseHandler() try: lb.addItemListener(h) except Exception: pass try: ctl = dlg.getControl(u'LoadBtn') ctl.addActionListener(h) ctl.setActionCommand(u'LoadBtn') except Exception: pass _refresh_comment() rc = dlg.execute() if chosen[u'kind'] is None and rc == 1: name = _selected_list_name() if name == u'' or name == _pw_cfg._WP_CREATE_DEFAULT_PRESET_NAME: chosen[u'kind'] = u'default' chosen[u'name'] = None else: chosen[u'kind'] = u'preset' chosen[u'name'] = name try: dlg.dispose() except Exception: pass if chosen[u'kind'] == u'preset' and chosen[u'name']: return (u'preset', chosen[u'name']) if chosen[u'kind'] == u'default': return (u'default', None) return None def _fill_new_param_sheet(doc, sheet, catalog): """ Заполнить только что созданный лист параметров: — есть сохранённые пресеты → диалог (шаблон + пресеты) и загрузка; — нет пресетов / отмена / шаблон → встроенный набор xml+ВПР. """ if sheet is None: return macro_version = u'3.9.2' cw = _try_import_collect() if cw is not None: try: macro_version = unicode(cw.MACRO_VERSION) except Exception: pass presets = _wp_load_presets_list() if len(presets) > 0: choice = _show_choose_preset_on_create_dialog(doc, presets) if choice is not None: kind, name = choice if kind == u'preset' and name: preset = _wp_find_preset_by_name(presets, name) if preset is not None: rows = preset.get('rows') or [] if len(rows) == 0: _show_message(u'Пресет «%s» пуст.\nБудет шаблон по умолчанию.' % name, doc=doc) else: err = _wp_apply_preset_rows(doc, sheet, rows, catalog) if err: _show_message(u'Не удалось загрузить пресет «%s»: %s\nБудет шаблон по умолчанию.' % (name, err), doc=doc) else: _freeze_param_sheet_header(doc, sheet) return else: _show_message(u'Пресет «%s» не найден.\nБудет шаблон по умолчанию.' % name, doc=doc) build_param_sheet_from_scratch(doc, sheet, macro_version, catalog) _freeze_param_sheet_header(doc, sheet) def _param_b_is_see_column_c(text): s = unicode(text or u'').strip().casefold() return s in (u'смотри в с', u'смотри в c', u'см.c', u'см. c') def _spec_has_col_c_subdialog(spec): return bool(spec and spec.get('col_c_subdialog')) def _spec_col_c_subdialog_kind(spec): return unicode(spec.get('col_c_subdialog_kind') or u'').strip() def _spec_col_c_keeps_b(spec): """True: B — обычное значение (шаблоны), C — JSON через подвизард (Листы_не_удалять).""" return bool(spec and spec.get('col_c_keep_b')) def _spec_col_c_see_only(spec): """True: B фиксируется как «смотри в С», всё в JSON колонки C.""" if not _spec_has_col_c_subdialog(spec): return False if _spec_col_c_keeps_b(spec): return False if spec.get('mode') == 'choice': return False return True def _spec_col_c_fn_key(spec): return unicode(spec.get('col_c_fn_key') or u'').strip() def _param_ui_yield(): try: ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) if toolkit is not None and hasattr(toolkit, 'processEventsToIdle'): toolkit.processEventsToIdle(200) except Exception: pass def _param_dispatch_optimal_row_height(doc, sheet, start_row, end_row, end_col): controller = None saved_sheet = None saved_sel = None try: controller = doc.getCurrentController() if controller is None: return False try: saved_sheet = controller.getActiveSheet() except Exception: pass try: saved_sel = controller.getSelection() except Exception: pass controller.setActiveSheet(sheet) rng = sheet.getCellRangeByPosition(0, int(start_row), int(end_col), int(end_row)) controller.select(rng) ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() helper = sm.createInstanceWithContext('com.sun.star.frame.DispatchHelper', ctx) frame = controller.getFrame() if helper is None or frame is None: return False helper.executeDispatch(frame, '.uno:OptimalRowHeight', '_self', 0, ()) _param_ui_yield() return True except Exception: return False finally: if controller is not None: try: if saved_sheet is not None: controller.setActiveSheet(saved_sheet) except Exception: pass try: if saved_sel is not None: controller.select(saved_sel) except Exception: pass def _param_reset_row_height_before_autofit(sheet, row): """ Сбросить зафиксированную высоту строки перед OptimalRowHeight. После insert/remove высоты «прилипают» к индексам, а не к содержимому: без сброса OptimalRowHeight часто оставляет старую высоту. """ if sheet is None or row is None: return try: row_obj = sheet.getRows().getByIndex(int(row)) try: row_obj.OptimalHeight = False except Exception: pass try: row_obj.Height = 100 except Exception: pass try: row_obj.OptimalHeight = True except Exception: pass except Exception: pass def _param_apply_rows_optimal_height(doc, sheet, start_row, end_row, end_col): if sheet is None or end_row < start_row: return rows = sheet.getRows() r = int(start_row) end_r = int(end_row) while r <= end_r: _param_reset_row_height_before_autofit(sheet, r) if doc is not None: _param_dispatch_optimal_row_height(doc, sheet, r, r, end_col) else: try: row_obj = rows.getByIndex(r) row_obj.OptimalHeight = True except Exception: pass r = r + 1 _param_ui_yield() def _param_autofit_row_heights(doc, sheet, start_row, end_row, end_col): """Автоподбор высоты строк таблицы параметров.""" _param_apply_rows_optimal_height(doc, sheet, start_row, end_row, end_col) def _param_scale_row_heights(sheet, start_row, end_row, factor=1.1): """Умножить высоту строк [start_row..end_row] на factor (после автофита).""" if sheet is None or start_row is None or end_row is None: return try: sr = int(start_row) er = int(end_row) except Exception: return if er < sr: sr, er = (er, sr) try: f = float(factor) except Exception: f = 1.1 if f <= 0: return rows = sheet.getRows() r = sr while r <= er: try: row_obj = rows.getByIndex(r) h = int(row_obj.Height) if h > 0: new_h = int(round(h * f)) if new_h < h + 1 and f > 1.0: new_h = h + 1 row_obj.Height = new_h try: row_obj.OptimalHeight = False except Exception: pass except Exception: pass r = r + 1 def _param_autofit_rows_plus_pct(doc, sheet, start_row, end_row, end_col=None, pct=10): """ Автофит высоты строк, затем увеличить на pct процентов. Используется после перемещения параметров вверх/вниз. """ if sheet is None or start_row is None or end_row is None: return try: sr = int(start_row) er = int(end_row) except Exception: return if er < sr: sr, er = (er, sr) if end_col is None: end_col = _sheet_last_used_col(sheet, 6) if end_col < 5: end_col = 5 _param_autofit_row_heights(doc, sheet, sr, er, int(end_col)) try: factor = 1.0 + float(pct) / 100.0 except Exception: factor = 1.1 _param_scale_row_heights(sheet, sr, er, factor=factor) def _param_border_line(color=None, width=10): if color is None: color = _pw_cfg._PARAM_BORDER_COLOR bl = uno.createUnoStruct('com.sun.star.table.BorderLine2') bl.Color = int(color) bl.LineStyle = 0 w = int(width) bl.LineWidth = w bl.InnerLineWidth = w bl.OuterLineWidth = w return bl def _param_set_cell_borders(cell, color=None): if cell is None: return line = _param_border_line(color) cell.TopBorder = line line = _param_border_line(color) cell.BottomBorder = line line = _param_border_line(color) cell.LeftBorder = line line = _param_border_line(color) cell.RightBorder = line def _param_apply_range_borders(sheet, col_start, row_start, col_end, row_end): try: rng = sheet.getCellRangeByPosition(col_start, row_start, col_end, row_end) line = _param_border_line() tb = uno.createUnoStruct('com.sun.star.table.TableBorder') tb.TopLine = line tb.BottomLine = _param_border_line() tb.LeftLine = _param_border_line() tb.RightLine = _param_border_line() tb.IsHorizontalLineValid = True tb.IsVerticalLineValid = True inner = _param_border_line() tb.HorizontalLine = inner tb.VerticalLine = inner rng.TableBorder = tb return except Exception: pass row = row_start while row <= row_end: col = col_start while col <= col_end: _param_set_cell_borders(sheet.getCellByPosition(col, row)) col = col + 1 row = row + 1 def _param_style_header_cell(cell): if cell is None: return cell.CharFontName = _pw_cfg._PARAM_FONT_NAME cell.CharHeight = _pw_cfg._PARAM_FONT_SIZE try: from com.sun.star.awt.FontWeight import BOLD cell.CharWeight = BOLD except Exception: pass cell.CellBackColor = _pw_cfg._PARAM_FILL_HEADER cell.IsCellBackgroundTransparent = False cell.IsTextWrapped = True try: from com.sun.star.table.CellHoriJustify import CENTER from com.sun.star.table.CellVertJustify2 import CENTER as VCENTER cell.HoriJustify = CENTER cell.VertJustify = VCENTER except Exception: pass def _param_style_name_cell(cell): if cell is None: return cell.CharFontName = _pw_cfg._PARAM_FONT_NAME cell.CharHeight = _pw_cfg._PARAM_FONT_SIZE try: from com.sun.star.awt.FontWeight import BOLD cell.CharWeight = BOLD except Exception: pass cell.CellBackColor = _pw_cfg._PARAM_FILL_NAME cell.IsCellBackgroundTransparent = False cell.IsTextWrapped = True try: from com.sun.star.table.CellHoriJustify import CENTER from com.sun.star.table.CellVertJustify2 import CENTER as VCENTER cell.HoriJustify = CENTER cell.VertJustify = VCENTER except Exception: pass def _param_style_value_cell(cell, wrap_top=False): if cell is None: return cell.CharFontName = _pw_cfg._PARAM_FONT_NAME cell.CharHeight = _pw_cfg._PARAM_FONT_SIZE try: from com.sun.star.awt.FontWeight import NORMAL cell.CharWeight = NORMAL except Exception: pass cell.IsTextWrapped = True try: from com.sun.star.table.CellHoriJustify import CENTER from com.sun.star.table.CellVertJustify2 import TOP, CENTER as VCENTER cell.HoriJustify = CENTER cell.VertJustify = TOP if wrap_top else VCENTER except Exception: pass def _param_set_column_widths(sheet, last_col): if sheet is None: return try: cols = sheet.getColumns() col = 0 while col <= last_col: w = _pw_cfg._PARAM_COL_WIDTHS[col] if col < len(_pw_cfg._PARAM_COL_WIDTHS) else 9500 cols.getByIndex(col).Width = int(w) col = col + 1 except Exception: pass def _param_sheet_hint(name): if name is None or unicode(name).strip() == u'': return u'' key = unicode(name).strip() if key in _pw_cfg.WIZARD_HINTS: return _pw_cfg.WIZARD_HINTS[key] return u'' def _param_write_hint_annotation(cell, text): if cell is None or text is None or unicode(text).strip() == u'': return try: ann = cell.Annotation if ann is None: return ann.String = unicode(text) except Exception: pass def build_param_sheet_from_scratch(doc, sheet, macro_version, catalog=None): """ Создать таблицу параметров с первой строки листа (индекс 0): шаблон xml+ВПР (vlookup_data_low_old), оформление и выпадающие списки. """ if sheet is None: return rows = _default_wizard_param_rows(macro_version) max_col = _pw_cfg._PARAM_VALUE_COLUMNS last_row = len(rows) - 1 if last_row < 0: return try: rng = sheet.getCellRangeByPosition(0, 0, max_col, _pw_cfg.MERGE_PARAM_SHEET_MAX_ROWS - 1) rng.clearContents(7) except Exception: pass row_idx = 0 comment_row = -1 while row_idx < len(rows): name, vals = (rows[row_idx][0], rows[row_idx][1]) if _param_match(name, _pw_cfg.P_MERGE_COMMENT): comment_row = row_idx sheet.getCellByPosition(0, row_idx).String = unicode(name) col = 1 vi = 0 while vi < max_col: cell = sheet.getCellByPosition(col, row_idx) if vi < len(vals): cell.String = unicode(vals[vi]) else: cell.String = u'' col = col + 1 vi = vi + 1 row_idx = row_idx + 1 row_idx = 0 while row_idx <= last_row: name = _cell_string(sheet.getCellByPosition(0, row_idx)) col = 0 while col <= max_col: cell = sheet.getCellByPosition(col, row_idx) if row_idx == 0 and _param_match(name, u'Параметр'): _param_style_header_cell(cell) elif col == 0 and name != u'': _param_style_name_cell(cell) _param_write_hint_annotation(cell, _param_sheet_hint(name)) elif comment_row >= 0 and row_idx == comment_row and (col >= 1): _param_style_value_cell(cell, wrap_top=True) else: _param_style_value_cell(cell, wrap_top=False) col = col + 1 row_idx = row_idx + 1 if comment_row >= 0: try: sheet.getCellRangeByPosition(1, comment_row, max_col, comment_row).merge(True) except Exception: pass _param_apply_range_borders(sheet, 0, 0, max_col, last_row) _param_set_column_widths(sheet, max_col) if catalog is not None: _apply_param_sheet_validations(sheet, catalog, last_row, extra_empty_rows=3, doc=doc) _param_apply_disabled_rows_styles(sheet, catalog) _param_autofit_row_heights(doc, sheet, 0, last_row, max_col) def reformat_param_sheet_table(doc, sheet, catalog=None): """Переоформить таблицу параметров без очистки данных (после пресета и т.п.).""" if sheet is None: return _restore_param_sheet_header_row(sheet) max_col = _param_sheet_max_value_col(sheet) last_row = _sheet_last_used_row(sheet, 30) if last_row < 0: return comment_row = -1 row_idx = 0 while row_idx <= last_row: name = _cell_string(sheet.getCellByPosition(0, row_idx)) if _param_match(name, _pw_cfg.P_MERGE_COMMENT): comment_row = row_idx row_idx = row_idx + 1 row_idx = 0 while row_idx <= last_row: name = _cell_string(sheet.getCellByPosition(0, row_idx)) col = 0 while col <= max_col: cell = sheet.getCellByPosition(col, row_idx) if row_idx == 0 and _param_match(name, u'Параметр'): _param_style_header_cell(cell) elif col == 0 and name != u'': _param_style_name_cell(cell) _param_write_hint_annotation(cell, _param_sheet_hint(name)) elif comment_row >= 0 and row_idx == comment_row and (col >= 1): _param_style_value_cell(cell, wrap_top=True) else: _param_style_value_cell(cell, wrap_top=False) col = col + 1 row_idx = row_idx + 1 if comment_row >= 0: try: sheet.getCellRangeByPosition(1, comment_row, max_col, comment_row).merge(True) except Exception: pass _param_apply_range_borders(sheet, 0, 0, max_col, last_row) _param_set_column_widths(sheet, max_col) if catalog is not None: _apply_param_sheet_validations(sheet, catalog, last_row, extra_empty_rows=3, doc=doc) _param_apply_disabled_rows_styles(sheet, catalog) _param_autofit_row_heights(doc, sheet, 0, last_row, max_col) def _freeze_param_sheet_header(doc, sheet): if doc is None or sheet is None: return try: ctrl = doc.getCurrentController() if ctrl is not None: if ctrl.getActiveSheet() is None or ctrl.getActiveSheet().Name != sheet.Name: ctrl.setActiveSheet(sheet) ctrl.freezeAtPosition(0, 1) except Exception: pass def _sheet_last_used_row(sheet, default_row=30): if sheet is None: return default_row try: cursor = sheet.createCursor() cursor.gotoEndOfUsedArea(False) return int(cursor.RangeAddress.EndRow) except Exception: return default_row def _param_sheet_has_content(sheet): """Есть ли на листе осмысленные параметры (не пустой шаблон).""" if sheet is None: return False markers = (_pw_cfg.P_MERGE_VERSION, u'Файлы-Источники', u'Режим', u'Листы') row = 0 while row < 40: name = _cell_string(sheet.getCellByPosition(0, row)) if name == u'': row = row + 1 continue j = 0 while j < len(markers): if _param_match(name, markers[j]): return True j = j + 1 row = row + 1 return False def _get_active_sheet(doc): if doc is None: return None try: ctrl = doc.getCurrentController() if ctrl is not None: return ctrl.getActiveSheet() except Exception: pass return None def _sheet_is_absolutely_empty(sheet): """Быстрое определение «абсолютно пустого» листа (без занятых областей/значений). Цель: не переписывать лист при наличии хотя бы небольшого used-area. """ if sheet is None: return False try: cursor = sheet.createCursor() cursor.gotoEndOfUsedArea(False) ea = cursor.RangeAddress end_row = int(ea.EndRow) end_col = int(ea.EndColumn) except Exception: return False # На реальном «новом пустом листе» used-area обычно в пределах top-left угла. if end_row > 2 or end_col > 2: return False try: r = 0 while r <= end_row: c = 0 while c <= end_col: if unicode(_cell_string(sheet.getCellByPosition(c, r)) or u'').strip() != u'': return False c = c + 1 r = r + 1 except Exception: # Если не смогли прочитать — лучше не считать лист пустым. return False # Проверка нескольких стандартных ячеек на случай «used-area есть, но значений нет». for c, r in ((0, 0), (1, 0), (0, 1), (1, 1), (2, 2)): try: if unicode(_cell_string(sheet.getCellByPosition(c, r)) or u'').strip() != u'': return False except Exception: pass return True def _next_param_sheet_name(doc, param_prefix): """Имя следующего листа параметров: `[prefix]_n`.""" if doc is None: return None prefix = unicode(param_prefix or u'').strip() if prefix == u'': return None try: sheets = doc.Sheets except Exception: return None n = 1 while n < 10000: suffix = u'_%d' % n candidate = prefix + suffix try: if not sheets.hasByName(candidate): return candidate except Exception: return candidate n = n + 1 return None def _wp_confirm_load_on_empty_sheet(doc, sheet_name): """Запрос при «пустом листе» перед заменой сценария на inplace.""" title = u'Параметры' text = u'Лист «%s» пустой. Загрузить параметры на него сейчас?%s' % (sheet_name, u'\n(пресеты если есть, иначе шаблон по умолчанию)') answer = _wp_message_box_execute(doc, None, QUERYBOX, MSG_BUTTONS.BUTTONS_YES_NO, title, text) if answer is None: return False return answer == YES def _insert_param_sheet(doc, sheet_name=None): """Создать лист параметров (надёжный insertNewByName + getByName).""" if doc is None: return (None, u'Документ недоступен') if sheet_name is None: sheet_name = _pw_cfg.MERGE_PARAM_SHEET_NAME name = unicode(sheet_name).strip() if name == u'': return (None, u'Пустое имя листа') try: sheets = doc.Sheets if sheets.hasByName(name): return (sheets.getByName(name), None) try: index = int(sheets.getCount()) except Exception: index = 0 sheets.insertNewByName(name, index) if sheets.hasByName(name): return (sheets.getByName(name), None) return (None, u'Лист не появился после insertNewByName') except Exception as err: return (None, u'Ошибка создания листа «%s»: %s' % (name, err)) def _activate_sheet(doc, sheet): if doc is None or sheet is None: return try: ctrl = doc.getCurrentController() if ctrl is not None: ctrl.setActiveSheet(sheet) except Exception: pass def _ensure_param_sheet(doc): """ Создать лист «Параметры_Объединения» с шаблоном xml+ВПР, если его нет или он пустой. Возвращает (sheet, error_text, created). """ if doc is None: return (None, u'Не открыта книга Calc.', False) active_sheet = _get_active_sheet(doc) sheet, sheet_err = _resolve_param_sheet_for_ui(doc, allow_create=True) if sheet is None and sheet_err: return (None, sheet_err, False) created = False repurposed = False # Новый сценарий: визард вызвали на «абсолютно пустом» листе и # в книге ещё нет листов параметров — загрузим inplace на него. if sheet is None and active_sheet is not None: try: active_name = unicode(active_sheet.Name or u'').strip() except Exception: active_name = u'' if active_name != u'' and (not active_name.startswith(_pw_cfg.MERGE_PARAM_SHEET_NAME)): if _sheet_is_absolutely_empty(active_sheet) and _wp_confirm_load_on_empty_sheet(doc, active_name): target_name = _next_param_sheet_name(doc, _pw_cfg.MERGE_PARAM_SHEET_NAME) if target_name: try: active_sheet.Name = target_name sheet = active_sheet created = True repurposed = True except Exception: # Если переименование не удалось — продолжаем по старой логике. sheet = None created = False repurposed = False # Если лист параметров (выбранный для визарда) уже существует и «абсолютно пустой» — # не пересоздаём его, просто заполним. if sheet is not None and (not repurposed): try: if active_sheet is sheet and _sheet_is_absolutely_empty(sheet) and (not _param_sheet_has_content(sheet)): repurposed = True created = True except Exception: pass needs_fill = sheet is None or not _param_sheet_has_content(sheet) if needs_fill: if repurposed and sheet is not None: cw = _try_import_collect() if cw is not None: try: cw.set_run_param_sheet(sheet.Name) cw.refresh_document(doc) except Exception: pass _activate_sheet(doc, sheet) return (sheet, None, True) if sheet is not None: try: doc.Sheets.removeByName(sheet.Name) except Exception: pass sheet = None sheet, err = _insert_param_sheet(doc) if sheet is None: return (None, err, False) cw = _try_import_collect() if cw is not None: try: cw.set_run_param_sheet(sheet.Name) cw.refresh_document(doc) except Exception: pass _activate_sheet(doc, sheet) created = True return (sheet, None, created) def _get_selection_cell(doc): """Текущая ячейка (col, row) 0-based или (None, None).""" if doc is None: return (None, None) try: ctrl = doc.getCurrentController() if ctrl is None: return (None, None) sel = ctrl.getSelection() if sel is None: return (None, None) addr = sel.getRangeAddress() return (int(addr.StartColumn), int(addr.StartRow)) except Exception: return (None, None) def _get_selection_row_range(doc): """ Диапазон выделенных строк (start_row, end_row) 0-based или (None, None). Поддерживает одну ячейку, прямоугольный диапазон и множественные диапазоны. """ if doc is None: return (None, None) try: ctrl = doc.getCurrentController() if ctrl is None: return (None, None) sel = ctrl.getSelection() if sel is None: return (None, None) try: addrs = sel.getRangeAddresses() if addrs is not None and len(addrs) > 0: start_row = int(addrs[0].StartRow) end_row = int(addrs[0].EndRow) i = 1 while i < len(addrs): a = addrs[i] sr = int(a.StartRow) er = int(a.EndRow) if sr < start_row: start_row = sr if er > end_row: end_row = er i = i + 1 if start_row > end_row: start_row, end_row = (end_row, start_row) return (start_row, end_row) except Exception: pass addr = sel.getRangeAddress() start_row = int(addr.StartRow) end_row = int(addr.EndRow) if start_row > end_row: start_row, end_row = (end_row, start_row) return (start_row, end_row) except Exception: return (None, None) def _sheet_last_used_col(sheet, default_col=6): if sheet is None: return default_col try: cursor = sheet.createCursor() cursor.gotoEndOfUsedArea(False) return int(cursor.RangeAddress.EndColumn) except Exception: return default_col def _copy_sheet_row(sheet, src_row, dest_row, end_col): """Скопировать одну строку (значения + форматы) через copyRange.""" src = sheet.getCellRangeByPosition(0, int(src_row), int(end_col), int(src_row)) dest = sheet.getCellByPosition(0, int(dest_row)) sheet.copyRange(dest.CellAddress, src.RangeAddress) def _merge_param_move_rows(doc, sheet, start_row, end_row, direction, min_row=1): """ Сдвиг блока строк на 1 вверх/вниз без потерь (перестановка с соседней строкой). direction: -1 (вверх) или +1 (вниз) min_row: минимальная разрешённая строка (0-based), по умолчанию 1 (не трогаем заголовок) Реализация: insertByIndex + copyRange + removeByIndex (без временных колонок за пределами листа Calc). Возвращает: (ok, err, new_start, new_end, touch_start, touch_end) где [new_start..new_end] — позиция перемещённого блока, [touch_start..touch_end] — все затронутые строки (блок + соседняя). """ if doc is None or sheet is None or start_row is None or (end_row is None): return (False, u'Не удалось определить выделение.', None, None, None, None) try: start_row = int(start_row) end_row = int(end_row) except Exception: return (False, u'Некорректный диапазон строк.', None, None, None, None) if start_row > end_row: start_row, end_row = (end_row, start_row) if start_row < int(min_row): return (False, u'Выделите строку параметра (не заголовок таблицы).', None, None, None, None) last_row = _sheet_last_used_row(sheet, 30) if last_row < int(min_row): return (False, u'На листе нет параметров для перемещения.', None, None, None, None) end_col = _sheet_last_used_col(sheet, 6) if end_col < 5: end_col = 5 rows = sheet.getRows() try: if int(direction) < 0: if start_row <= int(min_row): return (False, u'Выше сдвигать нельзя (граница таблицы).', None, None, None, None) rows.insertByIndex(end_row + 1, 1) _copy_sheet_row(sheet, start_row - 1, end_row + 1, end_col) rows.removeByIndex(start_row - 1, 1) new_start = start_row - 1 new_end = end_row - 1 return (True, None, new_start, new_end, new_start, end_row) if end_row >= int(last_row): return (False, u'Ниже сдвигать нельзя (граница таблицы).', None, None, None, None) rows.insertByIndex(start_row, 1) _copy_sheet_row(sheet, end_row + 2, start_row, end_col) rows.removeByIndex(end_row + 2, 1) new_start = start_row + 1 new_end = end_row + 1 return (True, None, new_start, new_end, start_row, new_end) except Exception as e: return (False, u'Не удалось переместить строки: %s' % e, None, None, None, None) def _active_param_sheet_for_move(doc): """Активный лист параметров или (None, error). Не создаёт лист заново.""" if doc is None: return (None, u'Не открыта книга Calc.') try: ctrl = doc.getCurrentController() if ctrl is None: return (None, u'Не удалось определить активный лист.') active = ctrl.getActiveSheet() if active is None: return (None, u'Не удалось определить активный лист.') name = unicode(active.Name) if name.startswith(_pw_cfg.MERGE_PARAM_SHEET_NAME): return (active, None) return (None, u'Активируйте лист «%s» (или лист с таким префиксом) и выделите строки параметров.' % _pw_cfg.MERGE_PARAM_SHEET_NAME) except Exception: return (None, u'Не удалось определить активный лист.') def _param_wizard_select_rows(doc, sheet, start_row, end_row): try: ctrl = doc.getCurrentController() if ctrl is None: return ctrl.setActiveSheet(sheet) sr = int(start_row) er = int(end_row) if er < sr: sr, er = (er, sr) ctrl.select(sheet.getCellRangeByPosition(0, sr, 1, er)) except Exception: pass def _row_param_name(sheet, row): if sheet is None or row is None or int(row) < 0: return u'' name = _cell_string(sheet.getCellByPosition(0, int(row))) if int(row) == 0 and _param_match(name, u'Параметр'): return u'' return name def _find_catalog_spec(catalog, param_name): if catalog is None or param_name is None: return None canonical = _param_canonical_sheet_name(param_name) i = 0 while i < len(catalog): if _param_match(catalog[i]['name'], canonical): return catalog[i] i = i + 1 return None def _spec_has_inline_transfer(spec): """Способ переноса в колонке C строки «Режим» (визард / инлайн-атрибут).""" if spec is None: return False if spec.get('inline_transfer'): return True pt = spec.get('paired_transfer') return pt not in (None, False, u'') def _normalize_pp_fn_key(fn_key): try: from libre_macros_param_codec import normalize_fn_key return normalize_fn_key(fn_key) except Exception: return unicode(fn_key or u'').strip().casefold() def _find_sheet_pp_extra_for_fn(sheet, spec, fn_key, exclude_row=None): """ Найти JSON/текст в C другой строки того же параметра (A) с той же функцией (B). exclude_row — текущая редактируемая строка (не копировать из себя). """ if sheet is None or spec is None: return u'' if spec.get('mode') not in ('postprocess_range', 'postprocess_row'): return u'' want = _normalize_pp_fn_key(fn_key) if want == u'': return u'' param_name = spec.get('name', u'') excl = -1 if exclude_row is not None: try: excl = int(exclude_row) except Exception: excl = -1 row = 0 while row < _pw_cfg.MERGE_PARAM_SHEET_MAX_ROWS: pname = _row_param_name(sheet, row) if row == 0 and _param_match(pname, u'Параметр'): row = row + 1 continue if pname == u'': break if row == excl: row = row + 1 continue if not _sheet_name_matches_param(pname, param_name): row = row + 1 continue primary, _plugin_ref, extra = _read_pp_row_values(sheet, row) got = _normalize_pp_fn_key(primary) if got == want: val = unicode(extra or u'').strip() if val != u'': return val row = row + 1 return u'' def _read_pp_row_values(sheet, row): """B/C/D строки постобработки: (primary, plugin_ref, extra).""" b = _cell_string(sheet.getCellByPosition(1, int(row))) c = _cell_string(sheet.getCellByPosition(2, int(row))) d = _cell_string(sheet.getCellByPosition(3, int(row))) if _pp_is_plugin_primary(b): return (b, c, d) return (b, u'', c) def _read_values_at_row(sheet, spec, row, col=None): """Значения B/C из конкретной строки (с учётом активной колонки).""" if sheet is None or spec is None or row is None or (int(row) < 1): return (u'', u'', u'') row = int(row) mode = spec.get('mode', 'text') primary = u'' extra = u'' plugin_ref = u'' icol = int(col) if col is not None else -1 if mode in ('postprocess_range', 'postprocess_row'): primary, plugin_ref, extra = _read_pp_row_values(sheet, row) return (primary, extra, plugin_ref) if mode == 'vlookup': return (_vlookup_read_row_values(sheet, row), None, u'') if mode in ('merge_sheets', 'split_sheets'): return (_cell_string(sheet.getCellByPosition(2, row)), None, u'') if spec.get('col_c_subdialog'): extra = _cell_string(sheet.getCellByPosition(2, row)) if _spec_col_c_keeps_b(spec) or spec.get('mode') == 'choice': primary = _cell_string(sheet.getCellByPosition(1, row)) return (primary, extra, u'') return (_pw_cfg._PARAM_B_SEE_COLUMN_C, extra, u'') if mode == 'files': if icol >= 1: vals = _row_values_upto_col(sheet, row, icol) else: vals = _row_values(sheet, row) if len(vals) == 0: return (u'', extra, u'') return (_join_files_path_text(vals, u';'), extra, u'') if spec.get('multi_column'): if icol >= 1: primary = _cell_string(sheet.getCellByPosition(icol, row)) else: vals = _row_values(sheet, row) primary = vals[0] if len(vals) > 0 else u'' return (primary, extra, u'') if mode == 'choice' and _spec_has_inline_transfer(spec): primary = _cell_string(sheet.getCellByPosition(1, row)) extra = _cell_string(sheet.getCellByPosition(2, row)) return (primary, extra, u'') if icol >= 1: primary = _cell_string(sheet.getCellByPosition(icol, row)) else: vals = _row_values(sheet, row) primary = vals[0] if len(vals) > 0 else u'' return (primary, extra, u'') def _transfer_param_name(): cw = _try_import_collect() if cw is not None: return getattr(cw, 'P_MERGE_DATA_TRANSFER', u'Способ_переноса') return u'Способ_переноса' def _transfer_wizard_choices(catalog=None): spec = None if catalog is not None: spec = _find_catalog_spec(catalog, _transfer_param_name()) if spec is not None: choices = spec.get('choices') or [] if len(choices) > 0: return list(choices) return [u'По_API', u'Буфер_по_заголовкам', u'Буфер_по_позиции', u'xml_excel_ods'] def _read_transfer_wizard_value(sheet, mode_row=None): """Значение для combo при редактировании «Режим» — колонка C той же строки.""" row = None if mode_row is not None and int(mode_row) >= 0: row = int(mode_row) else: cw = _try_import_collect() p_mode = getattr(cw, 'P_MERGE_MODE', u'Режим') if cw is not None else u'Режим' found = _find_param_row(sheet, p_mode) if found >= 0: row = found if row is not None and row >= 0: val = _cell_string(sheet.getCellByPosition(2, row)) if val != u'': return val return _pw_cfg._TRANSFER_WIZARD_DEFAULT def _mode_value_uses_transfer(mode_primary): return True def _scan_sheet_param_entries(sheet, catalog): """ Строки параметров на листе в порядке следования (только имена из каталога). Возвращает список dict: row, name, spec, label. """ if sheet is None or catalog is None: return [] entries = [] row = 0 while row < _pw_cfg.MERGE_PARAM_SHEET_MAX_ROWS: pname = _row_param_name(sheet, row) if row == 0 and pname == u'' and _param_match(_cell_string(sheet.getCellByPosition(0, 0)), u'Параметр'): row = row + 1 continue if pname == u'': break spec = _find_catalog_spec(catalog, pname) if spec is not None: canonical = _param_canonical_sheet_name(pname) entries.append({'row': int(row), 'name': canonical, 'sheet_name': pname, 'spec': spec, 'disabled': _param_sheet_name_is_disabled(pname), 'label': pname}) row = row + 1 name_counts = {} i = 0 while i < len(entries): key = entries[i]['name'].lower() name_counts[key] = name_counts.get(key, 0) + 1 i = i + 1 i = 0 while i < len(entries): if name_counts.get(entries[i]['name'].lower(), 0) > 1: entries[i]['label'] = u'%s — стр. %d' % (entries[i]['sheet_name'], entries[i]['row'] + 1) i = i + 1 return entries def _catalog_names_for_new_param(sheet, catalog): """ Имена параметров для режима «новый параметр»: все из каталога, кроме одноэкземплярных (multi_row=False), уже присутствующих на листе. """ if catalog is None: return [] existing_singletons = set() if sheet is not None: row = 0 while row < _pw_cfg.MERGE_PARAM_SHEET_MAX_ROWS: pname = _row_param_name(sheet, row) if row == 0 and pname == u'' and _param_match(_cell_string(sheet.getCellByPosition(0, 0)), u'Параметр'): row = row + 1 continue if pname == u'': break spec = _find_catalog_spec(catalog, pname) if spec is not None and (not spec.get('multi_row')): existing_singletons.add(_param_canonical_sheet_name(spec['name']).lower()) row = row + 1 out = [] i = 0 while i < len(catalog): spec = catalog[i] nm = spec['name'] if spec.get('multi_row') or nm.lower() not in existing_singletons: out.append(nm) i = i + 1 return out def _is_pivot_table_pp_name(name): return unicode(name or u'').strip().casefold() == u'сводная_таблица' def _spec_is_postprocess_xml(spec): """True — параметр «Постобработка_xml» (сводная только as_values).""" if spec is None: return False name = unicode(spec.get('name') or u'').strip() if name == u'': return False return name.casefold() == u'Постобработка_xml'.casefold() def _build_initial_from_row(sheet, catalog, row, col=-1): """Контекст предзаполнения основного окна визарда для строки параметра.""" pname = _row_param_name(sheet, row) if pname == u'': return None spec = _find_catalog_spec(catalog, pname) if spec is None: return None primary, extra, plugin_ref = _read_values_at_row(sheet, spec, row, col if col is not None and int(col) >= 0 else None) return {'spec': spec, 'row': int(row), 'col': int(col) if col is not None and int(col) >= 0 else -1, 'primary': primary, 'extra': extra, 'plugin_ref': plugin_ref} def _param_row_has_subdialog(spec, fn_key): """Есть ли у строки постобработки / ВПР отдельный субдиалог для колонки C.""" mode = spec.get('mode') if spec is not None else None if mode == 'vlookup': return True if mode in ('merge_sheets', 'split_sheets'): return True if mode not in ('postprocess_range', 'postprocess_row'): return False fn = unicode(fn_key or u'').strip() if fn == u'' or _pp_is_plugin_primary(fn): return False try: from libre_macros_param_codec import normalize_fn_key fn = normalize_fn_key(fn) or fn except Exception: pass return _inner_codec_dialog_supported(fn) or _is_pivot_table_pp_name(fn) def _try_subdialog_from_row(sheet, catalog, spec, row): """ Субдиалог для колонки C существующей строки. Возвращает dict(kind, spec, row, …) или None. """ if spec is None or row is None or int(row) < 1: return None row = int(row) mode = spec.get('mode') if mode == 'vlookup': return {'kind': 'vlookup', 'spec': spec, 'row': row, 'initial': _vlookup_read_row_values(sheet, row)} if mode == 'merge_sheets': return {'kind': 'merge_sheets', 'spec': spec, 'row': row, 'initial': _cell_string(sheet.getCellByPosition(2, row))} if mode == 'split_sheets': return {'kind': 'split_sheets', 'spec': spec, 'row': row, 'initial': _cell_string(sheet.getCellByPosition(2, row))} if mode not in ('postprocess_range', 'postprocess_row'): return None fn_b = _cell_string(sheet.getCellByPosition(1, row)) initial_text = _cell_string(sheet.getCellByPosition(2, row)) if _param_match(fn_b, u'объединить_листы_в_один'): merge_spec = _find_catalog_spec(catalog, u'объединить_листы_в_один') if merge_spec is not None: return {'kind': 'merge_sheets', 'spec': merge_spec, 'row': row, 'initial': initial_text} if _param_match(fn_b, u'разделить_листы'): split_spec = _find_catalog_spec(catalog, u'разделить_листы') if split_spec is not None: return {'kind': 'split_sheets', 'spec': split_spec, 'row': row, 'initial': initial_text} if not _param_row_has_subdialog(spec, fn_b): return None if _is_pivot_table_pp_name(fn_b): return {'kind': 'pivot', 'spec': spec, 'row': row, 'fn_key': fn_b, 'initial_text': initial_text} return {'kind': 'inner_codec', 'spec': spec, 'row': row, 'fn_key': fn_b, 'initial_text': initial_text} def _active_sheet_is_param_sheet(doc, param_sheet): try: ctrl = doc.getCurrentController() active = ctrl.getActiveSheet() if ctrl is not None else None return active is not None and param_sheet is not None and (unicode(active.Name) == unicode(param_sheet.Name)) except Exception: return False def _wizard_launch_plan(doc, param_sheet, catalog): """ План открытия визарда по позиции курсора. action: "subdialog" | "main" new_param_mode: для основного окна initial: контекст предзаполнения (main) или None subdialog: описание субдиалога (subdialog) или None """ entries = _scan_sheet_param_entries(param_sheet, catalog) on_param_sheet = _active_sheet_is_param_sheet(doc, param_sheet) col, row = _get_selection_cell(doc) if on_param_sheet else (None, None) if not on_param_sheet: if len(entries) > 0: e = entries[0] return {'action': 'main', 'new_param_mode': False, 'initial': _build_initial_from_row(param_sheet, catalog, e['row'], -1), 'subdialog': None} return {'action': 'main', 'new_param_mode': True, 'initial': None, 'subdialog': None} if row is None or int(row) < 0: row = entries[0]['row'] if len(entries) > 0 else _first_empty_row(param_sheet) row = int(row) pname = _row_param_name(param_sheet, row) if pname == u'': return {'action': 'main', 'new_param_mode': True, 'initial': None, 'subdialog': None} spec = _find_catalog_spec(catalog, pname) if spec is None: return {'action': 'main', 'new_param_mode': True, 'initial': None, 'subdialog': None} icol = int(col) if col is not None else -1 mode = spec.get('mode') if mode == 'source_extra' and icol >= 1: initial_text = _cell_string(param_sheet.getCellByPosition(icol, row)) source_path = _source_extra_path_for_col(param_sheet, icol) return { 'action': 'subdialog', 'new_param_mode': False, 'initial': None, 'subdialog': { 'kind': 'source_extra', 'spec': spec, 'row': row, 'target_col': icol, 'initial_text': initial_text, 'source_path': source_path, }, } if mode in ('merge_sheets', 'split_sheets'): sub = _try_subdialog_from_row(param_sheet, catalog, spec, row) if sub is not None: return {'action': 'subdialog', 'new_param_mode': False, 'initial': None, 'subdialog': sub} if mode in ('postprocess_range', 'postprocess_row'): fn_b = _cell_string(param_sheet.getCellByPosition(1, row)) if _param_match(fn_b, u'объединить_листы_в_один') or _param_match(fn_b, u'разделить_листы'): sub = _try_subdialog_from_row(param_sheet, catalog, spec, row) if sub is not None: return {'action': 'subdialog', 'new_param_mode': False, 'initial': None, 'subdialog': sub} if icol == 2: if _param_match(pname, u'удаление_верхних_строк'): initial_text = _cell_string(param_sheet.getCellByPosition(2, row)) return {'action': 'subdialog', 'new_param_mode': False, 'initial': None, 'subdialog': {'kind': 'delete_top_rows', 'spec': spec, 'row': row, 'initial_text': initial_text}} if _param_match(pname, u'Пропуск_строк_источника'): initial_text = _cell_string(param_sheet.getCellByPosition(2, row)) return {'action': 'subdialog', 'new_param_mode': False, 'initial': None, 'subdialog': {'kind': 'skip_source_rows', 'spec': spec, 'row': row, 'initial_text': initial_text}} if _param_match(pname, u'Ячейка_В_Столбец'): initial_text = _cell_string(param_sheet.getCellByPosition(2, row)) return {'action': 'subdialog', 'new_param_mode': False, 'initial': None, 'subdialog': {'kind': 'cell_to_column', 'spec': spec, 'row': row, 'initial_text': initial_text}} if _param_match(pname, u'Добавить_в_карту_переменных'): initial_text = _cell_string(param_sheet.getCellByPosition(2, row)) return {'action': 'subdialog', 'new_param_mode': False, 'initial': None, 'subdialog': {'kind': 'add_to_variables_map', 'spec': spec, 'row': row, 'initial_text': initial_text}} if _param_match(pname, u'Листы_не_удалять'): initial_text = _cell_string(param_sheet.getCellByPosition(2, row)) return {'action': 'subdialog', 'new_param_mode': False, 'initial': None, 'subdialog': {'kind': 'sheets_undelete_filter', 'spec': spec, 'row': row, 'initial_text': initial_text}} if _param_match(pname, u'Листы_удалить'): initial_text = _cell_string(param_sheet.getCellByPosition(2, row)) return {'action': 'subdialog', 'new_param_mode': False, 'initial': None, 'subdialog': {'kind': 'sheets_force_delete_filter', 'spec': spec, 'row': row, 'initial_text': initial_text}} if _param_match(pname, u'Листы'): initial_text = _cell_string(param_sheet.getCellByPosition(2, row)) return {'action': 'subdialog', 'new_param_mode': False, 'initial': None, 'subdialog': {'kind': 'sheets_include_filter', 'spec': spec, 'row': row, 'initial_text': initial_text}} if _param_match(pname, u'автофильтр_результата'): initial_text = _cell_string(param_sheet.getCellByPosition(2, row)) return {'action': 'subdialog', 'new_param_mode': False, 'initial': None, 'subdialog': {'kind': 'sheet_list_fn', 'spec': spec, 'row': row, 'fn_key': u'автофильтр', 'initial_text': initial_text}} if _param_match(pname, u'закрепить_заголовок'): initial_text = _cell_string(param_sheet.getCellByPosition(2, row)) return {'action': 'subdialog', 'new_param_mode': False, 'initial': None, 'subdialog': {'kind': 'sheet_list_fn', 'spec': spec, 'row': row, 'fn_key': u'закрепить_заголовок', 'initial_text': initial_text}} sub = _try_subdialog_from_row(param_sheet, catalog, spec, row) if sub is not None: return {'action': 'subdialog', 'new_param_mode': False, 'initial': None, 'subdialog': sub} return {'action': 'main', 'new_param_mode': False, 'initial': _build_initial_from_row(param_sheet, catalog, row, icol), 'subdialog': None} def _run_subdialog_and_save(doc, sheet, catalog, sub): """Открыть субдиалог и записать результат в строку параметра.""" if sub is None: return False kind = sub.get('kind') spec = sub.get('spec') row = int(sub.get('row', -1)) if spec is None or row < 1: return False if kind == 'vlookup': result = show_vlookup_param_dialog(None, sub.get('initial'), doc=doc) if result is None: return False row = apply_param_value(sheet, spec, result, None, target_row=row, target_col=2, catalog=catalog, doc=doc) elif kind == 'merge_sheets': result = show_merge_sheets_param_dialog(None, sub.get('initial'), doc=doc) if result is None: return False row = apply_param_value(sheet, spec, result, None, target_row=row, target_col=2, catalog=catalog, doc=doc) elif kind == 'split_sheets': result = show_split_sheets_param_dialog(None, sub.get('initial'), doc=doc) if result is None: return False row = apply_param_value(sheet, spec, result, None, target_row=row, target_col=2, catalog=catalog, doc=doc) elif kind == 'pivot': pivot_json = show_pivot_table_param_dialog(None, sub.get('initial_text'), doc=doc, param_sheet=sheet, param_row=row, param_primary=sub.get('fn_key'), force_as_values=_spec_is_postprocess_xml(spec)) if pivot_json is None: return False elif kind == 'inner_codec': result = _show_inner_codec_param_dialog(None, sub.get('fn_key'), sub.get('initial_text'), doc=doc) if result is None: return False primary = _cell_string(sheet.getCellByPosition(1, row)) row = apply_param_value(sheet, spec, primary, result, target_row=row, catalog=catalog, doc=doc) elif kind == 'delete_top_rows': result_text = show_delete_top_rows_param_dialog(None, sub.get('initial_text'), doc=doc) if result_text is None: return False try: sheet.getCellByPosition(1, row).String = _pw_cfg._PARAM_B_SEE_COLUMN_C except Exception: pass try: sheet.getCellByPosition(2, row).String = unicode(result_text).strip() except Exception: sheet.getCellByPosition(2, row).String = u'' elif kind == 'source_extra': target_col = int(sub.get('target_col') or 1) if target_col < 1: target_col = 1 result_text = show_source_extra_param_dialog( None, sub.get('initial_text'), doc=doc, source_path=sub.get('source_path'), target_col=target_col, ) if result_text is None: return False try: if _row_param_name(sheet, row) == u'': sheet.getCellByPosition(0, row).String = spec['name'] except Exception: pass _files_prepare_value_column(doc, sheet, target_col) try: sheet.getCellByPosition(target_col, row).String = unicode(result_text).strip() except Exception: sheet.getCellByPosition(target_col, row).String = u'' _ensure_param_row_formatting(sheet, row, catalog, spec, False, doc=doc) elif kind == 'cell_to_column': result_text = show_cell_to_column_param_dialog(None, sub.get('initial_text'), doc=doc) if result_text is None: return False try: sheet.getCellByPosition(1, row).String = _pw_cfg._PARAM_B_SEE_COLUMN_C except Exception: pass try: sheet.getCellByPosition(2, row).String = unicode(result_text).strip() except Exception: sheet.getCellByPosition(2, row).String = u'' elif kind == 'add_to_variables_map': result_text = show_add_to_variables_map_param_dialog(None, sub.get('initial_text'), doc=doc) if result_text is None: return False try: sheet.getCellByPosition(1, row).String = _pw_cfg._PARAM_B_SEE_COLUMN_C except Exception: pass try: sheet.getCellByPosition(2, row).String = unicode(result_text).strip() except Exception: sheet.getCellByPosition(2, row).String = u'' elif kind == 'sheet_list_fn': fn_key = sub.get('fn_key') result_text = _show_sheet_list_panel_dialog(None, fn_key, sub.get('initial_text'), doc=doc) if result_text is None: return False if spec.get('mode') != 'choice': try: sheet.getCellByPosition(1, row).String = _pw_cfg._PARAM_B_SEE_COLUMN_C except Exception: pass try: sheet.getCellByPosition(2, row).String = unicode(result_text).strip() except Exception: sheet.getCellByPosition(2, row).String = u'' elif kind == 'sheets_undelete_filter': result_text = show_sheets_undelete_filter_dialog(None, sub.get('initial_text'), doc=doc) if result_text is None: return False # B не трогаем — там шаблоны имён листов try: sheet.getCellByPosition(2, row).String = unicode(result_text).strip() except Exception: sheet.getCellByPosition(2, row).String = u'' elif kind == 'sheets_force_delete_filter': result_text = show_sheets_force_delete_filter_dialog(None, sub.get('initial_text'), doc=doc) if result_text is None: return False try: sheet.getCellByPosition(2, row).String = unicode(result_text).strip() except Exception: sheet.getCellByPosition(2, row).String = u'' elif kind == 'sheets_include_filter': result_text = show_sheets_include_filter_dialog(None, sub.get('initial_text'), doc=doc) if result_text is None: return False try: sheet.getCellByPosition(2, row).String = unicode(result_text).strip() except Exception: sheet.getCellByPosition(2, row).String = u'' else: return False _param_wizard_finalize(doc, sheet, catalog, spec, row, is_new=False) return True def _wizard_initial_context(doc, sheet, catalog): """ Если курсор на строке с известным параметром — данные для предзаполнения формы. Учитывается только когда активный лист совпадает с листом параметров. """ if not _active_sheet_is_param_sheet(doc, sheet): return None col, row = _get_selection_cell(doc) if row is None or int(row) < 0: return None return _build_initial_from_row(sheet, catalog, int(row), col) def _try_open_pivot_from_selection(doc, sheet, catalog): """Устар.: используйте _wizard_launch_plan. Оставлено для совместимости.""" plan = _wizard_launch_plan(doc, sheet, catalog) sub = plan.get('subdialog') if plan.get('action') != 'subdialog' or sub is None or sub.get('kind') != 'pivot': return None return (sub['spec'], sub['row'], sub.get('initial_text', u'')) def _find_param_row(sheet, param_name): row = 0 while row < _pw_cfg.MERGE_PARAM_SHEET_MAX_ROWS: name = _cell_string(sheet.getCellByPosition(0, row)) if row == 0 and _param_match(name, u'Параметр'): row = row + 1 continue if name == u'': return -1 if _sheet_name_matches_param(name, param_name): return row row = row + 1 return -1 def _first_empty_row(sheet): row = 0 last_used = 0 while row < _pw_cfg.MERGE_PARAM_SHEET_MAX_ROWS: name = _cell_string(sheet.getCellByPosition(0, row)) if row == 0 and _param_match(name, u'Параметр'): row = row + 1 continue if name != u'': last_used = row row = row + 1 continue return row return last_used + 1 def _row_values(sheet, row, max_cols=20): vals = [] col = 1 while col < max_cols: v = _cell_string(sheet.getCellByPosition(col, row)) if v == u'': break vals.append(v) col = col + 1 return vals def _row_values_upto_col(sheet, row, col, max_cols=20): """ Непустые значения в B… до активного столбца включительно. На B — непрерывная цепочка с B (_row_values). """ icol = int(col) if col is not None else -1 if icol <= 1: return _row_values(sheet, row, max_cols) vals = [] c = 1 end = min(icol + 1, max_cols) while c < end: v = _cell_string(sheet.getCellByPosition(c, row)) if v != u'': vals.append(v) c = c + 1 if len(vals) == 0: return _row_values(sheet, row, max_cols) return vals def _first_empty_value_col(sheet, row, start_col=1, max_cols=20): col = start_col while col < max_cols: if _cell_string(sheet.getCellByPosition(col, row)) == u'': return col col = col + 1 return -1 def _catalog_param_names(catalog): names = [] seen = set() for spec in catalog or []: n = unicode(spec.get('name', u'')).strip() if n == u'' or n in seen: continue names.append(n) seen.add(n) return names def _find_param_template_row(sheet): """Первая строка таблицы параметров с именем в A (образец формата).""" row = 0 while row < _pw_cfg.MERGE_PARAM_SHEET_MAX_ROWS: name = _cell_string(sheet.getCellByPosition(0, row)) if row == 0 and _param_match(name, u'Параметр'): row = row + 1 continue if name == u'': break return row return -1 def _find_param_format_source_row(sheet, before_row=None): """Последняя заполненная строка таблицы перед before_row — образец формата.""" if before_row is not None and int(before_row) > 0: row = int(before_row) - 1 while row >= 0: name = _cell_string(sheet.getCellByPosition(0, row)) if row == 0 and _param_match(name, u'Параметр'): row = row - 1 continue if name != u'': return row row = row - 1 last = -1 row = 0 while row < _pw_cfg.MERGE_PARAM_SHEET_MAX_ROWS: name = _cell_string(sheet.getCellByPosition(0, row)) if row == 0 and _param_match(name, u'Параметр'): row = row + 1 continue if name == u'': if last >= 0: break row = row + 1 continue last = row row = row + 1 if last >= 0: return last return _find_param_template_row(sheet) def _param_sheet_row_last_col(sheet, row, max_cols=20): last = 0 col = 0 while col < max_cols: if _cell_string(sheet.getCellByPosition(col, row)) != u'': last = col col = col + 1 return max(last, 2) def _param_sheet_max_value_col(sheet, min_col=None, max_scan=40): """Правый индекс столбца значений (B=1) по заголовкам «Значение N» и данным листа.""" header_last = _param_sheet_header_value_max_col(sheet, max_scan=max_scan, fallback=0) last = header_last row_end = _sheet_last_used_row(sheet, _pw_cfg.MERGE_PARAM_SHEET_MAX_ROWS - 1) if row_end < 0: row_end = 0 row = 0 while row <= row_end: lc = _param_sheet_row_last_col(sheet, row, max_cols=max_scan) if lc > last: last = lc row = row + 1 if min_col is None: min_col = header_last if header_last > 0 else _pw_cfg._PARAM_VALUE_COLUMNS return max(last, int(min_col)) def _refresh_param_sheet_value_merges(doc, sheet, catalog=None): """Пересобрать объединение строки «Комментарий» на все столбцы «Значение N».""" if sheet is None: return max_col = _param_sheet_max_value_col(sheet) comment_row = _find_param_row(sheet, _pw_cfg.P_MERGE_COMMENT) if comment_row < 0: row = 0 while row < 10: if _param_match(_row_param_name(sheet, row), _pw_cfg.P_MERGE_COMMENT): comment_row = row break row = row + 1 if comment_row >= 0 and max_col >= 1: try: sheet.getCellRangeByPosition(1, comment_row, 39, comment_row).merge(False) except Exception: pass try: sheet.getCellRangeByPosition(1, comment_row, max_col, comment_row).merge(True) except Exception: pass last_row = _sheet_last_used_row(sheet, 30) if last_row < 0: last_row = 0 _param_apply_range_borders(sheet, 0, 0, max_col, last_row) _param_set_column_widths(sheet, max_col) if doc is not None: _param_autofit_row_heights(doc, sheet, 0, last_row, max_col) def _validation_list_formula_quoted(items): """Inline-список для Calc: "A";"B";"C".""" parts = [] for it in items or []: s = unicode(it).strip() if s == u'': continue s = s.replace(u'"', u'""') parts.append(u'"%s"' % s) return u';'.join(parts) def _param_dv_ensure_sheet(doc): if doc is None: return None try: sheets = doc.getSheets() if sheets.hasByName(_pw_cfg._PARAM_DV_SHEET_NAME): sh = sheets.getByName(_pw_cfg._PARAM_DV_SHEET_NAME) else: sh = sheets.insertNewByName(_pw_cfg._PARAM_DV_SHEET_NAME, sheets.getCount()) try: sh.setPropertyValue('IsVisible', False) except Exception: try: from com.sun.star.sheet.SheetVisibility import VERY_HIDDEN sh.setPropertyValue('SheetState', VERY_HIDDEN) except Exception: pass return sh except Exception as err: print(u'param_wizard: лист списков валидации: %s' % err) return None def _param_dv_reset(doc): _pw_cfg._PARAM_DV_LIST_CACHE = {} _pw_cfg._PARAM_DV_NEXT_COL = 0 sh = _param_dv_ensure_sheet(doc) if sh is None: return try: sh.getCellRangeByPosition(0, 0, 255, 999).clearContents(7) except Exception: pass def _param_dv_list_formula(doc, items): """ Формула списка для TableValidation: короткий inline или ссылка на скрытый лист. """ clean = [] for it in items or []: s = unicode(it).strip() if s != u'': clean.append(s) if len(clean) == 0: return u'' key = tuple(clean) if key in _pw_cfg._PARAM_DV_LIST_CACHE: return _pw_cfg._PARAM_DV_LIST_CACHE[key] inline = _validation_list_formula_quoted(clean) if len(inline) <= _pw_cfg._PARAM_DV_INLINE_MAX: _pw_cfg._PARAM_DV_LIST_CACHE[key] = inline return inline sh = _param_dv_ensure_sheet(doc) if sh is None: if inline: _pw_cfg._PARAM_DV_LIST_CACHE[key] = inline return inline return u'' col = _pw_cfg._PARAM_DV_NEXT_COL _pw_cfg._PARAM_DV_NEXT_COL = _pw_cfg._PARAM_DV_NEXT_COL + 1 row_i = 0 while row_i < len(clean): sh.getCellByPosition(col, row_i).String = clean[row_i] row_i = row_i + 1 try: rng = sh.getCellRangeByPosition(col, 0, col, len(clean) - 1) formula = rng.AbsoluteName except Exception: formula = inline _pw_cfg._PARAM_DV_LIST_CACHE[key] = formula return formula def _set_cell_list_validation(cell, doc, items): if cell is None: return formula = _param_dv_list_formula(doc, items) if formula is None or unicode(formula).strip() == u'': return try: from com.sun.star.sheet.ValidationType import LIST as VALIDATION_LIST except Exception: VALIDATION_LIST = 6 try: from com.sun.star.sheet.ConditionOperator import EQUAL except Exception: EQUAL = 0 try: val = cell.Validation except Exception: val = None if val is None: try: val = uno.createUnoStruct('com.sun.star.sheet.TableValidation') except Exception: try: val = uno.createUnoStruct('com.sun.star.table.TableValidation') except Exception: return try: val.Type = VALIDATION_LIST try: val.setOperator(EQUAL) except Exception: val.Operator = EQUAL try: val.setFormula1(unicode(formula)) except Exception: val.Formula1 = unicode(formula) try: val.IgnoreBlankCells = True except Exception: val.IgnoreBlank = True try: val.ShowList = 1 except Exception: val.ShowList = True val.ShowErrorMessage = False val.ShowInputMessage = False try: cell.setPropertyValue('Validation', val) except Exception: cell.Validation = val except Exception as err: print(u'param_wizard: валидация ячейки: %s' % err) def _apply_param_row_validations(sheet, row, catalog, spec=None, doc=None): """Выпадающие списки для одной строки: A — имена параметров, B… — choices.""" if sheet is None or row is None or int(row) < 1 or (catalog is None): return row = int(row) names = _catalog_param_names(catalog) if len(names) > 0: _set_cell_list_validation(sheet.getCellByPosition(0, row), doc, names) if spec is None: name = _cell_string(sheet.getCellByPosition(0, row)) if name != u'': spec = _find_catalog_spec(catalog, name) if spec is None: return choices = spec.get('choices') or [] if len(choices) == 0: return ncol = int(spec.get('choices_columns', 1) or 1) if ncol < 1: ncol = 1 ci = 0 while ci < ncol: _set_cell_list_validation(sheet.getCellByPosition(1 + ci, row), doc, choices) ci = ci + 1 def _apply_param_sheet_validations(sheet, catalog, last_row=None, extra_empty_rows=3, doc=None): """ Выпадающие списки на листе параметров: колонка A и choice-колонки B… для всех используемых строк и нескольких пустых строк внизу. """ if sheet is None or catalog is None: return with _pw_timed(u'apply_param_sheet_validations'): if doc is not None: with _pw_timed(u'param_dv_reset'): _param_dv_reset(doc) if last_row is None: last_row = _sheet_last_used_row(sheet, 30) if last_row < 1: last_row = 1 end_row = last_row + int(extra_empty_rows or 0) max_end = _pw_cfg.MERGE_PARAM_SHEET_MAX_ROWS - 1 if end_row > max_end: end_row = max_end row_idx = 1 while row_idx <= end_row: name = _cell_string(sheet.getCellByPosition(0, row_idx)) spec = _find_catalog_spec(catalog, name) if name != u'' else None _apply_param_row_validations(sheet, row_idx, catalog, spec, doc=doc) row_idx = row_idx + 1 if _pw_cfg.PARAM_WIZARD_TIMING: _pw_time_log(u' validations rows: 1..%d' % end_row) def _copy_param_row_format(sheet, src_row, dst_row, last_col): if sheet is None or src_row < 0 or dst_row < 0 or (src_row == dst_row): return False try: src = sheet.getCellRangeByPosition(0, src_row, last_col, src_row) dst = sheet.getCellRangeByPosition(0, dst_row, last_col, dst_row) if src is None or dst is None: return False try: from com.sun.star.sheet.CellCopyFlags import STYLES, VALIDATION, NUMBER_FORMAT, BORDER, CHARACTER_ATTR flags = STYLES | VALIDATION | NUMBER_FORMAT | BORDER | CHARACTER_ATTR src.copy(dst, flags) return True except Exception: pass try: src.copy(dst, 65535) return True except Exception: pass except Exception: pass return False def _apply_param_sheet_used_area_format(sheet): """PT Sans 11pt, выравнивание по центру — на весь значащий диапазон листа (одним блоком).""" if sheet is None: return with _pw_timed(u'apply_param_sheet_used_area_format (диапазон)'): end_row = _sheet_last_used_row(sheet, _pw_cfg.MERGE_PARAM_SHEET_MAX_ROWS - 1) if end_row < 0: return end_col = _param_sheet_max_value_col(sheet) if _pw_cfg.PARAM_WIZARD_TIMING: _pw_time_log(u' used area: 0..%d x 0..%d' % (end_col, end_row)) try: rng = sheet.getCellRangeByPosition(0, 0, int(end_col), int(end_row)) rng.CharFontName = _pw_cfg._PARAM_FONT_NAME rng.CharHeight = _pw_cfg._PARAM_FONT_SIZE rng.IsTextWrapped = True try: from com.sun.star.table.CellHoriJustify import CENTER from com.sun.star.table.CellVertJustify2 import CENTER as VCENTER rng.HoriJustify = CENTER rng.VertJustify = VCENTER except Exception: pass except Exception: pass def _ensure_param_row_formatting(sheet, row, catalog, spec, needs_shell, doc=None): """Копия формата/валидации с последней строки таблицы для новой строки параметров.""" if sheet is None or row < 0: return max_col = max(_pw_cfg._PARAM_VALUE_COLUMNS, 5) if spec is not None and spec.get('mode') == 'vlookup' else _pw_cfg._PARAM_VALUE_COLUMNS if needs_shell: src = _find_param_format_source_row(sheet, before_row=row) if src >= 0 and src != row: last_col = max(_param_sheet_row_last_col(sheet, src), max_col) _copy_param_row_format(sheet, src, row, last_col) name = _cell_string(sheet.getCellByPosition(0, row)) if name != u'': _param_style_name_cell(sheet.getCellByPosition(0, row)) _param_write_hint_annotation(sheet.getCellByPosition(0, row), _param_sheet_hint(name)) col = 1 while col <= max_col: _param_style_value_cell(sheet.getCellByPosition(col, row)) col = col + 1 col = 0 while col <= max_col: _param_set_cell_borders(sheet.getCellByPosition(col, row)) col = col + 1 row_name = _cell_string(sheet.getCellByPosition(0, row)) col = 0 while col <= max_col: cell = sheet.getCellByPosition(col, row) if _param_sheet_name_is_disabled(row_name): _param_set_cell_disabled_fill(cell) elif col >= 1: _param_clear_cell_background(cell) col = col + 1 _apply_param_row_validations(sheet, row, catalog, spec, doc=doc) def _format_rest_hint(rest_hints, key): if not rest_hints or key is None or unicode(key).strip() == u'': return u'' item = _rest_hint_lookup(rest_hints, key) if item is None: return u'' if isinstance(item, (tuple, list)) and len(item) >= 1: desc = unicode(item[0]).strip() example = u'' if len(item) > 1: example = unicode(item[1]).strip() if example != u'': return u'%s\nПример C: %s' % (desc, example) return desc return unicode(item) def _format_selected_b_detail(spec, selected_b): """Описание выбранного значения колонки B.""" if spec is None or unicode(selected_b).strip() == u'': return u'' mode = spec.get('mode', 'text') key = unicode(selected_b).strip() lines = [u'── Выбрано в колонке B: «%s» ──' % key] if mode in ('postprocess_range', 'postprocess_row'): rest = spec.get('rest_hints') or {} item = _rest_hint_lookup(rest, key) if item is None: fb = _pw_cfg._FALLBACK_PP_RANGE_REST_HINTS if mode == 'postprocess_range' else _pw_cfg._FALLBACK_PP_ROW_REST_HINTS item = _rest_hint_lookup(fb, key) if item is None: lines.append(u'Функция не найдена в каталоге макроса.') else: desc = unicode(item[0]).strip() if len(item) > 0 else u'' example = unicode(item[1]).strip() if len(item) > 1 else u'' if desc in (u'', u'не используется') or u'не используется' in desc: lines.append(u'Колонка C: не обязательна (можно оставить пустой).') else: lines.append(u'Колонка C: %s' % desc) if example != u'': lines.append(u'Пример C: %s' % example) elif mode == 'choice': vh = _pw_cfg.VALUE_HINTS.get(key, u'') if vh != u'': lines.append(vh) return u'\n'.join(lines) def _format_choices_catalog(spec, max_items=35): """Список допустимых значений колонки B с пояснениями.""" choices = spec.get('choices') or [] if len(choices) == 0: return u'' mode = spec.get('mode', 'text') rest = spec.get('rest_hints') or {} lines = [u'Доступные значения колонки B (%d):' % len(choices)] shown = 0 i = 0 while i < len(choices) and shown < max_items: key = unicode(choices[i]) line = u' • ' + key if mode in ('postprocess_range', 'postprocess_row'): item = _rest_hint_lookup(rest, key) if item and len(item) > 0: desc = unicode(item[0]).strip() if desc and desc != u'не используется' and (u'не используется' not in desc): line = line + u' — C: ' + desc if len(item) > 1 and unicode(item[1]).strip() != u'': line = line + u' (пр. C: %s)' % unicode(item[1]).strip() elif mode == 'choice': vh = _pw_cfg.VALUE_HINTS.get(key, u'') if vh: line = line + u' — ' + unicode(vh).strip() lines.append(line) shown = shown + 1 i = i + 1 if len(choices) > shown: lines.append(u' … и ещё %d' % (len(choices) - shown)) return u'\n'.join(lines) def _build_wizard_description(spec, sheet, selected_b=u''): """Полное описание параметра для поля «Описание».""" if spec is None: return u'' blocks = [] detail = u'' if unicode(selected_b).strip() != u'': detail = _format_selected_b_detail(spec, selected_b) if detail != u'': blocks.append(detail) base = spec.get('hint', u'') if base != u'': if detail != u'': blocks.append(_pw_cfg._DESC_GENERAL_INFO_SEPARATOR) blocks.append(base) catalog = _format_choices_catalog(spec) if catalog != u'': blocks.append(catalog) summary = _current_value_summary(sheet, spec) if summary != u'': blocks.append(summary) return u'\n\n'.join(blocks) def _current_value_summary(sheet, spec): name = spec['name'] mode = spec.get('mode', 'text') if spec.get('multi_row'): return u'' row = _find_param_row(sheet, name) if row < 0: return u'' if mode == 'files' or spec.get('multi_column'): vals = _row_values(sheet, row) if len(vals) == 0: return u'' return u'Текущие значения: ' + u'; '.join(vals) vals = _row_values(sheet, row) if len(vals) == 0: return u'' return u'Текущее значение: ' + vals[0] def _find_param_last_row(sheet, param_name): """Последняя строка с данным именем в A (для повторяемых параметров).""" row = 0 last = -1 while row < _pw_cfg.MERGE_PARAM_SHEET_MAX_ROWS: name = _cell_string(sheet.getCellByPosition(0, row)) if row == 0 and _param_match(name, u'Параметр'): row = row + 1 continue if name == u'': break if _param_match(name, param_name): last = row row = row + 1 return last def _read_primary_value(sheet, spec): if spec.get('mode') == 'files': return _read_files_paths_text(sheet, spec) if spec.get('multi_row'): row = _find_param_last_row(sheet, spec['name']) else: row = _find_param_row(sheet, spec['name']) if row < 0: return u'' vals = _row_values(sheet, row) if len(vals) == 0: return u'' return vals[0] def _read_extra_value(sheet, spec): if not spec.get('multi_row'): row = _find_param_row(sheet, spec['name']) else: row = _find_param_last_row(sheet, spec['name']) if row < 0: return u'' mode = spec.get('mode', 'text') if mode in ('postprocess_range', 'postprocess_row'): _primary, _pref, extra = _read_pp_row_values(sheet, row) return extra return _cell_string(sheet.getCellByPosition(2, row)) def _validate_primary_value(spec, primary): mode = spec.get('mode', 'text') if mode == 'source_extra': return (True, u'') if spec.get('col_c_subdialog'): return (True, u'') if mode == 'vlookup': return (True, u'') if mode in ('merge_sheets', 'split_sheets'): return (True, u'') if mode == 'number': if primary == u'': return (True, u'') try: int(primary) return (True, u'') except Exception: return (False, u'Ожидается целое число.') if mode == 'choice': choices = spec.get('choices') or [] if primary != u'' and len(choices) > 0 and (primary not in choices): return (False, u'Значение не из списка. Допустимо: %s' % u', '.join(choices[:8])) if mode in ('postprocess_range', 'postprocess_row'): choices = spec.get('choices') or [] if primary == u'': return (False, u'Выберите функцию постобработки (колонка B).') if _pp_is_plugin_primary(primary): return (True, u'') if len(choices) > 0: primary_cf = unicode(primary).strip().casefold() in_list = False i = 0 while i < len(choices): if unicode(choices[i]).strip().casefold() == primary_cf: in_list = True break i = i + 1 if not in_list and (not _pp_allows_freeform_b(primary)): try: if _is_sheet_block_form_fn(primary): in_list = True except Exception: pass if not in_list and (not _pp_allows_freeform_b(primary)): return (False, u'Функция не из списка. Выберите ключ или «функция_плагин».') return (True, u'') def _validate_plugin_ref(spec, primary, plugin_ref): if not _pp_is_plugin_primary(primary): return (True, u'') ref = unicode(plugin_ref or u'').strip() if ref == u'': return (False, u'Укажите ссылку или код в колонке C (кнопка «Редактор…»).') if _pp_is_plugin_json_c(ref): return (True, u'') if not _pp_allows_freeform_c(ref): return (False, u'Некорректная ссылка или код: functions_*.py#имя, lambda или def.') return (True, u'') def apply_param_value(sheet, spec, primary, extra=None, target_row=None, target_col=None, catalog=None, doc=None, plugin_ref=None): name = spec['name'] mode = spec.get('mode', 'text') if mode == 'vlookup': if not isinstance(primary, dict): primary = {} if primary.get('save_as_json'): json_c = unicode(primary.get('json_c') or u'').strip() def _write_vlookup_json_row(row): ci = 1 while ci <= 5: sheet.getCellByPosition(ci, row).String = u'' ci = ci + 1 sheet.getCellByPosition(2, row).String = json_c _ensure_vlookup_param_column_headers(sheet) _ensure_vlookup_param_column_formats(sheet) return row if target_row is not None and int(target_row) >= 0: tr = int(target_row) rname = _row_param_name(sheet, tr) if rname == u'' or _param_row_matches_name(rname, name): needs_shell = rname == u'' if rname == u'': sheet.getCellByPosition(0, tr).String = name row = _write_vlookup_json_row(tr) _ensure_param_row_formatting(sheet, row, catalog, spec, needs_shell, doc=doc) return row row = _first_empty_row(sheet) sheet.getCellByPosition(0, row).String = name row = _write_vlookup_json_row(row) _ensure_param_row_formatting(sheet, row, catalog, spec, True, doc=doc) return row left_b = primary.get('left_b', u'') right_c = primary.get('right_c', u'') keys_d = primary.get('keys_d', u'') extract_e = primary.get('extract_e', u'') color_f = primary.get('color_f', u'') def _write_vlookup_row(row): sheet.getCellByPosition(1, row).String = left_b sheet.getCellByPosition(2, row).String = right_c sheet.getCellByPosition(3, row).String = keys_d sheet.getCellByPosition(4, row).String = extract_e sheet.getCellByPosition(5, row).String = color_f _ensure_vlookup_param_column_headers(sheet) _ensure_vlookup_param_column_formats(sheet) _write_vlookup_cell_hints(sheet, row) return row if target_row is not None and int(target_row) >= 0: tr = int(target_row) rname = _row_param_name(sheet, tr) if rname == u'' or _param_row_matches_name(rname, name): needs_shell = rname == u'' if rname == u'': sheet.getCellByPosition(0, tr).String = name row = _write_vlookup_row(tr) _ensure_param_row_formatting(sheet, row, catalog, spec, needs_shell, doc=doc) return row row = _first_empty_row(sheet) sheet.getCellByPosition(0, row).String = name row = _write_vlookup_row(row) _ensure_param_row_formatting(sheet, row, catalog, spec, True, doc=doc) return row if mode in ('merge_sheets', 'split_sheets'): if isinstance(primary, dict): json_c = unicode(primary.get('json_c') or u'').strip() else: json_c = unicode(primary or u'').strip() def _write_splitter_json_row(row): ci = 1 while ci <= 5: sheet.getCellByPosition(ci, row).String = u'' ci = ci + 1 sheet.getCellByPosition(2, row).String = json_c return row if target_row is not None and int(target_row) >= 0: tr = int(target_row) rname = _row_param_name(sheet, tr) if rname == u'' or _param_row_matches_name(rname, name): needs_shell = rname == u'' if rname == u'': sheet.getCellByPosition(0, tr).String = name row = _write_splitter_json_row(tr) _ensure_param_row_formatting(sheet, row, catalog, spec, needs_shell, doc=doc) return row row = _first_empty_row(sheet) sheet.getCellByPosition(0, row).String = name row = _write_splitter_json_row(row) _ensure_param_row_formatting(sheet, row, catalog, spec, True, doc=doc) return row if mode == 'files': paths, _fd = _split_files_path_text(primary) if target_row is not None and int(target_row) >= 0: tr = int(target_row) rname = _row_param_name(sheet, tr) if rname == u'' or _param_row_matches_name(rname, name): if rname == u'': sheet.getCellByPosition(0, tr).String = name return _apply_files_paths_to_row(doc, sheet, tr, paths, catalog, spec) row = _find_param_row(sheet, name) if row < 0: row = _first_empty_row(sheet) sheet.getCellByPosition(0, row).String = name return _apply_files_paths_to_row(doc, sheet, row, paths, catalog, spec) if mode == 'source_extra': # Значения B/C/D… пишет субвизард; здесь только имя строки в A. if target_row is not None and int(target_row) >= 0: tr = int(target_row) rname = _row_param_name(sheet, tr) needs_shell = rname == u'' if rname == u'' or _param_row_matches_name(rname, name): if rname == u'': sheet.getCellByPosition(0, tr).String = name _ensure_param_row_formatting(sheet, tr, catalog, spec, needs_shell, doc=doc) return tr row = _find_param_row(sheet, name) if row < 0: row = _first_empty_row(sheet) sheet.getCellByPosition(0, row).String = name _ensure_param_row_formatting(sheet, row, catalog, spec, True, doc=doc) return row def _write_values_to_row(row): if spec.get('col_c_subdialog'): if _spec_col_c_keeps_b(spec) or spec.get('mode') == 'choice': sheet.getCellByPosition(1, row).String = unicode(primary or u'').strip() else: sheet.getCellByPosition(1, row).String = _pw_cfg._PARAM_B_SEE_COLUMN_C sheet.getCellByPosition(2, row).String = unicode(extra or u'').strip() if extra is not None else u'' return (row, 1) if spec.get('multi_column'): col = -1 if target_col is not None and int(target_col) >= 1: col = int(target_col) if col < 1: col = _first_empty_value_col(sheet, row) if col < 1: col = 1 sheet.getCellByPosition(col, row).String = primary return (row, col) if mode in ('postprocess_range', 'postprocess_row'): sheet.getCellByPosition(1, row).String = primary if _pp_is_plugin_primary(primary): sheet.getCellByPosition(2, row).String = unicode(plugin_ref or u'').strip() sheet.getCellByPosition(3, row).String = unicode(extra or u'').strip() if extra is not None else u'' else: if extra is not None: c_val = unicode(extra).strip() if _is_pivot_table_pp_name(primary): c_val = _pivot_canonical_storage_json(c_val) or c_val sheet.getCellByPosition(2, row).String = c_val sheet.getCellByPosition(3, row).String = u'' return (row, 1) if mode == 'choice' and _spec_has_inline_transfer(spec) and (extra is not None): sheet.getCellByPosition(1, row).String = primary sheet.getCellByPosition(2, row).String = extra return (row, 1) sheet.getCellByPosition(1, row).String = primary return (row, 1) def _finish_row(row, needs_shell): _ensure_param_row_formatting(sheet, row, catalog, spec, needs_shell, doc=doc) return row if target_row is not None and int(target_row) >= 0: tr = int(target_row) rname = _row_param_name(sheet, tr) if rname == u'' or _param_row_matches_name(rname, name): needs_shell = rname == u'' if rname == u'': sheet.getCellByPosition(0, tr).String = name row, _col = _write_values_to_row(tr) return _finish_row(row, needs_shell) if spec.get('multi_row'): row = _first_empty_row(sheet) sheet.getCellByPosition(0, row).String = name sheet.getCellByPosition(1, row).String = primary if _pp_is_plugin_primary(primary): sheet.getCellByPosition(2, row).String = unicode(plugin_ref or u'').strip() if extra is not None: sheet.getCellByPosition(3, row).String = extra elif extra is not None: sheet.getCellByPosition(2, row).String = extra return _finish_row(row, True) if spec.get('multi_column'): row = _find_param_row(sheet, name) needs_shell = False if row < 0: row = _first_empty_row(sheet) sheet.getCellByPosition(0, row).String = name needs_shell = True col = _first_empty_value_col(sheet, row) if col < 0: col = 1 sheet.getCellByPosition(col, row).String = primary return _finish_row(row, needs_shell) row = _find_param_row(sheet, name) needs_shell = False if row < 0: row = _first_empty_row(sheet) sheet.getCellByPosition(0, row).String = name needs_shell = True if spec.get('col_c_subdialog'): row, _col = _write_values_to_row(row) return _finish_row(row, needs_shell) sheet.getCellByPosition(1, row).String = primary if _pp_is_plugin_primary(primary): sheet.getCellByPosition(2, row).String = unicode(plugin_ref or u'').strip() if extra is not None: sheet.getCellByPosition(3, row).String = extra elif extra is not None and (mode in ('postprocess_range', 'postprocess_row') or (mode == 'choice' and _spec_has_inline_transfer(spec))): sheet.getCellByPosition(2, row).String = extra return _finish_row(row, needs_shell) def _clear_param_sheet_row(sheet, row, catalog=None, doc=None): """Очистить строку параметра (имя и значения), не трогая заголовок листа.""" if sheet is None or row is None or int(row) < 0: return False row = int(row) if row == 0: hdr = _row_param_name(sheet, 0) if hdr == u'' or _param_match(hdr, u'Параметр'): return False max_col = _param_sheet_max_value_col(sheet) try: sheet.getCellRangeByPosition(0, row, max_col, row).clearContents(7) except Exception: col = 0 while col <= max_col: try: sheet.getCellByPosition(col, row).setString(u'') except Exception: pass col = col + 1 if catalog is not None: last_row = max(int(row), _sheet_last_used_row(sheet, int(row))) _apply_param_sheet_validations(sheet, catalog, last_row, extra_empty_rows=3, doc=doc) _apply_param_sheet_used_area_format(sheet) return True def _param_wizard_select_row(doc, sheet, row): try: ctrl = doc.getCurrentController() if ctrl is not None: ctrl.setActiveSheet(sheet) ctrl.select(sheet.getCellRangeByPosition(0, int(row), 1, int(row))) except Exception: pass def _param_wizard_post_save_row(doc, sheet, catalog, spec, row): """Валидация и авто-высота одной строки после сохранения.""" with _pw_timed(u'param_wizard_post_save_row row=%d' % int(row)): last_col = _param_row_max_col(sheet, int(row), catalog) with _pw_timed(u'apply_param_row_validations'): _apply_param_row_validations(sheet, int(row), catalog, spec, doc=doc) with _pw_timed(u'param_autofit_row_heights (строка)'): _param_autofit_row_heights(doc, sheet, int(row), int(row), last_col) def _param_wizard_post_save_full(doc, sheet, catalog, spec, row): """Полное обновление оформления и списков всего листа параметров.""" with _pw_timed(u'param_wizard_post_save_full'): last_col = _param_sheet_max_value_col(sheet) last_row = max(int(row), _sheet_last_used_row(sheet, int(row))) with _pw_timed(u'apply_param_sheet_validations end_row=%d' % (last_row + 3)): _apply_param_sheet_validations(sheet, catalog, last_row, extra_empty_rows=3, doc=doc) with _pw_timed(u'apply_param_sheet_used_area_format'): _apply_param_sheet_used_area_format(sheet) with _pw_timed(u'param_apply_disabled_rows_styles'): _param_apply_disabled_rows_styles(sheet, catalog) with _pw_timed(u'param_autofit_row_heights (лист)'): _param_autofit_row_heights(doc, sheet, 0, last_row, last_col) def _param_wizard_post_save(doc, sheet, catalog, spec, row, refresh_full_sheet=False): """После сохранения параметра: одна строка или весь лист (по галочке на форме).""" with _pw_timed(u'param_wizard_post_save row=%d param=%s full=%s' % (int(row), spec.get('name', u'?'), refresh_full_sheet)): if refresh_full_sheet: _param_wizard_post_save_full(doc, sheet, catalog, spec, row) else: _param_wizard_post_save_row(doc, sheet, catalog, spec, row) with _pw_timed(u'controller select row'): _param_wizard_select_row(doc, sheet, row) def _show_message(message, title=u'Параметры', dialog=None, doc=None): text = unicode(message) if _wp_soft_gray_info_box(text, title=title, parent_dialog=dialog, doc=doc): return if _wp_message_box_execute(doc, dialog, MESSAGEBOX, MSG_BUTTONS.BUTTONS_OK, title, text) is not None: return try: cw = _try_import_collect() if cw is not None and doc is not None: cw.show_message(message, doc) return except Exception: pass try: toolkit = XSCRIPTCONTEXT.getDesktop().getToolkit() parent = None if dialog is not None: try: parent = dialog.getPeer() except Exception: pass if parent is None: parent = XSCRIPTCONTEXT.getDesktop().getCurrentFrame().getContainerWindow() box = toolkit.createMessageBox(parent, MESSAGEBOX, MSG_BUTTONS.BUTTONS_OK, title, text) box.execute() except Exception: print(text) def _show_file_picker(): return _show_file_picker_with_initial(u'') def _try_import_pick_source(): if _pw_cfg._PICK_SOURCE is False: return None if _pw_cfg._PICK_SOURCE is None: try: here = os.path.dirname(os.path.abspath(__file__)) if here not in sys.path: sys.path.insert(0, here) import pick_source_file as psf _pw_cfg._PICK_SOURCE = psf except Exception: _pw_cfg._PICK_SOURCE = False return _pw_cfg._PICK_SOURCE if _pw_cfg._PICK_SOURCE is not False else None def _split_files_path_text(text): t = unicode(text).strip() if t == u'': return ([], u';') if u';' in t: delim = u';' raw = t.split(u';') elif u',' in t: delim = u',' raw = t.split(u',') else: return ([t], u';') parts = [] i = 0 while i < len(raw): p = unicode(raw[i]).strip() if p != u'': parts.append(p) i = i + 1 return (parts, delim) def _join_files_path_text(parts, delim): if delim == u',': return u','.join([unicode(p) for p in parts]) if len(parts) == 0: return u'' return u'; '.join([unicode(p) for p in parts]) def _files_picker_initial(path_str): home = os.path.expanduser('~') if path_str is None or unicode(path_str).strip() == u'': return (home, u'') p = os.path.normpath(unicode(path_str).strip()) name = os.path.basename(p) if name in (u'', u'.'): name = u'' d = os.path.dirname(p) if d == u'': d = home elif not os.path.isdir(d): abs_d = os.path.dirname(os.path.abspath(p)) if abs_d != u'' and os.path.isdir(abs_d): d = abs_d elif os.path.isdir(os.path.abspath(p)): d = os.path.abspath(p) name = u'' else: d = home if os.path.isfile(p): d = os.path.dirname(os.path.abspath(p)) name = os.path.basename(p) return (d, name) def _normalize_files_source_path(doc, path): if path is None or unicode(path).strip() == u'': return path cw = _try_import_collect() if cw is None: return path doc_path = u'' if doc is not None: try: doc_path = cw._merge_doc_system_path(doc) except Exception: doc_path = u'' try: return unicode(cw.normalize_source_path_cell_value(unicode(path), doc_path)) except Exception: return path def _show_file_picker_with_initial(path_str, doc=None): try: ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() picker = sm.createInstanceWithContext('com.sun.star.ui.dialogs.FilePicker', ctx) picker.setTitle(u'Выбор файла-источника') start_dir, start_name = _files_picker_initial(path_str) dir_url = None try: abs_dir = os.path.abspath(unicode(start_dir).strip()) if os.path.isdir(abs_dir): dir_url = uno.systemPathToFileUrl(abs_dir) except Exception: dir_url = None if dir_url is not None: try: picker.setDisplayDirectory(dir_url) except Exception: pass if start_name != u'': try: picker.setDefaultName(start_name) except Exception: pass if picker.execute() != 1: return None urls = picker.getFiles() if urls is None or len(urls) == 0: return None return _normalize_files_source_path(doc, os.path.abspath(uno.fileUrlToSystemPath(urls[0]))) except Exception as e: print(u'Ошибка FilePicker: %s' % e) return None def _read_files_paths_text(sheet, spec): row = _find_param_row(sheet, spec['name']) if row < 0: return u'' vals = _row_values(sheet, row) if len(vals) == 0: return u'' return _join_files_path_text(vals, u';') def _files_prepare_value_column(doc, sheet, col): psf = _try_import_pick_source() if psf is not None and doc is not None: try: psf._prepare_value_column(doc, sheet, col) return except Exception: pass if col < 1: return hdr = _cell_string(sheet.getCellByPosition(col, 0)) if hdr == u'' or not _param_is_value_column_header(hdr): sheet.getCellByPosition(col, 0).String = u'Значение %d' % int(col) def _apply_files_paths_to_row(doc, sheet, row, paths, catalog, spec): col = 1 while col < 30: try: sheet.getCellByPosition(col, row).String = u'' except Exception: pass col = col + 1 col = 1 i = 0 while i < len(paths): if doc is not None: _files_prepare_value_column(doc, sheet, col) sheet.getCellByPosition(col, row).String = _normalize_files_source_path(doc, paths[i]) col = col + 1 i = i + 1 _ensure_param_row_formatting(sheet, row, catalog, spec, False, doc=doc) if doc is not None: _refresh_param_sheet_value_merges(doc, sheet, catalog) return row def _pw_content_y(y): """Y с учётом SoftGrayTitleBar: тема сдвигает контролы на _SOFT_GRAY_TITLE_H.""" try: return int(y) + int(_pw_cfg._SOFT_GRAY_TITLE_H) except Exception: return int(y) def _set_control_visible(control, visible, restore_y=None, restore_h=None, restore_w=None): """Показ/скрытие контрола; в AlterOffice/LO setVisible до createPeer часто не срабатывает.""" if control is None: return try: control.setVisible(visible) except Exception: pass try: control.setEnable(visible) except Exception: pass try: model = control.Model if model is not None: model.Enable = visible if restore_y is not None: # restore_y — логическая координата до soft-gray; при показе добавляем offset титла. model.PositionY = _pw_content_y(restore_y) if visible else _pw_cfg._HIDDEN_CTRL_Y if restore_h is not None: model.Height = restore_h if visible else 0 if restore_w is not None: model.Width = restore_w if visible else 0 except Exception: pass def _raise_control_z_order(control): """Поднять контрол на передний план (скрытый ComboBox часто перекрывает Edit).""" if control is None: return try: peer = control.getPeer() if peer is not None: peer.setZOrder(0) except Exception: pass try: peer = control.getPeer() if peer is not None and hasattr(peer, 'toFront'): peer.toFront() except Exception: pass def _sync_value_row_z_order(value_combo, value_edit, col_c_params_btn, show_combo, show_edit, col_c_mode): """Z-order строки B: активное поле и кнопка «Параметры…» поверх скрытых контролов.""" if show_combo: if show_edit: _raise_control_z_order(value_edit) _raise_control_z_order(value_combo) return if show_edit: _raise_control_z_order(value_edit) if col_c_mode: _raise_control_z_order(col_c_params_btn) def _set_control_enabled(control, enabled): if control is None: return try: control.setEnable(bool(enabled)) except Exception: pass try: m = control.Model if m is not None: m.Enable = bool(enabled) except Exception: pass def _focus_control(control): if control is None: return try: control.setFocus() except Exception: pass def _dlg_control(dialog, name): try: return dialog.getControl(name) except Exception: return None def _combo_resolve_fn_key(control): """ Ключ функции из ComboBox: выбранный пункт, точное совпадение или первый пункт списка по префиксу (как автодополнение LO при вводе). """ if control is None: return u'' items = [] try: seq = control.Model.StringItemList if seq is not None: i = 0 while i < len(seq): s = unicode(seq[i]).strip() if s != u'': items.append(s) i = i + 1 except Exception: pass raw = u'' try: t = control.getText() if t is not None: raw = unicode(t).strip() except Exception: pass if raw == u'': try: pos = control.getSelectedItemPos() if pos is not None and int(pos) >= 0 and (int(pos) < len(items)): return items[int(pos)] except Exception: pass return u'' raw_cf = raw.casefold() i = 0 while i < len(items): if items[i].casefold() == raw_cf: return items[i] i = i + 1 i = 0 while i < len(items): if items[i].casefold().startswith(raw_cf): return items[i] i = i + 1 try: pos = control.getSelectedItemPos() if pos is not None and int(pos) >= 0 and (int(pos) < len(items)): sel = items[int(pos)] if sel.casefold().startswith(raw_cf): return sel except Exception: pass return raw def _combo_control_text(control): """Текст ComboBox: выбранный пункт списка или введённый текст (LO).""" if control is None: return u'' try: pos = control.getSelectedItemPos() if pos is not None and int(pos) >= 0: items = control.Model.StringItemList if items is not None and int(pos) < len(items): return unicode(items[int(pos)]).strip() except Exception: pass try: t = control.getText() if t is not None and unicode(t).strip() != u'': return unicode(t).strip() except Exception: pass try: t = control.Model.Text if t is not None and unicode(t).strip() != u'': return unicode(t).strip() except Exception: pass return u'' def _set_combo_items(control, items, select_text=None): if control is None: return try: seq = tuple([unicode(x) for x in items]) control.Model.StringItemList = seq if select_text is not None and unicode(select_text) in seq: control.setText(unicode(select_text)) elif len(seq) > 0: control.setText(seq[0]) else: control.setText(u'') except Exception: pass def _combo_items_with_custom(items, select_name=None): """Список combo + значение, если его ещё нет в пресетах.""" names = [unicode(x) for x in items or []] custom = unicode(select_name or u'').strip() if custom == u'' or custom.casefold() == u'(глобальный)'.casefold(): return tuple(names) key_cf = custom.casefold() for n in names: if unicode(n).strip().casefold() == key_cf: return tuple(names) names.append(custom) return tuple(names) def _apply_combo_selection(control, items, select_name=None, select_index=None, clear=False): """ Надёжная установка списка и выбора в ComboBox (LO / AlterOffice). Возвращает фактически выбранное имя или пустую строку. """ if control is None: return u'' names = [unicode(x) for x in items or []] seq = tuple(names) try: control.Model.StringItemList = seq except Exception: pass idx = -1 if clear or len(names) == 0: try: control.setText(u'') except Exception: pass try: control.selectItemPos(-1, False) except Exception: pass try: control.Model.Text = u'' except Exception: pass _param_ui_yield() return u'' if select_index is not None: try: idx = int(select_index) except Exception: idx = -1 elif select_name is not None: key = unicode(select_name).strip().lower() if key != u'': i = 0 while i < len(names): if names[i].strip().lower() == key: idx = i break i = i + 1 if idx < 0 or idx >= len(names): if select_name is not None: custom = unicode(select_name).strip() if custom != u'': if custom.casefold() == u'(глобальный)'.casefold() and len(names) > 0: idx = 0 else: merged = _combo_items_with_custom(names, custom) if len(merged) != len(names): names = list(merged) try: control.Model.StringItemList = tuple(names) except Exception: pass key_cf = custom.casefold() i = 0 while i < len(names): if unicode(names[i]).strip().casefold() == key_cf: idx = i break i += 1 if idx < 0 or idx >= len(names): if select_name is not None and unicode(select_name).strip() == u'': try: control.setText(u'') except Exception: pass try: control.selectItemPos(-1, False) except Exception: pass try: control.Model.Text = u'' except Exception: pass _param_ui_yield() return u'' selected = names[idx] try: control.selectItemPos(idx, True) except Exception: pass try: control.setText(selected) except Exception: pass try: control.Model.Text = selected except Exception: pass try: control.Model.SelectedItems = (idx,) except Exception: pass _param_ui_yield() return selected def _wizard_dlg_add_button(dm, name, label, x, y, w, h): btn = dm.createInstance('com.sun.star.awt.UnoControlButtonModel') btn.Name = name btn.Label = label btn.PositionX = x btn.PositionY = y btn.Width = w btn.Height = h dm.insertByName(name, btn) _wizard_dlg_raise_control(dm, name) return btn def _wizard_dlg_control_name(control): try: return unicode(control.getModel().Name) except Exception: try: return unicode(control.Model.Name) except Exception: return u'' def _wizard_dlg_is_named_control(src, ctl, model_name): """Сравнение контрола по ссылке и имени модели (AlterOffice: is/== ненадёжны).""" if src is None: return False if ctl is not None and (src is ctl or src == ctl): return True if model_name: return _wizard_dlg_control_name(src) == unicode(model_name) return False def _wizard_dlg_raise_control(dm, name): """Interactive control поверх подписей (порядок insert в DialogModel).""" try: m = dm.getByName(name) dm.removeByName(name) dm.insertByName(name, m) except Exception: pass def _wizard_dlg_ctl_x(rx, lbl_w=None, gap=None): if lbl_w is None: lbl_w = _pw_cfg._INNER_LBL_W if gap is None: gap = _pw_cfg._INNER_LBL_GAP return int(rx) + int(lbl_w) + int(gap) def _wizard_dlg_ctl_w(right_w, lbl_w=None, gap=None): if lbl_w is None: lbl_w = _pw_cfg._INNER_LBL_W if gap is None: gap = _pw_cfg._INNER_LBL_GAP return max(80, int(right_w) - int(lbl_w) - int(gap)) def _wizard_dlg_combo_single_item(text, items): """Один пункт списка combo без разделителей — признак выбора из dropdown.""" cur = unicode(text or u'').strip() if cur == u'' or u',' in cur or u';' in cur or u'%' in cur: return u'' cur_cf = cur.casefold() for it in items or (): if unicode(it).strip().casefold() == cur_cf: return unicode(it).strip() return u'' def _wizard_dlg_combo_dropdown_token(control, items, ev=None): pos = -1 if ev is not None: try: pos = int(ev.Selected) except Exception: pos = -1 if pos < 0: try: p = control.getSelectedItemPos() if p is not None: pos = int(p) except Exception: pos = -1 seq = items or () if pos >= 0 and pos < len(seq): return unicode(seq[pos]).strip() try: seq2 = control.Model.StringItemList or () if pos >= 0 and pos < len(seq2): return unicode(seq2[pos]).strip() except Exception: pass return u'' def _wizard_dlg_token_has_sep(token, seps): t = unicode(token or u'') for s in seps or (): if s and s in t: return True return False def _wizard_dlg_append_token_list(text, new_token, seps=(u',', u';')): """Добавить токен через первый найденный разделитель или первый из seps.""" token = unicode(new_token or u'').strip() if token == u'': return unicode(text or u'').strip() if _wizard_dlg_token_has_sep(token, seps): return unicode(text or u'').strip() raw = unicode(text or u'').strip() sep = seps[0] if seps else u',' for s in seps: if s in raw: sep = s break if sep == u'%': parts = [p.strip() for p in raw.split(u'%') if p.strip()] if raw else [] else: parts = _vlookup_split_token_list(raw) token_cf = token.casefold() for p in parts: if unicode(p).strip().casefold() == token_cf: return sep.join(parts) if sep != u',' else _vlookup_join_token_list(parts) parts.append(token) if sep == u'%': return u'%'.join(parts) return _vlookup_join_token_list(parts) def _wizard_dlg_split_token_list(text, seps=(u',', u';', u'%')): raw = unicode(text or u'').strip() if raw == u'': return [] if u'%' in raw and (u',' not in raw and u';' not in raw): return [p.strip() for p in raw.split(u'%') if p.strip()] return _vlookup_split_token_list(raw) def _wizard_dlg_join_token_list(tokens, sep=u','): parts = [unicode(t).strip() for t in tokens if unicode(t).strip()] if sep == u'%': return u'%'.join(parts) return _vlookup_join_token_list(parts) def _wizard_dlg_remove_last_token(text, seps=(u',', u';', u'%')): parts = _wizard_dlg_split_token_list(text, seps=seps) if len(parts) == 0: return u'' raw = unicode(text or u'').strip() sep = u',' if u'%' in raw and u',' not in raw and u';' not in raw: sep = u'%' return _wizard_dlg_join_token_list(parts[:-1], sep=sep) def _wizard_dlg_add_fixed(dm, name, label, x, y, w, h, multiline=False, align=None, vertical_align=None): lbl = dm.createInstance('com.sun.star.awt.UnoControlFixedTextModel') lbl.Name = name lbl.Label = label lbl.PositionX = x lbl.PositionY = y lbl.Width = w lbl.Height = h if multiline: try: lbl.MultiLine = True except Exception: pass if align is not None: try: lbl.Align = align except Exception: pass if vertical_align is not None: try: lbl.VerticalAlign = vertical_align except Exception: pass dm.insertByName(name, lbl) return lbl def _wizard_dlg_add_edit(dm, name, x, y, w, h, multiline=False, readonly=False): edit = dm.createInstance('com.sun.star.awt.UnoControlEditModel') edit.Name = name edit.PositionX = x edit.PositionY = y edit.Width = w edit.Height = h if multiline: edit.MultiLine = True try: edit.VScroll = True edit.HScroll = False except Exception: pass if readonly: edit.ReadOnly = True dm.insertByName(name, edit) _wizard_dlg_raise_control(dm, name) return edit def _wizard_dlg_add_combo(dm, name, x, y, w, h): combo = dm.createInstance('com.sun.star.awt.UnoControlComboBoxModel') combo.Name = name combo.PositionX = x combo.PositionY = y combo.Width = w combo.Height = h combo.Dropdown = True combo.LineCount = 12 try: # Иначе AO может не слать TextListener при выборе из списка. combo.Autocomplete = False except Exception: pass dm.insertByName(name, combo) _wizard_dlg_raise_control(dm, name) return combo def _wizard_dlg_add_listbox(dm, name, x, y, w, h, multiselect=True): lb = dm.createInstance('com.sun.star.awt.UnoControlListBoxModel') lb.Name = name lb.PositionX = x lb.PositionY = y lb.Width = w lb.Height = h if multiselect: lb.MultiSelection = True dm.insertByName(name, lb) _wizard_dlg_raise_control(dm, name) return lb def _wizard_dlg_add_checkbox(dm, name, label, x, y, w, h): chk = dm.createInstance('com.sun.star.awt.UnoControlCheckBoxModel') chk.Name = name chk.Label = label chk.PositionX = x chk.PositionY = y chk.Width = w chk.Height = h dm.insertByName(name, chk) _wizard_dlg_raise_control(dm, name) return chk def _inner_grid_list_height(dh, list_top_y, m=10, min_h=72): """Высота списков: вплотную до панели лист/ключи, затем до футера OK.""" footer_y = _inner_dialog_footer_y(dh, m) avail = footer_y - list_top_y - _pw_cfg._INNER_GRID_LIST_GAP - _pw_cfg._INNER_GRID_CONTROLS_H - _pw_cfg._INNER_GRID_LIST_GAP return max(min_h, avail) def _inner_dialog_set_sizeable(dm): try: dm.Sizeable = True except Exception: pass def _wizard_rgb(r, g, b): return (int(r) << 16) | (int(g) << 8) | int(b) def _wizard_apply_soft_gray_theme(dm, title_text=None): try: from libre_macros_ui_theme import apply_soft_gray_theme return apply_soft_gray_theme(dm, title_text=title_text) except Exception: return False def _wizard_try_paint_titlebar(dlg, title_bg=None, title_fg=None): try: from libre_macros_ui_theme import try_paint_titlebar return try_paint_titlebar(dlg, title_bg=title_bg, title_fg=title_fg) except Exception: return False def _inner_dialog_footer_y(dh, m): return dh - m - _pw_cfg._INNER_BTN_H - 4 - _pw_cfg._INNER_CHK_H def _inner_dialog_height(default_dh): """ Единая высота микродиалогов: как у основного визарда (если известна), иначе — заданная по месту дефолтная. """ try: return int(_pw_cfg._DLG_H) except Exception: return int(default_dh) def _inner_dialog_add_footer(dm, m, dh, chk_w=200): """OK и Отмена в одну строку слева; чекбокс JSON — под кнопкой OK.""" y_btn = _inner_dialog_footer_y(dh, m) ok_x = m cancel_x = ok_x + _pw_cfg._INNER_BTN_W + _pw_cfg._INNER_BTN_GAP ok_btn = dm.createInstance('com.sun.star.awt.UnoControlButtonModel') ok_btn.Name = 'OkButton' ok_btn.Label = u'OK' try: ok_btn.DefaultButton = True except Exception: pass try: ok_btn.PushButtonType = 1 except Exception: pass ok_btn.PositionX = ok_x ok_btn.PositionY = y_btn ok_btn.Width = _pw_cfg._INNER_BTN_W ok_btn.Height = _pw_cfg._INNER_BTN_H dm.insertByName('OkButton', ok_btn) cancel_btn = dm.createInstance('com.sun.star.awt.UnoControlButtonModel') cancel_btn.Name = 'CancelButton' cancel_btn.Label = u'Отмена' try: cancel_btn.PushButtonType = 2 except Exception: pass cancel_btn.PositionX = cancel_x cancel_btn.PositionY = y_btn cancel_btn.Width = _pw_cfg._INNER_BTN_W cancel_btn.Height = _pw_cfg._INNER_BTN_H dm.insertByName('CancelButton', cancel_btn) _wizard_dlg_add_checkbox(dm, 'JsonChk', u'Сохранять как JSON', ok_x, y_btn + _pw_cfg._INNER_BTN_H + 4, chk_w, _pw_cfg._INNER_CHK_H) def _inner_dialog_shallow_copy_list(items): out = [] for item in items or []: if isinstance(item, dict): out.append(dict(item)) else: out.append(item) return out def _inner_dialog_duplicate_block_fields(block, exclude=None): """Копия полей блока для «+ Блок» (§3.4.2 — дублировать последнюю строку).""" skip = set(exclude or ()) if not isinstance(block, dict): return {} out = {} for key, val in block.items(): if key in ('v', 'fn') or key in skip: continue if isinstance(val, list): out[key] = _inner_dialog_shallow_copy_list(val) elif isinstance(val, dict): out[key] = dict(val) else: out[key] = val return out def _inner_dialog_sheet_text(sheet_ed): try: return unicode(sheet_ed.getText() or u'').strip() except Exception: return u'' def _inner_dialog_set_block_sheet(block, sheet): if sheet == u'': block.pop('sheet', None) else: block['sheet'] = sheet def _inner_block_list_sheet_prefix(sheet): sheet = unicode(sheet or u'').strip() return sheet if sheet else _pw_cfg._INNER_BLOCK_ALL_SHEETS_LABEL def _inner_block_list_label(sheet, tail=u''): prefix = _inner_block_list_sheet_prefix(sheet) tail = unicode(tail or u'').strip() if tail: return u'%s: %s' % (prefix, tail) return prefix def _inner_dialog_update_blocks_list_label(blocks_lb, block_index, label): """Обновить подпись одной строки списка блоков без смены выделения.""" try: sel = -1 try: sel = int(blocks_lb.getSelectedItemPos()) except Exception: sel = -1 items = list(blocks_lb.Model.StringItemList or ()) if 0 <= block_index < len(items): items[block_index] = unicode(label or u'') blocks_lb.Model.StringItemList = tuple(items) if sel >= 0 and sel < len(items): try: blocks_lb.selectItemPos(sel, True) except Exception: pass except Exception: pass def _inner_dialog_set_sheet_field_text(sheet_ed, text, guard): """Подставить лист в поле, не вызывая синхронизацию textChanged.""" guard[0] = True try: sheet_ed.setText(unicode(text or u'')) except Exception: pass finally: guard[0] = False def _is_sheet_list_panel_fn(fn_key): key = unicode(fn_key or u'').strip() if key == u'': return False try: from libre_macros_param_codec import normalize_fn_key key = normalize_fn_key(key) or key except Exception: pass return unicode(key).casefold() in _pw_cfg._SHEET_LIST_PANEL_FN_CF def _sheet_list_panel_allows_special(fn_key): key = unicode(fn_key or u'').strip() if key == u'': return False try: from libre_macros_param_codec import normalize_fn_key key = normalize_fn_key(key) or key except Exception: pass return unicode(key).casefold() in frozenset((x.casefold() for x in _pw_cfg._SHEET_LIST_SPECIAL_FNS)) def _is_sheet_block_form_fn(fn_key): try: from libre_macros_param_codec import is_sheet_block_form_fn return is_sheet_block_form_fn(fn_key) except Exception: return False def _is_sheet_block_rules_fn(fn_key): try: from libre_macros_param_codec import is_sheet_block_rules_fn return is_sheet_block_rules_fn(fn_key) except Exception: return False def _sheet_block_form_schema(fn_key): key = _inner_codec_dialog_effective_key(fn_key) kcf = unicode(key).casefold() for cand, entry in _pw_cfg._SHEET_BLOCK_FORM_SCHEMAS.items(): if unicode(cand).casefold() == kcf: if entry.get('alias_of'): base = _sheet_block_form_schema(entry.get('alias_of')) if base is None: return None merged = dict(base) for k, v in entry.items(): if k != 'alias_of': merged[k] = v return merged return entry return None def _sheet_list_panel_allows_convert(fn_key): try: from libre_macros_param_codec import sheet_block_form_allows_convert return sheet_block_form_allows_convert(fn_key) except Exception: return False def _inner_codec_dialog_supported(fn_key): key = unicode(fn_key or u'').strip() if key == u'': return False try: from libre_macros_param_codec import normalize_fn_key key = normalize_fn_key(key) or key except Exception: pass if _param_match(key, u'объединить_листы_в_один') or _param_match(key, u'разделить_листы'): return True if _is_pivot_table_pp_name(key): return True if _is_sheet_list_panel_fn(key): return True if _is_sheet_block_form_fn(key): return True if _is_sheet_block_rules_fn(key): return True k = key.casefold() for cand in _pw_cfg._INNER_CODEC_DIALOG_REGISTRY: if unicode(cand).casefold() == k: return True return False def _inner_codec_dialog_effective_key(fn_key): key = unicode(fn_key or u'').strip() if key == u'': return key try: from libre_macros_param_codec import normalize_fn_key key = normalize_fn_key(key) or key except Exception: pass k = key.casefold() for cand, entry in _pw_cfg._INNER_CODEC_DIALOG_REGISTRY.items(): if unicode(cand).casefold() == k: alias = entry.get('alias_of') return alias or cand return key def _pp_fn_has_json_params(fn_key): """Функция постобработки/финала с JSON в колонке C (кнопка «Параметры…»).""" key = unicode(fn_key or u'').strip() if key == u'' or _pp_is_plugin_primary(key): return False try: from libre_macros_param_codec import normalize_fn_key key = normalize_fn_key(key) or key except Exception: pass return _inner_codec_dialog_supported(key) def _pp_minimal_json_for_fn(fn_key, force_as_values=False): """Минимальный JSON-блок для колонки C: [{"v":1,"fn":…}].""" try: from libre_macros_param_codec import normalize_fn_key, param_encode except Exception: key = unicode(fn_key or u'').strip() if key == u'': return u'[]' block = {u'v': 1, u'fn': key} if force_as_values and _is_pivot_table_pp_name(key): block[u'as_values'] = True try: import json return json.dumps([block], ensure_ascii=False, separators=(',', ':')) except Exception: return u'[{"v":1,"fn":"%s"}]' % key key = normalize_fn_key(fn_key) if key == u'': return u'[]' block = {u'v': 1, u'fn': key} if force_as_values and key == u'сводная_таблица': block[u'as_values'] = True try: return param_encode(key, [block]) except Exception: try: import json return json.dumps([block], ensure_ascii=False, separators=(',', ':')) except Exception: return u'[{"v":1,"fn":"%s"}]' % key def _pp_extra_needs_json_seed(fn_key, extra_text): """True — в C пусто / не JSON / другой fn → подставить минимальный шаблон.""" key = unicode(fn_key or u'').strip() if key == u'' or not _pp_fn_has_json_params(key): return False raw = unicode(extra_text or u'').strip() if raw == u'': return True try: from libre_macros_param_codec import normalize_fn_key, param_decode blocks = param_decode(key, raw) except Exception: return True if not blocks: return True try: b0 = blocks[0] if isinstance(blocks[0], dict) else {} bfn = normalize_fn_key(b0.get('fn') or u'') return bfn != normalize_fn_key(key) except Exception: return True def _show_inner_codec_param_dialog(parent_dialog, fn_key, initial_text, doc=None, force_as_values=False): """ Простой «как у сводной» вложенный диалог-редактор: - поле C (многострочное) - ☑ Сохранять как JSON - OK/Отмена через PushButtonType """ try: from libre_macros_param_codec import param_decode, param_encode except Exception as err: _wp_message_box_execute(doc, parent_dialog, MESSAGEBOX, MSG_BUTTONS.BUTTONS_OK, u'Параметры', u'Не удалось загрузить libre_macros_param_codec: %s' % err) return None fn_key = _inner_codec_dialog_effective_key(fn_key) if unicode(fn_key).casefold() == u'объединить_листы_в_один'.casefold(): result = show_merge_sheets_param_dialog(parent_dialog, initial_text, doc=doc) if result is None: return None if isinstance(result, dict): return unicode(result.get(u'json_c') or u'').strip() or None return unicode(result or u'').strip() or None if unicode(fn_key).casefold() == u'разделить_листы'.casefold(): result = show_split_sheets_param_dialog(parent_dialog, initial_text, doc=doc) if result is None: return None if isinstance(result, dict): return unicode(result.get(u'json_c') or u'').strip() or None return unicode(result or u'').strip() or None if _is_pivot_table_pp_name(fn_key): init = unicode(initial_text or u'').strip() if init == u'': init = _pp_minimal_json_for_fn(fn_key, force_as_values=force_as_values) return show_pivot_table_param_dialog(parent_dialog, init, doc=doc, force_as_values=bool(force_as_values)) if _is_sheet_list_panel_fn(fn_key): try: return _show_sheet_list_panel_dialog(parent_dialog, fn_key, initial_text, doc=doc) except Exception: pass if unicode(fn_key).casefold() == u'сортировка'.casefold(): try: return _show_sort_param_dialog(parent_dialog, initial_text, doc=doc) except Exception: pass if unicode(fn_key).casefold() in ( u'текстовые_операции'.casefold(), u'нормализация_текста'.casefold(), u'нормализовать_текст'.casefold(), u'normalize_text'.casefold(), ): try: return _show_normalize_text_param_dialog(parent_dialog, initial_text, doc=doc) except Exception as err: _wp_message_box_execute( doc, parent_dialog, MESSAGEBOX, MSG_BUTTONS.BUTTONS_OK, fn_key, u'Ошибка диалога текстовых операций:\n%s' % unicode(err), ) return None if unicode(fn_key).casefold() in (u'раскрасить_блоки'.casefold(), u'раскрасить'.casefold()): try: return _show_colorize_blocks_param_dialog(parent_dialog, initial_text, doc=doc) except Exception: pass if _is_sheet_block_rules_fn(fn_key): try: return _show_block_rules_param_dialog(parent_dialog, fn_key, initial_text, doc=doc) except Exception as err: _wp_message_box_execute(doc, parent_dialog, MESSAGEBOX, MSG_BUTTONS.BUTTONS_OK, fn_key, u'Ошибка диалога правил:\n%s' % unicode(err)) return None if _is_sheet_block_form_fn(fn_key): try: return _show_sheet_block_form_dialog(parent_dialog, fn_key, initial_text, doc=doc) except Exception as err: _wp_message_box_execute(doc, parent_dialog, MESSAGEBOX, MSG_BUTTONS.BUTTONS_OK, fn_key, u'Ошибка диалога формы:\n%s' % unicode(err)) return None title = fn_key hint = u'' for cand, entry in _pw_cfg._INNER_CODEC_DIALOG_REGISTRY.items(): if unicode(cand).casefold() == unicode(fn_key).casefold(): title = entry.get('title') or title hint = entry.get('hint') or u'' break init = unicode(initial_text or u'').strip() if init == u'' and _pp_fn_has_json_params(fn_key): init = _pp_minimal_json_for_fn(fn_key, force_as_values=force_as_values) as_json_default = True parse_warn = None try: blocks = param_decode(fn_key, init) if not blocks and _pp_fn_has_json_params(fn_key): init = _pp_minimal_json_for_fn(fn_key, force_as_values=force_as_values) blocks = param_decode(fn_key, init) preview = param_encode(fn_key, blocks) if blocks else init except Exception as err: preview = init parse_warn = unicode(err) try: ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) dm = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialogModel', ctx) except Exception as err: _wp_message_box_execute(doc, parent_dialog, MESSAGEBOX, MSG_BUTTONS.BUTTONS_OK, title, u'Не удалось создать модель диалога: %s' % err) return None dw = int(round(520 * 1.0)) dh = _inner_dialog_height(int(round(360 * 1.0))) m = 10 dm.PositionX = 120 dm.PositionY = 80 dm.Width = dw dm.Height = dh dm.Title = title try: dm.Sizeable = True except Exception: pass y = m if hint: _wizard_dlg_add_fixed(dm, 'HintLbl', hint, m, y, dw - m * 2, 32, multiline=True) y += 36 _wizard_dlg_add_fixed(dm, 'MainLbl', u'Параметры (C):', m, y, 200, 14) y += 18 edit_h = dh - y - _pw_cfg._INNER_FOOTER_RESERVE _wizard_dlg_add_edit(dm, 'ParamEdit', m, y, dw - m * 2, edit_h, multiline=True) _inner_dialog_add_footer(dm, m, dh) dlg = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialog', ctx) dlg.setModel(dm) _nested_subdialog_create_peer( dlg, toolkit, doc, parent_dialog, center_in_main_wizard=bool(parent_dialog) ) try: dlg.getControl('ParamEdit').setText(preview) except Exception: pass try: dlg.getControl('JsonChk').setState(1 if as_json_default else 0) except Exception: pass if parse_warn: _wp_message_box_execute(doc, parent_dialog, MESSAGEBOX, MSG_BUTTONS.BUTTONS_OK, title, u'Не удалось разобрать текущее значение:\n%s\n\nОткрыт редактор с исходной строкой.' % parse_warn) if dlg.execute() != 1: return None raw = u'' try: raw = dlg.getControl('ParamEdit').getText().strip() except Exception: pass use_json = True try: use_json = bool(dlg.getControl('JsonChk').getState()) except Exception: pass try: blocks = param_decode(fn_key, raw) return param_encode(fn_key, blocks) except Exception as err: _wp_message_box_execute(doc, parent_dialog, MESSAGEBOX, MSG_BUTTONS.BUTTONS_OK, title, u'Ошибка параметров:\n%s' % unicode(err)) return None def _grid_create_column(col_model, title, width=90): try: col = col_model.createColumn() col.Title = title try: col.ColumnWidth = int(width) except Exception: pass col_model.addColumn(col) except Exception: pass def _delete_rows_mode_is_formula(mode_text): return unicode(mode_text or u'').strip().casefold() == u'formula' def _sync_delete_rows_mode_fields(fn_key, edits, combos, mode_text): """columns — столбцы активны; formula — поле формулы активно.""" if unicode(fn_key or u'').casefold() not in (u'удаление_строк'.casefold(), u'пропуск_пустых_строк'.casefold()): return is_formula = _delete_rows_mode_is_formula(mode_text) cols_ctl = edits.get('columns') formula_ctl = edits.get('formula') for ctl, enabled in ((cols_ctl, not is_formula), (formula_ctl, is_formula)): if ctl is None: continue try: ctl.setEnable(bool(enabled)) except Exception: try: ctl.Model.Enabled = bool(enabled) except Exception: pass def _sheet_block_form_label(block, schema): sheet = unicode(block.get('sheet') or u'').strip() fields = schema.get('fields') or () field_ids = [unicode(f.get('id') or u'') for f in fields] if u'old' in field_ids and u'new' in field_ids: mappings = block.get('mappings') or [] first = mappings[0] if isinstance(mappings, (list, tuple)) and mappings else {} if isinstance(first, dict): old = unicode(first.get('old') or block.get('old') or u'').strip() new = unicode(first.get('new') or block.get('new') or u'').strip() if old and new: tail = u'%s → %s' % (old, new) if len(tail) > 28: tail = tail[:28] + u'…' return _inner_block_list_label(sheet, tail) tail = u'' if len(fields) > 0: fid = fields[0].get('id') raw = block.get(fid) if isinstance(raw, (list, tuple)): val = u', '.join((unicode(x) for x in raw if unicode(x).strip() != u'')) elif raw is None: val = u'' else: val = unicode(raw).strip() tail = val[:24] + (u'…' if len(val) > 24 else u'') return _inner_block_list_label(sheet, tail) def _block_field_to_text(block, field_id, fn_key=None): if unicode(fn_key or u'').casefold() == u'градиент'.casefold() and field_id == 'sort_dir': raw = unicode(block.get(field_id) or u'').strip().casefold() if raw in (u'-', u'desc', u'убыв', u'по_убыв'): return u'-' if raw in (u'+', u'asc', u'возр', u'по_возр'): return u'+' return u'+' if unicode(fn_key or u'').casefold() == u'переименовать_столбцы'.casefold(): mappings = block.get('mappings') or [] first = mappings[0] if isinstance(mappings, (list, tuple)) and mappings else {} if field_id == 'old': return unicode((first or {}).get('old') or block.get('old') or u'').strip() if field_id == 'new': return unicode((first or {}).get('new') or block.get('new') or u'').strip() if unicode(fn_key or u'').casefold() == u'замена_значений'.casefold() and field_id in ('find', 'replace'): val = block.get(field_id) parts = [] if isinstance(val, (list, tuple)): for x in val: s = unicode(x).strip() if field_id == 'replace' and s.casefold() == u'_пусто_': parts.append(u'') elif s != u'': parts.append(s) else: s = unicode(val or u'').strip() if field_id == 'replace' and s.casefold() == u'_пусто_': parts.append(u'') elif s != u'': parts.append(s) return u'; '.join(parts) if field_id == 'ops': val = block.get('ops') if val is None or val == u'' or val == []: return u'' if isinstance(val, (unicode, str)): return unicode(val) try: import json return unicode(json.dumps(val, ensure_ascii=False)) except Exception: return unicode(val) if field_id == 'keep': raw = unicode(block.get('keep') or u'first').strip().casefold() return _pw_cfg.DEDUP_KEEP_LABEL.get(raw, unicode(block.get('keep') or u'first')) if field_id == 'output': if bool(block.get('mark_duplicates')): return _pw_cfg.DEDUP_OUTPUT_LABEL.get( u'mark', u'Маркировка (столбец)' ) raw = unicode(block.get('output') or u'inplace').strip().casefold() return _pw_cfg.DEDUP_OUTPUT_LABEL.get(raw, unicode(block.get('output') or u'inplace')) if field_id in ('key_columns', 'exclude_columns'): val = block.get(field_id) if val is None or val == u'' or val == []: return u'' if isinstance(val, (list, tuple)): parts = [unicode(x).strip() for x in val if unicode(x).strip()] else: parts = [unicode(val).strip()] if unicode(val).strip() else [] if not parts: return u'' try: from libre_macros_param_codec import join_column_name_tokens sep = u';' if any(u',' in p for p in parts) else u',' return join_column_name_tokens(parts, sep=sep) except Exception: return u','.join(parts) if fn_key == u'зебра_диапазон' and field_id in ('header', 'even', 'odd'): roles = block.get('roles') or {} spec = roles.get(field_id) or {} fill = unicode(spec.get('fill') or u'').strip() font = unicode(spec.get('font') or u'').strip() if fill and font: return u'%s/%s' % (fill, font) return fill or font if field_id in ('wrap', 'include_header', 'whole_sheet', 'whole_row', 'sort', 'bold', 'italic', 'font_color_auto', 'as_values', 'copy'): return u'1' if block.get(field_id) else u'0' val = block.get(field_id) if isinstance(val, (list, tuple)): return u', '.join((unicode(x) for x in val if unicode(x).strip())) if field_id in ('height_mm', 'width_mm', 'steps', 'fit_pages', 'font_size'): if val is None: return u'' if val == int(val): return unicode(int(val)) return unicode(val) if field_id in ('size_data', 'size_header'): if val is None: return u'' if val == int(val): return unicode(int(val)) return unicode(val) if field_id in ('threshold', 'threshold_min', 'threshold_max') and val is not None: try: fv = float(val) if fv == int(fv): return unicode(int(fv)) return unicode(fv) except (TypeError, ValueError): return unicode(val) if field_id == 'separator': return unicode(val or u'') if field_id == 'format': fmt = unicode(val or u'').strip() if unicode(fn_key or u'').casefold() in (u'формат_даты'.casefold(), u'формат_деньги'.casefold()): return fmt if fmt else u'(глобальный)' return fmt if field_id == 'mappings': items = val if isinstance(val, (list, tuple)) else [] parts = [] for item in items: if isinstance(item, dict): old = unicode(item.get('old') or u'').strip() new = unicode(item.get('new') or u'').strip() if old and new: parts.append(u'%s=%s' % (old, new)) return u', '.join(parts) if field_id == 'position': raw = unicode(val or u'').strip().casefold() rev = {u'start': u'_начало_', u'end': u'_конец_', u'before': u'_перед_', u'after': u'_после_'} return rev.get(raw, unicode(val or u'').strip()) return unicode(val or u'').strip() def _text_to_block_field(field_id, text, field_type=None): if field_type == 'bool': if isinstance(text, bool): return text text = unicode(text or u'').strip() return text.lower() not in (u'0', u'false', u'нет', u'no', u'-') if field_id == 'sort_dir': t = unicode(text or u'').strip().casefold() if t in (u'-', u'desc', u'убыв', u'по_убыв'): return u'по_убыв' if t in (u'+', u'asc', u'возр', u'по_возр'): return u'по_возр' return u'по_возр' if field_id in ('find', 'replace'): text = unicode(text or u'').strip() if text == u'': if field_id == 'replace': return [u'_ПУСТО_'] return [] parts = [] for p in re.split('[;,]', text): p = p.strip() if field_id == 'replace' and p == u'': parts.append(u'_ПУСТО_') elif p != u'': parts.append(p) if field_id == 'replace' and not parts: return [u'_ПУСТО_'] return parts if field_id == 'ops': text = unicode(text or u'').strip() if text == u'': return [] try: from libre_macros_normalize_lib import coerce_ops_list return coerce_ops_list(text) except Exception: try: import json parsed = json.loads(text) if isinstance(parsed, list): return parsed except Exception: pass return [{'op': p.strip()} for p in re.split(r'[;\n]+', text) if p.strip()] if field_id == 'separator': return unicode(text or u'') if field_id == 'format': t = unicode(text or u'').strip() if t == u'' or t == u'(глобальный)': return u'' try: gs_lib = _wp_import_global_settings_lib() if gs_lib and t == gs_lib['CUSTOM_NUMBER_FORMAT_LABEL']: return u'' except Exception: pass return t if field_id == 'mappings': text = unicode(text or u'').strip() if text == u'': return [] try: from libre_macros_param_codec import _parse_column_rename_mappings return _parse_column_rename_mappings(text) except Exception: out = [] for part in re.split('[,;]', text): part = part.strip() if u'=' in part: old, _, new = part.partition(u'=') elif u'->' in part: old, _, new = part.partition(u'->') else: continue old = old.strip() new = new.strip() if old and new: out.append({'old': old, 'new': new}) return out if field_id in ('old', 'new'): return unicode(text or u'').strip() text = unicode(text or u'').strip() if field_type == 'columns_pick' or field_id in ('key_columns', 'exclude_columns'): text = unicode(text or u'').strip() if text == u'': return [] try: from libre_macros_param_codec import split_column_name_tokens parts = split_column_name_tokens(text) if parts: return parts except Exception: pass out = [] for p in re.split('[,;]', text): p = p.strip() if p != u'': out.append(p) return out if field_id == 'keep': t = unicode(text or u'').strip().casefold() code = _pw_cfg.DEDUP_KEEP_CODE.get(t, t) return code if code in (u'first', u'last') else u'first' if field_id == 'output': t = unicode(text or u'').strip().casefold() code = _pw_cfg.DEDUP_OUTPUT_CODE.get(t, t) if code in (u'inplace', u'new_sheet', u'offset', u'mark'): return code return u'inplace' if field_id in ('markers', 'columns', 'new_names'): if text == u'': return [] out = [] for p in re.split('[,;]', text): p = p.strip() if p == u'': continue if field_id != 'new_names' and p.isdigit(): out.append(int(p)) else: out.append(p) return out if field_id == 'fit_pages': if text == u'': return 1 try: return int(text) except (TypeError, ValueError): return 1 if field_id == 'threshold': if text == u'': return 1000.0 try: return float(text.replace(u',', u'.')) except (TypeError, ValueError): return 1000.0 if field_id in ('threshold_min', 'threshold_max'): if text == u'': return None try: return float(text.replace(u',', u'.')) except (TypeError, ValueError): return None if field_id in ('height_mm', 'width_mm', 'steps', 'size_data', 'size_header', 'font_size'): if text in (u'', u'-', u'—'): return None try: return float(text.replace(u',', u'.')) except (TypeError, ValueError): return text return text def _zebra_role_from_field_text(text): text = unicode(text or u'').strip() if text == u'': return None fill, font = (text, u'') if u'/' in text: fill, _, font = text.partition(u'/') return {'fill': fill.strip(), 'font': font.strip()} def _sheet_block_field_row_height(fld, fn_key=None): ftype = fld.get('type') if unicode(fn_key or u'').casefold() == u'градиент'.casefold(): try: if unicode(fld.get('id') or u'').casefold() == u'sort_dir'.casefold(): return 0 except Exception: pass # Второй checkbox в pair_after / второй в row_group — не добавляет высоту if ftype == 'bool' and fld.get('pair_after'): return 0 if ftype == 'bool' and int(fld.get('row_group_slot') or 0) > 0: return 0 if fld.get('row_group') and int(fld.get('row_group_slot') or 0) > 0: return 0 if ftype == 'bool': return 16 + _pw_cfg._SHEET_BLOCK_FIELD_GAP if ftype == 'columns_pick': return 12 + 2 + 18 + _pw_cfg._SHEET_BLOCK_FIELD_GAP if fld.get('tall'): return 12 + 2 + 36 + _pw_cfg._SHEET_BLOCK_FIELD_GAP if ftype == 'combo' and fld.get('with_auto_check'): return 12 + 2 + 18 + 18 + _pw_cfg._SHEET_BLOCK_FIELD_GAP return 12 + 2 + 18 + _pw_cfg._SHEET_BLOCK_FIELD_GAP def _sheet_block_form_fields_height(fields, convert_flag=False, fn_key=None): total = 0 for fld in fields or (): total += _sheet_block_field_row_height(fld, fn_key=fn_key) if convert_flag: total += 24 return total def _show_sheet_block_form_dialog(parent_dialog, fn_key, initial_text, doc=None): """SheetBlockGrid (форма): блоки по листам + поля из схемы §6.1.""" from libre_macros_param_codec import param_decode, param_encode fn_key = _inner_codec_dialog_effective_key(fn_key) schema = _sheet_block_form_schema(fn_key) if schema is None: return None init = unicode(initial_text or u'').strip() _pw_dbg(u'=== sheet_block_form open fn=%s ===' % fn_key) _pw_dbg_kv(u'initial_text', init) try: blocks = param_decode(fn_key, init) except Exception as err: blocks = [] _pw_dbg(u'param_decode failed: %s' % unicode(err)) if _pw_cfg.PARAM_WIZARD_DEBUG: try: for i, b in enumerate(blocks or []): _pw_dbg(u'decoded[%d] repr=%s' % (i, repr(b))) except Exception: pass norm_blocks = [dict(b) for b in blocks if isinstance(b, dict)] if len(norm_blocks) == 0: empty = {'v': 1, 'fn': fn_key} for fld in schema.get('fields') or (): empty[fld['id']] = _text_to_block_field(fld['id'], fld.get('default'), fld.get('type')) norm_blocks = [empty] ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) dm = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialogModel', ctx) dw = 560 m = 10 fields = schema.get('fields') or () convert_flag = bool(schema.get('convert_existing')) form_bottom_pad = int(schema.get('form_bottom_pad') or 0) form_h = _sheet_block_form_fields_height(fields, convert_flag, fn_key=fn_key) + form_bottom_pad hint = schema.get('hint') or u'' hint_lines = int(schema.get('hint_lines') or 2) if hint else 0 hint_h = max(28, min(80, hint_lines * 14 + 6)) if hint else 0 list_top = m + (hint_h + 4 if hint else 0) + 22 + 16 list_h = max(72, min(110, form_h + 20)) # Низ правых полей и низ левой колонки (лист + «+ Блок» / «Удалить»). fields_bottom = list_top + form_h left_bottom = list_top + list_h + 8 + 40 + 16 content_bottom = max(fields_bottom, left_bottom) dh_content = content_bottom + 8 + _pw_cfg._INNER_FOOTER_RESERVE + m base_dh = _inner_dialog_height(_pw_cfg._INNER_GRID_DH) if schema.get('dialog_fit_content'): # Не раздувать до высоты родительского визарда: OK/Отмена/JSON # на уровне низа правых полей (последнее поле), без пустого низа. min_footer_y = left_bottom + 6 dh = max(fields_bottom + m, min_footer_y + _pw_cfg._INNER_FOOTER_RESERVE + m) else: dh = min(base_dh, dh_content) dm.PositionX = 120 dm.PositionY = 80 dm.Width = dw dm.Height = dh dm.Title = schema.get('title') or fn_key _inner_dialog_set_sizeable(dm) y = m if hint: _wizard_dlg_add_fixed(dm, 'HintLbl', hint, m, y, dw - m * 2, hint_h, multiline=True) y += hint_h + 4 same_all_init = len(norm_blocks) <= 1 and all((not unicode(b.get('sheet') or u'').strip() for b in norm_blocks)) _wizard_dlg_add_checkbox(dm, 'SameAllChk', u'Одинаково на всех листах', m, y, dw - m * 2, 16) y += 22 col_gap = 10 left_w = 190 right_w = dw - m * 2 - left_w - col_gap list_top_y = y + 16 _wizard_dlg_add_fixed(dm, 'BlocksLbl', u'Блоки:', m, y, left_w, 14) _wizard_dlg_add_fixed(dm, 'ParamsLbl', u'Параметры:', m + left_w + col_gap, y, right_w, 14) y += 16 _wizard_dlg_add_listbox(dm, 'BlocksLB', m, y, left_w, list_h, multiselect=False) rx = m + left_w + col_gap fy = y field_edits = {} field_checks = {} field_combos = {} field_choices = {} field_types = {} field_col_pick_btns = {} field_col_pick_from_headers = set() field_header_choice_combos = set() fields_by_id = {} for fld in fields: fields_by_id[fld['id']] = fld for fld in fields: fid = fld['id'] ftype = fld.get('type') field_types[fid] = ftype # pair_after bool — рисуем вместе с родительским combo if ftype == 'bool' and fld.get('pair_after'): continue row_h = _sheet_block_field_row_height(fld, fn_key=fn_key) if row_h <= 0 and ftype == 'bool' and int(fld.get('row_group_slot') or 0) > 0: continue lbl = fld.get('label') or fid if ftype == 'bool': if fn_key == u'градиент' and fid == 'sort': sort_cb_w = max(160, right_w - 86) _wizard_dlg_add_checkbox(dm, fid + 'Chk', lbl, rx, fy, sort_cb_w, 16) field_checks[fid] = fid + 'Chk' _wizard_dlg_add_combo(dm, 'sort_dirCb', rx + sort_cb_w + 6, fy - 2, right_w - sort_cb_w - 6, 18) field_combos['sort_dir'] = 'sort_dirCb' elif fld.get('row_group') and int(fld.get('row_group_slot') or 0) == 0: # Две галочки в один ряд (жирный + курсив) half = max(100, int((right_w - 8) / 2)) _wizard_dlg_add_checkbox(dm, fid + 'Chk', lbl, rx, fy, half, 16) field_checks[fid] = fid + 'Chk' peer = None for other in fields: if other.get('row_group') == fld.get('row_group') and int(other.get('row_group_slot') or 0) == 1: peer = other break if peer is not None: pid = peer['id'] _wizard_dlg_add_checkbox(dm, pid + 'Chk', peer.get('label') or pid, rx + half + 8, fy, half, 16) field_checks[pid] = pid + 'Chk' field_types[pid] = 'bool' else: _wizard_dlg_add_checkbox(dm, fid + 'Chk', lbl, rx, fy, right_w, 16) field_checks[fid] = fid + 'Chk' elif ftype == 'columns_pick': btn_w = 22 gap = 4 combo_w = max(80, right_w - 2 * btn_w - 2 * gap) _wizard_dlg_add_fixed(dm, fid + 'Lbl', lbl, rx, fy, right_w, 12) _wizard_dlg_add_combo(dm, fid + 'Cb', rx, fy + 12, combo_w, 18) _wizard_dlg_add_button(dm, fid + 'DelLastBtn', u'×', rx + combo_w + gap, fy + 12, btn_w, 16) _wizard_dlg_add_button(dm, fid + 'DelAllBtn', u'××', rx + combo_w + gap + btn_w + gap, fy + 12, btn_w, 16) field_combos[fid] = fid + 'Cb' field_choices[fid] = tuple(fld.get('choices') or ()) field_col_pick_btns[fid] = (fid + 'DelLastBtn', fid + 'DelAllBtn') # По умолчанию для columns_pick — заголовки листа (если нет явного choices). use_headers = fld.get('choices_from_headers') if use_headers is None: use_headers = not bool(fld.get('choices')) if use_headers: field_col_pick_from_headers.add(fid) elif ftype == 'combo': if fn_key == u'градиент' and fid == 'sort_dir': field_choices[fid] = tuple(fld.get('choices') or ()) fy += row_h continue auto_id = fld.get('with_auto_check') auto_fld = fields_by_id.get(auto_id) if auto_id else None _wizard_dlg_add_fixed(dm, fid + 'Lbl', lbl, rx, fy, right_w, 12) if auto_fld is not None: combo_w = max(120, int(right_w * 0.52)) chk_w = max(100, right_w - combo_w - 6) _wizard_dlg_add_combo(dm, fid + 'Cb', rx, fy + 12, combo_w, 18) field_combos[fid] = fid + 'Cb' field_choices[fid] = tuple(fld.get('choices') or ()) _wizard_dlg_add_checkbox(dm, auto_id + 'Chk', auto_fld.get('label') or auto_id, rx + combo_w + 6, fy + 14, chk_w, 16) field_checks[auto_id] = auto_id + 'Chk' field_types[auto_id] = 'bool' else: _wizard_dlg_add_combo(dm, fid + 'Cb', rx, fy + 12, right_w, 18) field_combos[fid] = fid + 'Cb' field_choices[fid] = tuple(fld.get('choices') or ()) if fld.get('choices_from_headers'): field_header_choice_combos.add(fid) elif fld.get('row_group'): slot = int(fld.get('row_group_slot') or 0) if slot > 0: continue half = max(100, int((right_w - 8) / 2)) edit_w = max(60, half - 4) _wizard_dlg_add_fixed(dm, fid + 'Lbl', lbl, rx, fy, half, 12) _wizard_dlg_add_edit(dm, fid + 'Ed', rx, fy + 12, edit_w, 18) field_edits[fid] = fid + 'Ed' peer = None for other in fields: if other.get('row_group') == fld.get('row_group') and int(other.get('row_group_slot') or 0) == 1: peer = other break if peer is not None: pid = peer['id'] plbl = peer.get('label') or pid _wizard_dlg_add_fixed(dm, pid + 'Lbl', plbl, rx + half + 8, fy, half, 12) _wizard_dlg_add_edit(dm, pid + 'Ed', rx + half + 8, fy + 12, edit_w, 18) field_edits[pid] = pid + 'Ed' field_types[pid] = peer.get('type') else: edit_h = 36 if fld.get('tall') else 18 _wizard_dlg_add_fixed(dm, fid + 'Lbl', lbl, rx, fy, right_w, 12) _wizard_dlg_add_edit(dm, fid + 'Ed', rx, fy + 12, right_w, edit_h) field_edits[fid] = fid + 'Ed' fy += row_h if fn_key in (u'формат_даты', u'формат_деньги', u'применить_формулу') and field_combos.get('format'): gs_lib = _wp_import_global_settings_lib() if gs_lib: blank = u'(глобальный)' if fn_key in (u'формат_деньги', u'применить_формулу'): calc_fn = gs_lib.get('calc_number_format_choices') if calc_fn is not None: base = (blank,) + tuple(calc_fn()) else: base = (blank,) + tuple(gs_lib['NUMBER_FORMAT_CHOICES']) else: base = (blank,) + tuple(gs_lib['DATE_FORMAT_CHOICES']) extra = [] seen = set((unicode(x).strip().casefold() for x in base)) for b in norm_blocks: fmt = unicode((b or {}).get('format') or u'').strip() if fmt == u'' or fmt.casefold() == blank.casefold(): continue if fmt.casefold() not in seen: seen.add(fmt.casefold()) extra.append(fmt) field_choices['format'] = base + tuple(extra) if convert_flag: conv_lbl = schema.get('convert_label') or u'Конвертировать текст в число/дату перед форматом' _wizard_dlg_add_checkbox(dm, 'ConvertChk', conv_lbl, rx, fy, right_w, 16) y2 = y + list_h + 8 _wizard_dlg_add_fixed(dm, 'SheetLbl', u'Лист (пусто = все листы):', m, y2, left_w, 14) _wizard_dlg_add_combo(dm, 'SheetEd', m, y2 + 16, left_w, 18) _wizard_dlg_add_button(dm, 'AddBlockBtn', u'+ Блок', m, y2 + 40, 80, 16) _wizard_dlg_add_button(dm, 'DelBlockBtn', u'Удалить', m + 90, y2 + 40, 80, 16) _inner_dialog_add_footer(dm, m, dh) if unicode(fn_key or u'').casefold() == u'применить_формулу'.casefold(): _wizard_apply_soft_gray_theme(dm, title_text=schema.get('title') or fn_key) dlg = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialog', ctx) dlg.setModel(dm) _nested_subdialog_create_peer( dlg, toolkit, doc, parent_dialog, center_in_main_wizard=bool(parent_dialog) ) if unicode(fn_key or u'').casefold() == u'применить_формулу'.casefold(): _wizard_try_paint_titlebar(dlg) blocks_lb = dlg.getControl('BlocksLB') sheet_ed = dlg.getControl('SheetEd') edits = {fid: dlg.getControl(name) for fid, name in field_edits.items()} checks = {fid: dlg.getControl(name) for fid, name in field_checks.items()} combos = {fid: dlg.getControl(name) for fid, name in field_combos.items()} col_pick_del_last = {} col_pick_del_all = {} columns_pick_guard = [False] columns_acc = {} columns_sep = {} for _cp_fid in field_col_pick_btns: columns_acc[_cp_fid] = [u''] columns_sep[_cp_fid] = u',' for fid, btn_names in field_col_pick_btns.items(): try: col_pick_del_last[fid] = dlg.getControl(btn_names[0]) except Exception: col_pick_del_last[fid] = None try: col_pick_del_all[fid] = dlg.getControl(btn_names[1]) except Exception: col_pick_del_all[fid] = None def _threshold_fn(): return unicode(fn_key or u'').casefold() in ( u'подсветка_по_порогу'.casefold(), u'подкрасить_пороги'.casefold(), u'условное_форматирование'.casefold(), ) def _has_columns_pick(): return bool(field_col_pick_btns) def _refresh_header_choice_combos(): if not field_header_choice_combos: return sheet_name = u'' try: sheet_name = _inner_dialog_sheet_text(sheet_ed) except Exception: sheet_name = u'' names = [] try: if sheet_name: names = _vlookup_list_sheet_columns(doc, sheet_name, u'1', u'1') if not names and doc is not None: try: active = doc.getCurrentController().getActiveSheet() nm = unicode(getattr(active, 'Name', u'') or u'') if nm: names = _vlookup_list_sheet_columns(doc, nm, u'1', u'1') except Exception: pass if not names: for sn in _pivot_list_doc_sheet_names(doc) or []: names = _vlookup_list_sheet_columns(doc, sn, u'1', u'1') if names: break except Exception: names = [] names_t = tuple(names) for fid in field_header_choice_combos: field_choices[fid] = names_t ctl = combos.get(fid) if ctl is None: continue try: cur = _combo_control_text(ctl) except Exception: cur = u'' try: _apply_combo_selection(ctl, names_t, select_name=cur) except Exception: pass def _refresh_columns_pick_choices(): if not field_col_pick_btns: return sheet_name = u'' try: sheet_name = _inner_dialog_sheet_text(sheet_ed) except Exception: sheet_name = u'' names = [] try: if sheet_name: names = _vlookup_list_sheet_columns(doc, sheet_name, u'1', u'1') if not names and doc is not None: try: active = doc.getCurrentController().getActiveSheet() nm = unicode(getattr(active, 'Name', u'') or u'') if nm: names = _vlookup_list_sheet_columns(doc, nm, u'1', u'1') except Exception: pass if not names: for sn in _pivot_list_doc_sheet_names(doc) or []: names = _vlookup_list_sheet_columns(doc, sn, u'1', u'1') if names: break except Exception: names = [] names_t = tuple(names) for fid in field_col_pick_btns: if fid in field_col_pick_from_headers: names_t = tuple(names) else: fld = fields_by_id.get(fid) or {} names_t = tuple(fld.get('choices') or ()) field_choices[fid] = names_t ctl = combos.get(fid) if ctl is None: continue if fid in columns_acc: cur = columns_acc[fid][0] else: cur = u'' try: cur = _combo_edit_text(ctl) except Exception: cur = u'' try: columns_pick_guard[0] = True # Не через _set_combo_items: при select_text='' оно подставляет первый пункт списка. try: ctl.Model.StringItemList = names_t except Exception: pass _set_combo_edit_text(ctl, cur) except Exception: pass finally: columns_pick_guard[0] = False def _columns_pick_parts_from_text(text, fid=None): known = field_choices.get(fid) if fid else None try: from libre_macros_param_codec import split_column_name_tokens parts = split_column_name_tokens(text, known_names=known) except Exception: parts = [] if not parts: for chunk in re.split(u'[,;]', unicode(text or u'')): t = unicode(chunk or u'').strip() if t != u'': parts.append(t) seen = set() out = [] for t in parts: k = t.casefold() if k in seen: continue seen.add(k) out.append(t) return out def _columns_pick_remember_sep(fid, text=None, parts=None): """Выбрать и запомнить разделитель по тексту / токенам / known headers.""" known = field_choices.get(fid) or () raw = unicode(text if text is not None else columns_acc.get(fid, [u''])[0] or u'') try: from libre_macros_param_codec import detect_column_name_list_sep sep = detect_column_name_list_sep(raw, known_names=known) except Exception: sep = u';' if u';' in raw else u',' toks = parts if parts is not None else _columns_pick_parts_from_text(raw, fid=fid) if any(u',' in unicode(t) for t in toks): sep = u';' # Уже зафиксированный «;» не откатываем на «,» (пока поле не очищено). prev = columns_sep.get(fid) if prev == u';' and unicode(raw or u'').strip() != u'': sep = u';' columns_sep[fid] = sep return sep def _columns_pick_join_parts(parts, fid=None): sep = columns_sep.get(fid) if fid else None try: from libre_macros_param_codec import join_column_name_tokens return join_column_name_tokens(parts, sep=sep) except Exception: use = sep if sep in (u',', u';') else u',' return use.join(parts) def _columns_pick_set_text(fid, parts): ctl = combos.get(fid) if ctl is None: return if any(u',' in unicode(p) for p in parts or ()): columns_sep[fid] = u';' text = _columns_pick_join_parts(parts, fid=fid) if fid in columns_acc: columns_acc[fid][0] = text try: columns_pick_guard[0] = True _set_combo_edit_text(ctl, text) except Exception: pass finally: columns_pick_guard[0] = False def _columns_pick_sync_from_acc(fid): ctl = combos.get(fid) if ctl is None: return acc = columns_acc.get(fid, [u''])[0] if fid in columns_acc else u'' _columns_pick_remember_sep(fid, text=acc) try: columns_pick_guard[0] = True try: ctl.Model.StringItemList = tuple(field_choices.get(fid) or ()) except Exception: pass _set_combo_edit_text(ctl, acc) except Exception: pass finally: columns_pick_guard[0] = False def _columns_pick_control_name(control): try: return unicode(control.getModel().Name) except Exception: try: return unicode(control.Model.Name) except Exception: return u'' def _columns_pick_fid_for_control(control): """Сопоставить combo с полем columns_pick (по имени модели; is/== для AO).""" if control is None: return None name = _columns_pick_control_name(control) for cp_fid in field_col_pick_btns: expect = cp_fid + u'Cb' if name == expect: return cp_fid ctl = combos.get(cp_fid) if ctl is None: continue if control is ctl or control == ctl: return cp_fid return None def _columns_pick_dropdown_token(control, items, ev=None): pos = -1 if ev is not None: try: pos = int(ev.Selected) except Exception: pos = -1 if pos < 0: try: pos = int(getattr(ev, 'Selected', -1)) except Exception: pos = -1 if pos < 0: try: p = control.getSelectedItemPos() if p is not None: pos = int(p) except Exception: pos = -1 seq = items or () if pos >= 0 and pos < len(seq): return unicode(seq[pos]).strip() try: seq2 = control.Model.StringItemList or () if pos >= 0 and pos < len(seq2): return unicode(seq2[pos]).strip() except Exception: pass return u'' def _columns_pick_is_single_dropdown_item(text, items): cur = unicode(text or u'').strip() if cur == u'': return u'' cur_cf = cur.casefold() for it in items or (): if unicode(it).strip().casefold() == cur_cf: return unicode(it).strip() if u',' in cur or u';' in cur: return u'' return u'' def _columns_pick_handle_select(cp_fid, token): """Накопить токен из списка и обновить блок/подпись слева.""" if not token: return False if not _columns_pick_append_token(cp_fid, token): return False bi = form_block_index[0] if bi < 0: return True _apply_form_to_block(bi) form_loading[0] = True try: _inner_dialog_update_blocks_list_label( blocks_lb, bi, _sheet_block_form_label(norm_blocks[bi], schema) ) finally: form_loading[0] = False return True def _columns_pick_handle_text(cp_fid, control): """textChanged для columns_pick: накопление или ручной ввод.""" if columns_pick_guard[0] or form_loading[0]: return cur = _combo_edit_text(control) if u',' in cur or u';' in cur: items = field_choices.get(cp_fid) or () picked = _columns_pick_is_single_dropdown_item(cur, items) if not picked: try: picked = _columns_pick_is_single_dropdown_item( cur, control.Model.StringItemList or () ) except Exception: picked = u'' if picked: acc = columns_acc.get(cp_fid, [u''])[0] if picked.casefold() != unicode(acc or u'').casefold(): _columns_pick_handle_select(cp_fid, picked) else: columns_acc[cp_fid][0] = cur _columns_pick_remember_sep(cp_fid, text=cur) bi = form_block_index[0] if bi >= 0: _apply_form_to_block(bi) form_loading[0] = True try: _inner_dialog_update_blocks_list_label( blocks_lb, bi, _sheet_block_form_label(norm_blocks[bi], schema) ) finally: form_loading[0] = False return columns_acc[cp_fid][0] = cur _columns_pick_remember_sep(cp_fid, text=cur) bi = form_block_index[0] if bi >= 0: _apply_form_to_block(bi) form_loading[0] = True try: _inner_dialog_update_blocks_list_label( blocks_lb, bi, _sheet_block_form_label(norm_blocks[bi], schema) ) finally: form_loading[0] = False return items = field_choices.get(cp_fid) or () picked = _columns_pick_is_single_dropdown_item(cur, items) if not picked: try: picked = _columns_pick_is_single_dropdown_item( cur, control.Model.StringItemList or () ) except Exception: picked = u'' if picked: # Выбор из списка подменил CSV одним пунктом — дописать, если это не весь acc. acc = columns_acc.get(cp_fid, [u''])[0] if picked.casefold() != unicode(acc or u'').casefold(): _columns_pick_handle_select(cp_fid, picked) else: # Тот же одиночный пункт уже в acc — всё равно синхронизировать блок/подпись. bi = form_block_index[0] if bi >= 0: _apply_form_to_block(bi) form_loading[0] = True try: _inner_dialog_update_blocks_list_label( blocks_lb, bi, _sheet_block_form_label(norm_blocks[bi], schema) ) finally: form_loading[0] = False return columns_acc[cp_fid][0] = cur _columns_pick_remember_sep(cp_fid, text=cur) bi = form_block_index[0] if bi >= 0: _apply_form_to_block(bi) form_loading[0] = True try: _inner_dialog_update_blocks_list_label( blocks_lb, bi, _sheet_block_form_label(norm_blocks[bi], schema) ) finally: form_loading[0] = False def _columns_pick_append_token(fid, token): if fid not in columns_acc or columns_pick_guard[0] or form_loading[0]: return False token = unicode(token or u'').strip() if token == u'': return False ctl = combos.get(fid) if ctl is None: return False try: from libre_macros_param_codec import append_column_name_token merged, sep = append_column_name_token( columns_acc[fid][0], token, sep=columns_sep.get(fid), known_names=field_choices.get(fid) or (), ) except Exception: merged = _append_csv_token_no_dup(columns_acc[fid][0], token) sep = u';' if (u';' in merged or u',' in token) else u',' columns_acc[fid][0] = merged columns_sep[fid] = sep columns_pick_guard[0] = True try: _set_combo_edit_text(ctl, merged) finally: columns_pick_guard[0] = False return True def _columns_pick_del_last(fid): if fid in columns_acc: parts = _columns_pick_parts_from_text(columns_acc[fid][0], fid=fid) if parts: parts = parts[:-1] if not parts: columns_sep[fid] = u',' columns_acc[fid][0] = _columns_pick_join_parts(parts, fid=fid) _columns_pick_sync_from_acc(fid) return ctl = combos.get(fid) if ctl is None: return try: cur = _combo_control_text(ctl) except Exception: cur = u'' parts = _columns_pick_parts_from_text(cur, fid=fid) if parts: parts = parts[:-1] if not parts: columns_sep[fid] = u',' _columns_pick_set_text(fid, parts) def _columns_pick_del_all(fid): columns_sep[fid] = u',' if fid in columns_acc: columns_acc[fid][0] = u'' _columns_pick_sync_from_acc(fid) return _columns_pick_set_text(fid, []) convert_chk = dlg.getControl('ConvertChk') if convert_flag else None same_all_chk = dlg.getControl('SameAllChk') doc_sheet_names = _pivot_list_doc_sheet_names(doc) if len(doc_sheet_names) > 0: try: _set_combo_items(sheet_ed, doc_sheet_names, select_text=u'') except Exception: pass try: same_all_chk.setState(1 if same_all_init else 0) except Exception: pass try: dlg.getControl('JsonChk').setState(1) except Exception: pass form_block_index = [-1] sheet_field_guard = [False] form_loading = [False] def _refresh_blocks(selected_index=0): form_loading[0] = True items = [_sheet_block_form_label(b, schema) for b in norm_blocks] try: blocks_lb.Model.StringItemList = tuple(items) except Exception: pass try: if len(items) > 0: blocks_lb.selectItemPos(max(0, min(selected_index, len(items) - 1)), True) except Exception: pass form_loading[0] = False def _block_index_selected(): try: pos = blocks_lb.getSelectedItemPos() return int(pos) if pos is not None else -1 except Exception: return -1 def _load_block_to_form(block_index): form_loading[0] = True form_block_index[0] = block_index if block_index < 0 or block_index >= len(norm_blocks): _inner_dialog_set_sheet_field_text(sheet_ed, u'', sheet_field_guard) for fid, ctl in edits.items(): try: ctl.setText(u'') except Exception: pass for fid, ctl in combos.items(): try: _set_combo_items(ctl, field_choices.get(fid) or (), select_text=u'') except Exception: pass for fid, ctl in checks.items(): try: ctl.setState(0) except Exception: pass if convert_chk is not None: try: convert_chk.setState(0) except Exception: pass _sync_delete_rows_mode_fields(fn_key, edits, combos, u'columns') form_loading[0] = False return b = norm_blocks[block_index] _pw_dbg(u'_load_block_to_form idx=%s roles=%s' % (block_index, repr(b.get('roles')))) _inner_dialog_set_sheet_field_text(sheet_ed, unicode(b.get('sheet') or u'').strip(), sheet_field_guard) _refresh_header_choice_combos() for fid, ctl in edits.items(): try: field_text = _block_field_to_text(b, fid, fn_key=fn_key) ctl.setText(field_text) _pw_dbg(u' field %s -> %r' % (fid, field_text)) except Exception: pass for fid, ctl in combos.items(): try: field_text = _block_field_to_text(b, fid, fn_key=fn_key) if fid in columns_acc: columns_acc[fid][0] = field_text _columns_pick_remember_sep(fid, text=field_text) # Если в токенах есть запятая — пересобрать через «;». parts = _columns_pick_parts_from_text(field_text, fid=fid) if len(parts) > 1 and columns_sep.get(fid) == u';': rebuilt = _columns_pick_join_parts(parts, fid=fid) columns_acc[fid][0] = rebuilt _columns_pick_sync_from_acc(fid) else: _apply_combo_selection(ctl, field_choices.get(fid) or (), select_name=field_text) _pw_dbg(u' combo %s -> %r' % (fid, field_text)) except Exception: pass for fid, ctl in checks.items(): try: ctl.setState(1 if b.get(fid) else 0) except Exception: pass if fn_key == u'градиент': try: sort_on = bool(b.get('sort')) except Exception: sort_on = False try: if combos.get('sort_dir') is not None: combos['sort_dir'].setEnable(bool(sort_on)) except Exception: try: combos['sort_dir'].Model.Enabled = bool(sort_on) except Exception: pass if convert_chk is not None: try: convert_chk.setState(1 if b.get('convert_existing') else 0) except Exception: pass if fn_key == u'заголовок_плюс_высота' or _threshold_fn(): fc = unicode(b.get('font_color') or u'').strip() auto_ctl = checks.get('font_color_auto') if auto_ctl is not None: try: if fc: auto_ctl.setState(0) else: auto_ctl.setState(1 if b.get('font_color_auto', True) else 0) except Exception: pass if _has_columns_pick(): _refresh_columns_pick_choices() _refresh_header_choice_combos() if unicode(fn_key or u'').casefold() in (u'удаление_строк'.casefold(), u'пропуск_пустых_строк'.casefold()): mode_text = unicode(b.get('mode') or u'columns') if combos.get('mode') is not None: try: mode_text = _combo_control_text(combos['mode']) except Exception: pass _sync_delete_rows_mode_fields(fn_key, edits, combos, mode_text) form_loading[0] = False def _sync_header_height_font_color_ui(clear_color=False, disable_auto=False): if form_loading[0]: return if fn_key != u'заголовок_плюс_высота' and (not _threshold_fn()): return fc_ctl = combos.get('font_color') auto_ctl = checks.get('font_color_auto') if fc_ctl is None or auto_ctl is None: return if clear_color: try: fc_ctl.setText(u'') except Exception: pass return if disable_auto: try: auto_ctl.setState(0) except Exception: pass return try: if bool(auto_ctl.getState()): fc_ctl.setText(u'') except Exception: pass def _apply_form_to_block(block_index): if block_index < 0 or block_index >= len(norm_blocks): return b = norm_blocks[block_index] _inner_dialog_set_block_sheet(b, _inner_dialog_sheet_text(sheet_ed)) for fid, ctl in edits.items(): try: raw = unicode(ctl.getText() or u'') except Exception: raw = u'' val = _text_to_block_field(fid, raw, field_types.get(fid)) if fn_key == u'зебра_диапазон' and fid in ('header', 'even', 'odd'): continue b[fid] = val for fid, ctl in combos.items(): try: if fid in columns_acc: raw = columns_acc[fid][0] if not unicode(raw or u'').strip(): # Acc ещё пуст, а в combo уже выбран пункт (AO: событие мимо is). raw = _combo_edit_text(ctl) columns_acc[fid][0] = raw else: raw = _combo_control_text(ctl) except Exception: raw = u'' val = _text_to_block_field(fid, raw, field_types.get(fid)) b[fid] = val if unicode(fn_key or u'').casefold() == u'переименовать_столбцы'.casefold(): old_name = unicode(b.get('old') or u'').strip() new_name = unicode(b.get('new') or u'').strip() if old_name and new_name: b['mappings'] = [{'old': old_name, 'new': new_name}] else: b.pop('mappings', None) b.pop('old', None) b.pop('new', None) if fn_key == u'зебра_диапазон': roles = {} for role_id in ('header', 'even', 'odd'): raw = u'' if role_id in edits: try: raw = unicode(edits[role_id].getText() or u'') except Exception: pass spec = _zebra_role_from_field_text(raw) if spec: roles[role_id] = spec _pw_dbg(u'_apply_form roles=%s' % repr(roles)) if roles: b['roles'] = roles else: b.pop('roles', None) for role_id in ('header', 'even', 'odd'): b.pop(role_id, None) for fid, ctl in checks.items(): try: b[fid] = bool(ctl.getState()) except Exception: b[fid] = False if fn_key == u'заголовок_плюс_высота' or _threshold_fn(): fc = unicode(b.get('font_color') or u'').strip() if fc: b['font_color_auto'] = False elif b.get('font_color_auto'): b['font_color'] = u'' if convert_chk is not None: try: if convert_chk.getState(): b['convert_existing'] = True else: b.pop('convert_existing', None) except Exception: b.pop('convert_existing', None) if fn_key == u'копировать_значения' and 'whole_sheet' in checks: try: if checks['whole_sheet'].getState(): b['whole_sheet'] = True b['columns'] = [] else: b.pop('whole_sheet', None) except Exception: b.pop('whole_sheet', None) if unicode(fn_key or u'').casefold() == u'удалить_дубликаты'.casefold(): out_code = unicode(b.get('output') or u'inplace').strip().casefold() if out_code == u'mark': b['mark_duplicates'] = True b.pop('output', None) b.pop('dest_sheet', None) b.pop('dest_cell', None) mc = unicode(b.get('mark_column') or u'').strip() if mc == u'': b['mark_column'] = u'Дубликаты' else: b['mark_duplicates'] = False b['output'] = out_code if out_code != u'new_sheet': b.pop('dest_sheet', None) if out_code != u'offset': b.pop('dest_cell', None) else: dc = unicode(b.get('dest_cell') or u'').strip() if dc == u'': b['dest_cell'] = u'A1' b.pop('mark_column', None) b['v'] = 1 b['fn'] = fn_key def _sync_sheet_field_to_block(): if sheet_field_guard[0] or form_loading[0]: return bi = form_block_index[0] if bi < 0 or bi >= len(norm_blocks): return _inner_dialog_set_block_sheet(norm_blocks[bi], _inner_dialog_sheet_text(sheet_ed)) form_loading[0] = True try: _inner_dialog_update_blocks_list_label( blocks_lb, bi, _sheet_block_form_label(norm_blocks[bi], schema) ) finally: form_loading[0] = False def _switch_block_form(block_index): prev = form_block_index[0] if prev >= 0 and prev != block_index: _apply_form_to_block(prev) form_loading[0] = True try: _inner_dialog_update_blocks_list_label( blocks_lb, prev, _sheet_block_form_label(norm_blocks[prev], schema) ) finally: form_loading[0] = False _load_block_to_form(block_index) def _add_block_form_empty(): prev = form_block_index[0] if prev >= 0: _apply_form_to_block(prev) _inner_dialog_update_blocks_list_label(blocks_lb, prev, _sheet_block_form_label(norm_blocks[prev], schema)) new_block = {'v': 1, 'fn': fn_key} for fld in fields: new_block[fld['id']] = _text_to_block_field(fld['id'], fld.get('default'), fld.get('type')) norm_blocks.append(new_block) new_idx = len(norm_blocks) - 1 _refresh_blocks(selected_index=new_idx) _load_block_to_form(new_idx) def _sync_same_all_ui(): same_on = False try: same_on = bool(same_all_chk.getState()) except Exception: pass for ctl_name in ('BlocksLB', 'BlocksLbl', 'SheetLbl', 'SheetEd', 'AddBlockBtn', 'DelBlockBtn'): try: dlg.getControl(ctl_name).Model.Enabled = not same_on except Exception: pass if same_on and len(norm_blocks) > 1: while len(norm_blocks) > 1: norm_blocks.pop() _refresh_blocks(0) _load_block_to_form(0) class _BlockFormHandler(unohelper.Base, XActionListener, XItemListener, XTextListener): def disposing(self, ev): pass def _sync_params_to_block(self): if form_loading[0]: return bi = form_block_index[0] if bi < 0: return _apply_form_to_block(bi) # Обновление StringItemList иначе сбрасывает selection и перезагружает форму. form_loading[0] = True try: _inner_dialog_update_blocks_list_label( blocks_lb, bi, _sheet_block_form_label(norm_blocks[bi], schema) ) finally: form_loading[0] = False def textChanged(self, ev): try: src = ev.Source except Exception: return if _wizard_dlg_is_named_control(src, sheet_ed, u'SheetEd'): _sync_sheet_field_to_block() elif src in edits.values(): self._sync_params_to_block() else: cp_fid = _columns_pick_fid_for_control(src) if cp_fid is not None: _columns_pick_handle_text(cp_fid, src) return # Fallback по имени модели (AO иногда даёт другой proxy). src_name = _columns_pick_control_name(src) for fid, ctl in combos.items(): if src_name == fid + u'Cb' or src is ctl or src == ctl: if fid in field_col_pick_btns: _columns_pick_handle_text(fid, ctl or src) elif (fn_key == u'заголовок_плюс_высота' or _threshold_fn()) and fid == u'font_color': _sync_header_height_font_color_ui(disable_auto=True) self._sync_params_to_block() else: self._sync_params_to_block() return def itemStateChanged(self, ev): try: src = ev.Source except Exception: return src_name = _columns_pick_control_name(src) if src == blocks_lb or src_name == u'BlocksLB': if form_loading[0]: return _switch_block_form(_block_index_selected()) elif src == same_all_chk or src_name == u'SameAllChk': if form_loading[0]: return _sync_same_all_ui() elif (fn_key == u'заголовок_плюс_высота' or _threshold_fn()) and ( src is checks.get('font_color_auto') or src_name == u'font_color_autoChk' ): try: if bool(src.getState()): _sync_header_height_font_color_ui(clear_color=True) except Exception: pass self._sync_params_to_block() elif fn_key == u'градиент' and (src is checks.get('sort') or src_name == u'sortChk'): try: sort_on = bool(src.getState()) except Exception: sort_on = False try: if combos.get('sort_dir') is not None: combos['sort_dir'].setEnable(bool(sort_on)) except Exception: try: combos['sort_dir'].Model.Enabled = bool(sort_on) except Exception: pass self._sync_params_to_block() elif unicode(fn_key or u'').casefold() in (u'удаление_строк'.casefold(), u'пропуск_пустых_строк'.casefold()) and ( src is combos.get('mode') or src_name == u'modeCb' ): try: mode_text = _combo_control_text(combos['mode']) except Exception: mode_text = u'columns' _sync_delete_rows_mode_fields(fn_key, edits, combos, mode_text) self._sync_params_to_block() else: cp_fid = _columns_pick_fid_for_control(src) if cp_fid is not None: if columns_pick_guard[0] or form_loading[0]: return token = _columns_pick_dropdown_token( src, field_choices.get(cp_fid) or (), ev ) if token == u'': try: token = _columns_pick_dropdown_token( src, src.Model.StringItemList or (), ev ) except Exception: token = u'' if token == u'': return _columns_pick_handle_select(cp_fid, token) return if _wizard_dlg_is_named_control(src, sheet_ed, u'SheetEd'): if form_loading[0]: return _sync_sheet_field_to_block() if _has_columns_pick(): _refresh_columns_pick_choices() _refresh_header_choice_combos() return for fid, ctl in checks.items(): if src is ctl or src == ctl or src_name == fid + u'Chk': self._sync_params_to_block() return if convert_chk is not None and (src is convert_chk or src_name == u'ConvertChk'): self._sync_params_to_block() return for fid, ctl in combos.items(): if src is ctl or src == ctl or src_name == fid + u'Cb': self._sync_params_to_block() return def actionPerformed(self, ev): cmd = u'' try: cmd = unicode(ev.ActionCommand) except Exception: pass if not cmd: try: cmd = _columns_pick_control_name(ev.Source) except Exception: cmd = u'' if cmd == u'AddBlockBtn': _add_block_form_empty() return if cmd == u'DelBlockBtn': bi = _block_index_selected() if 0 <= bi < len(norm_blocks): del norm_blocks[bi] form_block_index[0] = -1 _refresh_blocks(selected_index=max(0, bi - 1)) _switch_block_form(_block_index_selected()) return if cmd.endswith(u'DelLastBtn'): fid = cmd[:-len(u'DelLastBtn')] _columns_pick_del_last(fid) self._sync_params_to_block() return if cmd.endswith(u'DelAllBtn'): fid = cmd[:-len(u'DelAllBtn')] _columns_pick_del_all(fid) self._sync_params_to_block() return h = _BlockFormHandler() try: blocks_lb.addItemListener(h) sheet_ed.addTextListener(h) try: sheet_ed.addItemListener(h) except Exception: pass for ctl in edits.values(): ctl.addTextListener(h) for ctl in combos.values(): try: ctl.addItemListener(h) except Exception: pass try: ctl.addTextListener(h) except Exception: pass for ctl in checks.values(): try: ctl.addItemListener(h) except Exception: pass if convert_chk is not None: try: convert_chk.addItemListener(h) except Exception: pass try: same_all_chk.addItemListener(h) except Exception: pass except Exception: pass for btn in ('AddBlockBtn', 'DelBlockBtn'): try: ctl = dlg.getControl(btn) ctl.addActionListener(h) ctl.setActionCommand(btn) except Exception: pass for fid, btn_names in field_col_pick_btns.items(): for bname in btn_names: try: ctl = dlg.getControl(bname) ctl.addActionListener(h) ctl.setActionCommand(bname) except Exception: pass class _SheetBlockFooterHandler(unohelper.Base, XActionListener): def disposing(self, ev): pass def actionPerformed(self, ev): try: nm = unicode(ev.Source.getModel().Name) except Exception: return if nm == u'OkButton': try: dlg.endExecute(1) except Exception: try: dlg.endExecute() except Exception: pass elif nm == u'CancelButton': try: dlg.endExecute(0) except Exception: try: dlg.endExecute() except Exception: pass _footer_h = _SheetBlockFooterHandler() for _btn_nm in (u'OkButton', u'CancelButton'): try: dlg.getControl(_btn_nm).addActionListener(_footer_h) except Exception: pass _refresh_blocks(0) if _has_columns_pick(): _refresh_columns_pick_choices() _load_block_to_form(0 if len(norm_blocks) > 0 else -1) _sync_same_all_ui() if dlg.execute() != 1: return None bi = form_block_index[0] if bi < 0: bi = _block_index_selected() if bi < 0 and len(norm_blocks) > 0: bi = 0 if bi >= 0: _apply_form_to_block(bi) same_on = False try: same_on = bool(same_all_chk.getState()) except Exception: pass if same_on: out_blocks = [] if len(norm_blocks) > 0: b = dict(norm_blocks[0]) b.pop('sheet', None) out_blocks.append(b) else: empty = {'v': 1, 'fn': fn_key} for fld in fields: empty[fld['id']] = _text_to_block_field(fld['id'], fld.get('default'), fld.get('type')) out_blocks = [empty] return param_encode(fn_key, out_blocks) return param_encode(fn_key, norm_blocks) def _parse_delete_top_rows_rules_text(text): """ Разбор правил «ИмяЛиста -> N» (или «=», «\\») из текста. Разделители: `,` `;` и переносы строк. Возвращает dict: sheet_name -> n_rows (n_rows > 0). """ out = {} s = unicode(text or u'').strip() if s == u'': return out for part in re.split('[,;\\n\\r]+', s): part = unicode(part or u'').strip() if part == u'': continue sheet_name = u'' n_txt = u'' for sep in (u'->', u'=', u'\\'): if sep in part: left, right = part.split(sep, 1) sheet_name = unicode(left).strip() n_txt = unicode(right).strip() break if sheet_name == u'' or n_txt == u'': continue try: n = int(n_txt) except Exception: continue if n <= 0: continue out[sheet_name] = n return out def _delete_top_rows_mapping_to_rules_text(mapping): parts = [] if isinstance(mapping, dict): keys = sorted(list(mapping.keys()), key=lambda k: unicode(k).casefold()) for k in keys: try: n = int(mapping[k]) except Exception: continue if n > 0: parts.append(u'%s->%d' % (unicode(k).strip(), n)) return u', '.join(parts) def _try_decode_delete_top_rows_json(initial_text): """ Пытаться разобрать JSON вида {"Лист": N, ...} (или список объектов). Возвращает mapping или None при ошибке. """ s = unicode(initial_text or u'').strip() if s == u'': return {} if not (s.startswith(u'{') or s.startswith(u'[')): return None try: obj = json.loads(s) except Exception: try: if s.startswith(u'"') and s.endswith(u'"') and (len(s) >= 2): obj = json.loads(s[1:-1].replace(u'""', u'"')) else: return None except Exception: return None if isinstance(obj, dict): out = {} for k, v in obj.items(): sn = unicode(k or u'').strip() if sn == u'': continue try: n = int(v) except Exception: continue if n > 0: out[sn] = n return out if isinstance(obj, list): out = {} for item in obj: if not isinstance(item, dict): continue sn = unicode(item.get('sheet') or item.get('name') or item.get('s') or u'').strip() n_txt = item.get('n', item.get('rows', item.get('count', u''))) try: n = int(n_txt) except Exception: continue if sn != u'' and n > 0: out[sn] = n return out return None def show_delete_top_rows_param_dialog(parent_dialog=None, initial_text=u'', doc=None): return _show_sheet_block_form_dialog(parent_dialog, u'удаление_верхних_строк', initial_text, doc=doc) def show_skip_source_rows_param_dialog(parent_dialog=None, initial_text=u'', doc=None): """JSON для «Пропуск_строк_источника»: sheet = лист источника, columns.""" return _show_sheet_block_form_dialog(parent_dialog, u'пропуск_строк_источника', initial_text, doc=doc) def _source_extra_files_param_name(): cw = _try_import_collect() if cw is not None: return getattr(cw, 'P_MERGE_FILES', u'Файлы-Источники') return u'Файлы-Источники' def _source_extra_path_for_col(sheet, col): """Путь источника из строки «Файлы-Источники» для столбца col (1=B).""" if sheet is None: return u'' try: col_i = int(col) except (TypeError, ValueError): return u'' if col_i < 1: return u'' row = _find_param_row(sheet, _source_extra_files_param_name()) if row < 0: return u'' return _cell_string(sheet.getCellByPosition(col_i, row)) def _source_extra_column_labels(sheet, min_cols=1): """ Подписи колонок для списка «Доп_Параметры_Источника». Возвращает list[(col_index, label)] где col_index 1=B. """ paths = [] row = _find_param_row(sheet, _source_extra_files_param_name()) if row >= 0: paths = _wp_read_row_values(sheet, row, max_col=40) n = max(int(min_cols or 1), len(paths), 1) out = [] i = 0 while i < n: col = i + 1 path = paths[i] if i < len(paths) else u'' short = path if path != u'': try: short = os.path.basename(path.replace(u'\\', u'/')) except Exception: short = path if len(short) > 48: short = short[:45] + u'…' if path == u'': label = u'Значение %d (столбец %s) — источник не задан' % (col, _col_letter(col)) else: label = u'Значение %d (%s): %s' % (col, _col_letter(col), short) out.append((col, label)) i = i + 1 return out def _col_letter(col_1based): """1 → B, 2 → C (столбцы значений параметра).""" try: n = int(col_1based) + 1 # B=2 in A1 except (TypeError, ValueError): n = 2 letters = u'' while n > 0: n, rem = divmod(n - 1, 26) letters = unichr(65 + rem) + letters return letters or u'B' def _source_extra_is_csv_path(path): p = unicode(path or u'').strip().lower() if p.endswith(u'.csv'): return True try: from libre_macros_csv_source_lib import is_csv_source_path return bool(is_csv_source_path(path)) except Exception: return False def _source_extra_is_book_path(path): p = unicode(path or u'').strip().lower() if p.endswith((u'.ods', u'.xls', u'.xlsx', u'.xlsm')): return True try: from libre_macros_book_source_lib import is_book_source_path return bool(is_book_source_path(path)) except Exception: return False def show_source_extra_param_dialog(parent_dialog=None, initial_text=u'', doc=None, source_path=u'', target_col=1): """ Диспетчер субвизарда «Доп_Параметры_Источника» по расширению источника. """ if _source_extra_is_csv_path(source_path): return _show_source_extra_csv_param_dialog( parent_dialog, initial_text, doc, source_path, target_col, ) if _source_extra_is_book_path(source_path): return show_source_extra_book_param_dialog( parent_dialog, initial_text, doc, source_path, target_col, ) return _show_source_extra_unknown_param_dialog( parent_dialog, initial_text, doc, source_path, target_col, ) def _show_source_extra_unknown_param_dialog(parent_dialog=None, initial_text=u'', doc=None, source_path=u'', target_col=1): col_letter = _col_letter(target_col) path_note = unicode(source_path or u'').strip() if path_note == u'': path_note = u'(путь источника не задан в «Файлы-Источники»)' ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) dm = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialogModel', ctx) dw = 480 dh = _inner_dialog_height(220) m = 10 dm.PositionX = 150 dm.PositionY = 100 dm.Width = dw dm.Height = dh dm.Title = u'Доп. параметры источника (%s)' % col_letter _inner_dialog_set_sizeable(dm) hint = ( u'Столбец %s. Источник:\n%s\n\n' u'Задайте путь к .csv или к книге (.ods / .xls / .xlsx / .xlsm) в «Файлы-Источники», ' u'затем откройте этот диалог снова.' % (col_letter, path_note) ) _wizard_dlg_add_fixed(dm, 'HintLbl', hint, m, m, dw - m * 2, 80, multiline=True) _inner_dialog_add_footer(dm, m, dh) dlg = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialog', ctx) dlg.setModel(dm) _nested_subdialog_create_peer( dlg, toolkit, doc, parent_dialog, center_in_main_wizard=bool(parent_dialog) ) if dlg.execute() != 1: return None return unicode(initial_text or u'').strip() def show_source_extra_book_param_dialog(parent_dialog=None, initial_text=u'', doc=None, source_path=u'', target_col=1): """Субвизард book-extra: prepare_source_in_memory и субпараметры §3.2.""" try: from libre_macros_csv_source_lib import parse_source_extra_dict from libre_macros_source_extra_lib import ( SOURCE_EXTRA_DEFAULT_CHUNK_ROWS, encode_source_extra_book_json, source_extra_chunk_rows, source_extra_convert_types, source_extra_merged_cells, source_extra_prepare_in_memory, source_extra_trim_empty, source_extra_values_mode, ) except Exception: def parse_source_extra_dict(raw): return None SOURCE_EXTRA_DEFAULT_CHUNK_ROWS = 50000 def source_extra_prepare_in_memory(extra): return False def source_extra_values_mode(extra): return u'cached' def source_extra_merged_cells(extra): return u'top_left' def source_extra_trim_empty(extra, prepare_active=False): return bool(prepare_active) def source_extra_convert_types(extra): return u'in_memory' def source_extra_chunk_rows(extra): return SOURCE_EXTRA_DEFAULT_CHUNK_ROWS def encode_source_extra_book_json(extra, prepare_active=False): import json try: return unicode(json.dumps(extra or {}, ensure_ascii=False)) except Exception: return u'' init = unicode(initial_text or u'').strip() extra = parse_source_extra_dict(init) or {} cur_prep = bool(source_extra_prepare_in_memory(extra)) cur_values = source_extra_values_mode(extra) cur_merge = source_extra_merged_cells(extra) cur_trim = source_extra_trim_empty(extra, prepare_active=cur_prep) cur_convert = source_extra_convert_types(extra) cur_chunk = unicode(source_extra_chunk_rows(extra)) col_letter = _col_letter(target_col) path_note = unicode(source_path or u'').strip() if path_note == u'': path_note = u'(путь источника не задан в «Файлы-Источники»)' elif len(path_note) > 70: path_note = u'…' + path_note[-67:] ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) dm = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialogModel', ctx) dw = 520 dh = _inner_dialog_height(390) m = 10 dm.PositionX = 140 dm.PositionY = 80 dm.Width = dw dm.Height = dh dm.Title = u'Доп. параметры источника — книга (%s)' % col_letter _inner_dialog_set_sizeable(dm) y = m hint = ( u'Столбец %s. Источник:\n%s\n' u'Книга ODS/XLS/XLSX — подготовка в памяти для режимов «На один/разные листы» ' u'и буфера по заголовкам/позиции.' % (col_letter, path_note) ) _wizard_dlg_add_fixed(dm, 'HintLbl', hint, m, y, dw - m * 2, 52, multiline=True) y += 56 field_defs = ( (u'PrepLbl', u'Подготовка источника в памяти:', u'PrepCombo'), (u'ValuesLbl', u'Значения ячеек:', u'ValuesCombo'), (u'MergeLbl', u'Объединённые ячейки:', u'MergeCombo'), (u'TrimLbl', u'Обрезать пустые края:', u'TrimCombo'), (u'ConvertLbl', u'Числа и даты:', u'ConvertCombo'), (u'ChunkLbl', u'Строк в чанке памяти:', u'ChunkCombo'), ) fi = 0 while fi < len(field_defs): lbl, label, combo = field_defs[fi] _wizard_dlg_add_fixed(dm, lbl, label, m, y, dw - m * 2, 12) y += 14 _wizard_dlg_add_combo(dm, combo, m, y, dw - m * 2, 16) y += 22 fi = fi + 1 _inner_dialog_add_footer(dm, m, dh) dlg = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialog', ctx) dlg.setModel(dm) _nested_subdialog_create_peer( dlg, toolkit, doc, parent_dialog, center_in_main_wizard=bool(parent_dialog) ) def _fill_combo(name, choices, cur_code, code_to_label=None): combo_ctl = dlg.getControl(name) labels = [] sel = 0 ci = 0 while ci < len(choices): code, label = choices[ci] labels.append(label) if unicode(code) == unicode(cur_code): sel = ci ci = ci + 1 try: _set_combo_items(combo_ctl, labels, labels[sel] if labels else u'') except Exception: pass try: if combo_ctl is not None: combo_ctl.setText(labels[sel] if labels else u'') except Exception: pass return combo_ctl prep_choices = getattr(_pw_cfg, '_SOURCE_EXTRA_PREPARE_IN_MEMORY_CHOICES', None) or ( (u'нет', u'Нет — UNO (по умолчанию)'), (u'да', u'Да — бандл → матрица'), ) _fill_combo(u'PrepCombo', prep_choices, u'да' if cur_prep else u'нет') values_choices = getattr(_pw_cfg, '_SOURCE_EXTRA_VALUES_MODE_CHOICES', None) or ( (u'cached', u'Кэш / вычисленные'), (u'formulas', u'Текст формул'), (u'prefer_cached', u'Кэш, иначе формула'), ) _fill_combo(u'ValuesCombo', values_choices, cur_values) merge_choices = getattr(_pw_cfg, '_SOURCE_EXTRA_MERGED_CELLS_CHOICES', None) or ( (u'top_left', u'Только левая верхняя'), (u'fill', u'Размножить на объединение'), ) _fill_combo(u'MergeCombo', merge_choices, cur_merge) trim_choices = getattr(_pw_cfg, '_SOURCE_EXTRA_TRIM_EMPTY_CHOICES', None) or ( (u'да', u'Да'), (u'нет', u'Нет'), ) _fill_combo(u'TrimCombo', trim_choices, u'да' if cur_trim else u'нет') convert_choices = getattr(_pw_cfg, '_SOURCE_EXTRA_CONVERT_TYPES_CHOICES', None) or ( (u'in_memory', u'В памяти'), (u'uno', u'Оставить для UNO'), (u'no', u'Не конвертировать'), (u'keep_text', u'Всё текстом'), ) _fill_combo(u'ConvertCombo', convert_choices, cur_convert) chunk_choices = getattr(_pw_cfg, '_SOURCE_EXTRA_CHUNK_ROWS_CHOICES', None) or ( (u'10000', u'10 000'), (u'25000', u'25 000'), (u'50000', u'50 000'), (u'100000', u'100 000'), ) chunk_sel = unicode(SOURCE_EXTRA_DEFAULT_CHUNK_ROWS) ci = 0 while ci < len(chunk_choices): if unicode(chunk_choices[ci][0]) == cur_chunk: chunk_sel = cur_chunk break ci = ci + 1 _fill_combo(u'ChunkCombo', chunk_choices, chunk_sel) sub_combos = (u'ValuesCombo', u'MergeCombo', u'TrimCombo', u'ConvertCombo', u'ChunkCombo') def _set_sub_enabled(enabled): ei = 0 while ei < len(sub_combos): try: dlg.getControl(sub_combos[ei]).Enabled = bool(enabled) except Exception: pass ei = ei + 1 _set_sub_enabled(cur_prep) class _PrepListener(unohelper.Base, XItemListener): def disposing(self, ev): pass def itemStateChanged(self, ev): try: prep_ctl = dlg.getControl(u'PrepCombo') txt = unicode(prep_ctl.getText() or u'').strip().lower() _set_sub_enabled(txt.startswith(u'д') or txt == u'yes' or u'да' in txt) except Exception: pass try: prep_combo = dlg.getControl(u'PrepCombo') prep_combo.addItemListener(_PrepListener()) except Exception: pass if dlg.execute() != 1: return None def _combo_code(combo_name, choices, default_code): chosen = u'' try: chosen = unicode(dlg.getControl(combo_name).getText() or u'').strip() except Exception: chosen = u'' ci = 0 while ci < len(choices): code, label = choices[ci] if chosen == label or chosen == code: return code ci = ci + 1 return default_code prep_code = _combo_code(u'PrepCombo', prep_choices, u'нет') prep_val = prep_code in (u'да', u'yes', u'1', u'true') payload = {u'prepare_source_in_memory': bool(prep_val)} if prep_val: payload[u'values_mode'] = _combo_code(u'ValuesCombo', values_choices, u'cached') payload[u'merged_cells'] = _combo_code(u'MergeCombo', merge_choices, u'top_left') trim_code = _combo_code(u'TrimCombo', trim_choices, u'да') payload[u'trim_empty'] = trim_code in (u'да', u'yes', u'1', u'true') payload[u'convert_types'] = _combo_code(u'ConvertCombo', convert_choices, u'in_memory') try: payload[u'chunk_rows'] = int(_combo_code(u'ChunkCombo', chunk_choices, unicode(SOURCE_EXTRA_DEFAULT_CHUNK_ROWS))) except (TypeError, ValueError): payload[u'chunk_rows'] = int(SOURCE_EXTRA_DEFAULT_CHUNK_ROWS) return encode_source_extra_book_json(payload, prepare_active=prep_val) def _show_source_extra_csv_param_dialog(parent_dialog=None, initial_text=u'', doc=None, source_path=u'', target_col=1): """ Субвизард «Доп_Параметры_Источника» для CSV. Разделитель, кодировка, «переносить всем листом». Возвращает JSON или None. """ try: from libre_macros_csv_source_lib import ( DEFAULT_CSV_ENCODING, encode_source_extra_json, normalize_csv_delimiter, normalize_csv_encoding, parse_source_extra_dict, source_extra_copy_whole_sheet, source_extra_delimiter, source_extra_encoding, ) except Exception: DEFAULT_CSV_ENCODING = u'windows-1251' def parse_source_extra_dict(raw): return None def source_extra_delimiter(extra): return u';' def source_extra_encoding(extra): return DEFAULT_CSV_ENCODING def source_extra_copy_whole_sheet(extra): return False def encode_source_extra_json(extra): import json try: return unicode(json.dumps(extra or {}, ensure_ascii=False)) except Exception: return u'' def normalize_csv_delimiter(raw): return unicode(raw or u';') or u';' def normalize_csv_encoding(raw): return unicode(raw or DEFAULT_CSV_ENCODING) or DEFAULT_CSV_ENCODING init = unicode(initial_text or u'').strip() extra = parse_source_extra_dict(init) or {} cur_delim = source_extra_delimiter(extra) if cur_delim == u'\t': cur_code = u'\\t' else: cur_code = cur_delim cur_enc = source_extra_encoding(extra) cur_whole = bool(source_extra_copy_whole_sheet(extra)) is_csv = _source_extra_is_csv_path(source_path) col_letter = _col_letter(target_col) path_note = unicode(source_path or u'').strip() if path_note == u'': path_note = u'(путь источника не задан в «Файлы-Источники»)' elif len(path_note) > 70: path_note = u'…' + path_note[-67:] ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) dm = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialogModel', ctx) dw = 480 dh = _inner_dialog_height(310) m = 10 dm.PositionX = 150 dm.PositionY = 100 dm.Width = dw dm.Height = dh dm.Title = u'Доп. параметры источника (%s)' % col_letter _inner_dialog_set_sizeable(dm) y = m hint = ( u'Столбец %s. Источник:\n%s\n' % (col_letter, path_note) ) if is_csv: hint = hint + u'Файл CSV — разделитель, кодировка и способ копирования листа.' else: hint = hint + u'Сейчас настройки нужны для .csv; для других форматов можно оставить значения по умолчанию.' _wizard_dlg_add_fixed(dm, 'HintLbl', hint, m, y, dw - m * 2, 48, multiline=True) y += 52 _wizard_dlg_add_fixed(dm, 'DelimLbl', u'Разделитель полей CSV:', m, y, dw - m * 2, 12) y += 14 _wizard_dlg_add_combo(dm, 'DelimCombo', m, y, dw - m * 2, 16) y += 22 _wizard_dlg_add_fixed(dm, 'EncLbl', u'Кодировка файла:', m, y, dw - m * 2, 12) y += 14 _wizard_dlg_add_combo(dm, 'EncCombo', m, y, dw - m * 2, 16) y += 22 _wizard_dlg_add_fixed( dm, 'WholeLbl', u'При копировании переносить всем листом:', m, y, dw - m * 2, 12, ) y += 14 _wizard_dlg_add_combo(dm, 'WholeCombo', m, y, dw - m * 2, 16) _inner_dialog_add_footer(dm, m, dh) dlg = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialog', ctx) dlg.setModel(dm) _nested_subdialog_create_peer( dlg, toolkit, doc, parent_dialog, center_in_main_wizard=bool(parent_dialog) ) combo = dlg.getControl('DelimCombo') enc_combo = dlg.getControl('EncCombo') whole_combo = dlg.getControl('WholeCombo') labels = [] sel_idx = 0 di = 0 delim_choices = getattr(_pw_cfg, '_SOURCE_EXTRA_DELIM_CHOICES', None) or ( (u';', u'Точка с запятой (;)'), (u',', u'Запятая (,)'), (u'\\t', u'Табуляция'), (u'|', u'Вертикальная черта (|)'), ) while di < len(delim_choices): code, label = delim_choices[di] labels.append(label) if code == cur_code or (code == u';' and cur_delim == u';'): sel_idx = di di = di + 1 try: _set_combo_items(combo, labels, labels[sel_idx] if labels else u'') except Exception: pass try: if combo is not None: combo.setText(labels[sel_idx] if labels else u'') except Exception: pass enc_choices = getattr(_pw_cfg, '_SOURCE_EXTRA_ENCODING_CHOICES', None) or ( (u'windows-1251', u'Windows-1251 (кириллица, по умолчанию)'), (u'utf-8', u'UTF-8'), (u'utf-8-sig', u'UTF-8 с BOM'), ) enc_labels = [] enc_sel = 0 ei = 0 while ei < len(enc_choices): code, label = enc_choices[ei] enc_labels.append(label) if normalize_csv_encoding(code) == cur_enc or code == cur_enc: enc_sel = ei ei = ei + 1 try: _set_combo_items(enc_combo, enc_labels, enc_labels[enc_sel] if enc_labels else u'') except Exception: pass try: if enc_combo is not None: enc_combo.setText(enc_labels[enc_sel] if enc_labels else u'') except Exception: pass whole_choices = getattr(_pw_cfg, '_SOURCE_EXTRA_WHOLE_SHEET_CHOICES', None) or ( (u'нет', u'Нет — матрица в памяти → setDataArray (по умолчанию)'), (u'да', u'Да — открыть CSV в Calc и скопировать лист целиком'), ) whole_labels = [] wi = 0 while wi < len(whole_choices): whole_labels.append(whole_choices[wi][1]) wi = wi + 1 whole_sel = 1 if cur_whole else 0 try: _set_combo_items(whole_combo, whole_labels, whole_labels[whole_sel] if whole_labels else u'') except Exception: pass try: if whole_combo is not None: whole_combo.setText(whole_labels[whole_sel] if whole_labels else u'') except Exception: pass if dlg.execute() != 1: return None chosen = u'' try: chosen = unicode(combo.getText() or u'').strip() except Exception: chosen = u'' delim_code = u';' di = 0 while di < len(delim_choices): code, label = delim_choices[di] if chosen == label or chosen == code: delim_code = code break di = di + 1 else: if chosen != u'': delim_code = chosen delim_val = normalize_csv_delimiter(delim_code) enc_chosen = u'' try: enc_chosen = unicode(enc_combo.getText() or u'').strip() except Exception: enc_chosen = u'' enc_code = DEFAULT_CSV_ENCODING ei = 0 while ei < len(enc_choices): code, label = enc_choices[ei] if enc_chosen == label or enc_chosen == code: enc_code = code break ei = ei + 1 else: if enc_chosen != u'': enc_code = enc_chosen enc_val = normalize_csv_encoding(enc_code) whole_chosen = u'' try: whole_chosen = unicode(whole_combo.getText() or u'').strip() except Exception: whole_chosen = u'' whole_val = False wi = 0 while wi < len(whole_choices): code, label = whole_choices[wi] if whole_chosen == label or whole_chosen == code: whole_val = (code == u'да' or code == u'true' or code == u'1') break wi = wi + 1 else: low = whole_chosen.lower() if low in (u'да', u'da', u'true', u'1', u'yes'): whole_val = True return encode_source_extra_json({ u'delimiter': delim_val if delim_val != u'\t' else u'\\t', u'encoding': enc_val, u'copy_whole_sheet': whole_val, }) def show_cell_to_column_param_dialog(parent_dialog=None, initial_text=u'', doc=None): return _show_sheet_block_form_dialog(parent_dialog, u'Ячейка_В_Столбец', initial_text, doc=doc) def show_add_to_variables_map_param_dialog(parent_dialog=None, initial_text=u'', doc=None): return _show_sheet_block_form_dialog(parent_dialog, u'Добавить_в_карту_переменных', initial_text, doc=doc) def show_sheets_name_filter_dialog(parent_dialog=None, initial_text=u'', doc=None, title=u'Фильтр листов (C)', hint=None): """ Подвизард колонки C: JSON {"v":1,"filter":"lambda name: …"}. """ import json try: from libre_macros_sheet_filter_lib import ( SHEET_FILTER_EXAMPLE_LAST_3_DAYS, parse_sheet_filter_code, ) except Exception: SHEET_FILTER_EXAMPLE_LAST_3_DAYS = ( u'lambda name: sheet_age_days(name) is not None and sheet_age_days(name) <= 3' ) def parse_sheet_filter_code(raw): return unicode(raw or u'').strip() if hint is None: hint = ( u'Опциональная лямбда: аргумент name — имя листа, уже отобранного по шаблонам в B.\n' u'True — включить лист в обработку; False — пропустить.\n' u'Доступны: sheet_age_days(name), sheet_name_date(name), datetime, re.\n' u'Код проходит санацию (libre_macros_sanitize_lib).' ) init = unicode(initial_text or u'').strip() filt = parse_sheet_filter_code(init) ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) dm = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialogModel', ctx) dw = 560 dh = _inner_dialog_height(340) m = 10 dm.PositionX = 140 dm.PositionY = 90 dm.Width = dw dm.Height = dh dm.Title = title _inner_dialog_set_sizeable(dm) y = m _wizard_dlg_add_fixed(dm, 'HintLbl', hint, m, y, dw - m * 2, 52, multiline=True) y += 56 _wizard_dlg_add_fixed(dm, 'FiltLbl', u'filter (lambda name: …):', m, y, dw - m * 2, 12) y += 14 footer_top = dh - m - _pw_cfg._INNER_FOOTER_RESERVE edit_h = max(80, footer_top - y - 28) _wizard_dlg_add_edit(dm, 'FilterEd', m, y, dw - m * 2, edit_h, multiline=True) y = footer_top - 22 _wizard_dlg_add_button(dm, 'ExampleBtn', u'Пример ≤3 дня', m, y, 120, 16) _inner_dialog_add_footer(dm, m, dh) dlg = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialog', ctx) dlg.setModel(dm) _nested_subdialog_create_peer( dlg, toolkit, doc, parent_dialog, center_in_main_wizard=bool(parent_dialog) ) filt_ed = dlg.getControl('FilterEd') try: filt_ed.setText(filt) except Exception: pass class _H(unohelper.Base, XActionListener): def disposing(self, ev): pass def actionPerformed(self, ev): try: cmd = unicode(ev.ActionCommand) except Exception: cmd = u'' if cmd == u'ExampleBtn': try: filt_ed.setText(SHEET_FILTER_EXAMPLE_LAST_3_DAYS) except Exception: pass h = _H() try: btn = dlg.getControl('ExampleBtn') btn.addActionListener(h) btn.setActionCommand(u'ExampleBtn') except Exception: pass if dlg.execute() != 1: return None try: code = unicode(filt_ed.getText() or u'').strip() except Exception: code = u'' if code == u'': return u'' # проверка санации при OK try: from libre_macros_sheet_filter_lib import compile_sheet_name_filter compile_sheet_name_filter(code) except Exception as err: try: _show_message(u'Фильтр отклонён санацией/синтаксисом:\n%s' % err, doc=doc) except Exception: pass return None return unicode(json.dumps({'v': 1, 'filter': code}, ensure_ascii=False)) def show_sheets_undelete_filter_dialog(parent_dialog=None, initial_text=u'', doc=None): """Подвизард C для «Листы_не_удалять».""" return show_sheets_name_filter_dialog( parent_dialog, initial_text, doc=doc, title=u'Листы_не_удалять — фильтр (C)', hint=( u'Опциональная лямбда: аргумент name — имя листа, уже отобранного по шаблонам в B.\n' u'True — сохранить лист; False — можно удалить при предварительной очистке.\n' u'Доступны: sheet_age_days(name), sheet_name_date(name), datetime, re.\n' u'Код проходит санацию (libre_macros_sanitize_lib).' ), ) def show_sheets_force_delete_filter_dialog(parent_dialog=None, initial_text=u'', doc=None): """Подвизард C для «Листы_удалить».""" return show_sheets_name_filter_dialog( parent_dialog, initial_text, doc=doc, title=u'Листы_удалить — фильтр (C)', hint=( u'Опциональная лямбда: аргумент name — имя листа, уже отобранного по шаблонам в B.\n' u'True — удалить лист; False — пропустить.\n' u'Не удаляются только Параметры_Объединения* и листы-источники этой книги.\n' u'Доступны: sheet_age_days(name), sheet_name_date(name), datetime, re.\n' u'Код проходит санацию (libre_macros_sanitize_lib).' ), ) def show_sheets_include_filter_dialog(parent_dialog=None, initial_text=u'', doc=None): """Подвизард C для «Листы» (включение в копирование/перенос/вовлечение).""" return show_sheets_name_filter_dialog( parent_dialog, initial_text, doc=doc, title=u'Листы — фильтр (C)', hint=( u'Опциональная лямбда: аргумент name — имя листа, уже отобранного по шаблонам в B.\n' u'True — включить лист в копирование/перенос/объединение/вовлечение; False — пропустить.\n' u'Доступны: sheet_age_days(name), sheet_name_date(name), datetime, re.\n' u'Код проходит санацию (libre_macros_sanitize_lib).' ), ) def _show_sheet_list_panel_dialog(parent_dialog, fn_key, initial_text, doc=None): """ SheetListPanel (§3.4.3): «Все листы» + список имён; опционально convert_existing. """ from libre_macros_param_codec import param_decode, param_encode fn_key = _inner_codec_dialog_effective_key(fn_key) init = unicode(initial_text or u'').strip() try: blocks = param_decode(fn_key, init) except Exception: blocks = [] block = blocks[0] if blocks else {'v': 1, 'fn': fn_key} all_sheets = not block.get('sheets') sheet_names = list(block.get('sheets') or []) convert_default = bool(block.get('convert_existing')) allow_convert = _sheet_list_panel_allows_convert(fn_key) allow_special = _sheet_list_panel_allows_special(fn_key) allow_filter = allow_special filter_default = unicode(block.get('filter') or u'').strip() ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) dm = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialogModel', ctx) dw = 560 dh = _inner_dialog_height(460 if allow_filter else (400 if allow_special else 360)) m = 10 dm.PositionX = 140 dm.PositionY = 100 dm.Width = dw dm.Height = dh dm.Title = fn_key _inner_dialog_set_sizeable(dm) y = m hint = u'Пустой список или «Все листы» — шаг на каждом листе цепочки (range) или на всех (final). Имя листа: «+ Лист» или сразу OK.' if allow_convert: hint += u' «Конвертировать» — текст в число перед форматом (§6.1.1).' _wizard_dlg_add_fixed(dm, 'HintLbl', hint, m, y, dw - m * 2, 28, multiline=True) y += 32 _wizard_dlg_add_checkbox(dm, 'AllSheetsChk', u'Все листы', m, y, 160, 16) y += 22 controls_below = 8 + 22 + 22 if allow_special: controls_below += 14 + 18 + 16 if allow_convert: controls_below += 22 if allow_filter: controls_below += 14 + 56 + 8 col_gap = 10 left_w = 240 right_w = dw - m * 2 - left_w - col_gap _wizard_dlg_add_fixed(dm, 'SheetsLbl', u'Листы:', m, y, left_w, 14) _wizard_dlg_add_fixed(dm, 'ParamsLbl', u'Параметры:', m + left_w + col_gap, y, right_w, 14) y += 16 list_top = y footer_top = dh - m - _pw_cfg._INNER_FOOTER_RESERVE list_h = footer_top - list_top - controls_below - 8 list_h = max(60, min(list_h, 200)) _wizard_dlg_add_listbox(dm, 'SheetsLB', m, list_top, left_w, list_h, multiselect=False) _wizard_dlg_add_fixed(dm, 'NoParamsLbl', u'Для этого действия дополнительных параметров не предусмотрено.', m + left_w + col_gap, list_top, right_w, list_h, multiline=True) y2 = list_top + list_h + 8 _wizard_dlg_add_combo(dm, 'SheetNameEd', m, y2, left_w, 18) y_btn = y2 + 22 _wizard_dlg_add_button(dm, 'AddSheetBtn', u'+ Лист', m, y_btn - 2, 80, 16) _wizard_dlg_add_button(dm, 'DelSheetBtn', u'Удалить', m + 88, y_btn - 2, 70, 16) if allow_special: y_spec = y_btn + 22 _wizard_dlg_add_fixed(dm, 'SpecLbl', u'Спец. (добавить в список):', m, y_spec, dw - m * 2, 14) y_spec += 18 spec_x = m for idx, token in enumerate(_pw_cfg._SHEET_LIST_SPECIAL_TOKENS): btn_name = 'SpecBtn%d' % idx _wizard_dlg_add_button(dm, btn_name, token, spec_x, y_spec, 118, 16) spec_x += 124 if allow_convert: y3 = y_btn + 22 if allow_special: y3 = y_spec + 16 + 8 _wizard_dlg_add_checkbox(dm, 'ConvertChk', u'Конвертировать текст в число', m, y3, dw - m * 2, 16) if allow_filter: yf = y_btn + 22 if allow_special: yf = y_spec + 16 + 8 if allow_convert: yf = y3 + 22 _wizard_dlg_add_fixed(dm, 'FilterLbl', u'filter (lambda name: …), санация:', m, yf, dw - m * 2, 12) _wizard_dlg_add_edit(dm, 'FilterEd', m, yf + 14, dw - m * 2, 48, multiline=True) _inner_dialog_add_footer(dm, m, dh) dlg = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialog', ctx) dlg.setModel(dm) _nested_subdialog_create_peer( dlg, toolkit, doc, parent_dialog, center_in_main_wizard=bool(parent_dialog) ) all_chk = dlg.getControl('AllSheetsChk') sheets_lb = dlg.getControl('SheetsLB') sheet_ed = dlg.getControl('SheetNameEd') try: dlg.getControl('JsonChk').setState(1) except Exception: pass try: all_chk.setState(1 if all_sheets else 0) except Exception: pass if allow_convert: try: dlg.getControl('ConvertChk').setState(1 if convert_default else 0) except Exception: pass if allow_filter: try: dlg.getControl('FilterEd').setText(filter_default) except Exception: pass doc_sheet_names = _pivot_list_doc_sheet_names(doc) def _refresh_sheets_list(selected_index=0): try: sheets_lb.Model.StringItemList = tuple(sheet_names) except Exception: pass try: if len(sheet_names) > 0: sheets_lb.selectItemPos(max(0, min(selected_index, len(sheet_names) - 1)), True) except Exception: pass def _sync_sheet_controls_enabled(): all_on = False try: all_on = bool(all_chk.getState()) except Exception: pass for ctl_name in ('SheetsLB', 'SheetNameEd', 'AddSheetBtn', 'DelSheetBtn'): try: dlg.getControl(ctl_name).Model.Enabled = not all_on except Exception: pass def _sheet_index_selected(): try: pos = sheets_lb.getSelectedItemPos() return int(pos) if pos is not None else -1 except Exception: return -1 class _SheetListHandler(unohelper.Base, XActionListener, XItemListener): def disposing(self, ev): pass def itemStateChanged(self, ev): try: src = ev.Source except Exception: return if src == all_chk: _sync_sheet_controls_enabled() def actionPerformed(self, ev): cmd = u'' try: cmd = unicode(ev.ActionCommand) except Exception: pass if cmd == u'AddSheetBtn': try: name = unicode(sheet_ed.getText() or u'').strip() except Exception: name = u'' if name == u'': return if name not in sheet_names: sheet_names.append(name) _refresh_sheets_list(selected_index=len(sheet_names) - 1) try: sheet_ed.setText(u'') except Exception: pass return if cmd == u'DelSheetBtn': idx = _sheet_index_selected() if 0 <= idx < len(sheet_names): del sheet_names[idx] _refresh_sheets_list(selected_index=max(0, idx - 1)) return if cmd.startswith(u'SpecBtn'): try: idx = int(cmd.replace(u'SpecBtn', u'')) except Exception: idx = -1 if 0 <= idx < len(_pw_cfg._SHEET_LIST_SPECIAL_TOKENS): token = _pw_cfg._SHEET_LIST_SPECIAL_TOKENS[idx] if token not in sheet_names: sheet_names.append(token) _refresh_sheets_list(selected_index=len(sheet_names) - 1) return h = _SheetListHandler() try: all_chk.addItemListener(h) except Exception: pass for btn in ('AddSheetBtn', 'DelSheetBtn'): try: ctl = dlg.getControl(btn) ctl.addActionListener(h) ctl.setActionCommand(btn) except Exception: pass if allow_special: for idx in range(len(_pw_cfg._SHEET_LIST_SPECIAL_TOKENS)): btn_name = 'SpecBtn%d' % idx try: ctl = dlg.getControl(btn_name) ctl.addActionListener(h) ctl.setActionCommand(btn_name) except Exception: pass if len(doc_sheet_names) > 0: try: _set_combo_items(sheet_ed, doc_sheet_names, select_text=u'') except Exception: pass _refresh_sheets_list(0) _sync_sheet_controls_enabled() if dlg.execute() != 1: return None use_json = True try: use_json = bool(dlg.getControl('JsonChk').getState()) except Exception: pass all_on = False try: all_on = bool(all_chk.getState()) except Exception: pass def _flush_pending_sheet_from_edit(): """Текст в поле «Лист» без «+ Лист» — учесть при OK (частый сценарий).""" if all_on: return try: name = unicode(sheet_ed.getText() or u'').strip() except Exception: name = u'' if name == u'': return if name not in sheet_names: sheet_names.append(name) try: sheet_ed.setText(u'') except Exception: pass _flush_pending_sheet_from_edit() out_block = {'v': 1, 'fn': fn_key} if not all_on and len(sheet_names) > 0: out_block['sheets'] = list(sheet_names) if allow_convert: try: if dlg.getControl('ConvertChk').getState(): out_block['convert_existing'] = True except Exception: pass if allow_filter: try: fcode = unicode(dlg.getControl('FilterEd').getText() or u'').strip() except Exception: fcode = u'' if fcode: try: from libre_macros_sheet_filter_lib import compile_sheet_name_filter compile_sheet_name_filter(fcode) out_block['filter'] = fcode except Exception as err: try: _show_message(u'Фильтр отклонён:\n%s' % err, doc=doc) except Exception: pass return None return param_encode(fn_key, [out_block]) def _show_sheet_block_grid_v1_dialog(parent_dialog, fn_key, initial_text, doc=None): """ SheetBlockGrid v1 (минимально жизнеспособный): - грид: Лист | Параметры (в текстовом виде) - кнопки: + Блок / Удалить / Правка… - чекбокс «Сохранять как JSON» Хранение: blocks через libre_macros_param_codec. """ from libre_macros_param_codec import param_decode, param_encode title = u'Параметры' hint = u'' if unicode(fn_key).casefold() == u'сортировка'.casefold(): title = u'Сортировка' hint = u'Лист + список ключей. «Правка…» открывает текст ключей для выбранной строки.' elif unicode(fn_key).casefold() == u'раскрасить_блоки'.casefold(): title = u'Раскрасить блоки' hint = u'Лист + ключ->цвета. «Правка…» открывает текст правой части для выбранной строки.' blocks = [] try: blocks = param_decode(fn_key, unicode(initial_text or u'').strip()) except Exception: blocks = [] rows = [] for b in blocks: sheet = unicode(b.get('sheet') or u'').strip() t = param_encode(fn_key, [b]) if t.startswith('['): import json as _json try: one = _json.loads(t) if isinstance(one, list) and len(one) == 1: right = _json.dumps(one[0], ensure_ascii=False, separators=(',', ':')) else: right = t except (ValueError, TypeError): right = t else: right = t rows.append([sheet, right]) ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) dm = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialogModel', ctx) dw = 560 dh = _inner_dialog_height(_pw_cfg._INNER_GRID_DH) m = 10 dm.PositionX = 120 dm.PositionY = 80 dm.Width = dw dm.Height = dh dm.Title = title try: dm.Sizeable = True except Exception: pass y = m if hint: _wizard_dlg_add_fixed(dm, 'HintLbl', hint, m, y, dw - m * 2, 32, multiline=True) y += 36 grid_model = dm.createInstance('com.sun.star.awt.grid.UnoControlGridModel') grid_model.Name = 'BlocksGrid' grid_model.PositionX = m grid_model.PositionY = y grid_model.Width = dw - m * 2 grid_model.Height = dh - y - _pw_cfg._INNER_FOOTER_RESERVE - 36 try: grid_model.ShowColumnHeader = True grid_model.ShowRowHeader = False grid_model.UseGridLines = True except Exception: pass col_model = grid_model.ColumnModel _grid_create_column(col_model, u'Лист', width=140) _grid_create_column(col_model, u'Параметры', width=360) data_model = grid_model.GridDataModel try: for rid in list(getattr(data_model, 'RowIds', ()) or ()): try: data_model.removeRow(rid) except Exception: pass except Exception: pass i = 0 for r in rows: try: data_model.addRow(str(i), tuple(r)) except Exception: pass i += 1 dm.insertByName('BlocksGrid', grid_model) y_btn_row = y + grid_model.Height + 8 _wizard_dlg_add_button(dm, 'AddRowBtn', u'+ Блок', m, y_btn_row, 80, 16) _wizard_dlg_add_button(dm, 'DelRowBtn', u'Удалить', m + 90, y_btn_row, 80, 16) _wizard_dlg_add_button(dm, 'EditRowBtn', u'Правка…', m + 180, y_btn_row, 80, 16) _inner_dialog_add_footer(dm, m, dh) dlg = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialog', ctx) dlg.setModel(dm) _nested_subdialog_create_peer( dlg, toolkit, doc, parent_dialog, center_in_main_wizard=bool(parent_dialog) ) grid = dlg.getControl('BlocksGrid') try: dlg.getControl('JsonChk').setState(1) except Exception: pass class _GridHandler(unohelper.Base, XActionListener): def __init__(self): self.result = None def disposing(self, ev): pass def _selected_row_index(self): try: sel = grid.getSelectedRows() if sel and len(sel) > 0: return int(sel[0]) except Exception: pass return -1 def actionPerformed(self, ev): name = u'' try: name = ev.ActionCommand except Exception: pass if name == 'AddRowBtn': try: rid = str(data_model.getRowCount()) except Exception: rid = str(len(rows)) try: data_model.addRow(rid, (u'', u'')) except Exception: pass return if name == 'DelRowBtn': idx = self._selected_row_index() if idx >= 0: try: rid = data_model.getRowHeading(idx) data_model.removeRow(rid) except Exception: try: data_model.removeRow(str(idx)) except Exception: pass return if name == 'EditRowBtn': idx = self._selected_row_index() if idx < 0: return try: row = data_model.getRowData(idx) sheet = unicode(row[0] or u'').strip() right = unicode(row[1] or u'').strip() except Exception: return edited = _show_inner_codec_param_dialog(parent_dialog, fn_key, sheet + u' | ' + right if sheet else right, doc=doc) if edited: if u'|' in edited: left, rr = edited.split(u'|', 1) sheet2 = unicode(left).strip() right2 = unicode(rr).strip() else: sheet2 = u'' right2 = unicode(edited).strip() try: rid = data_model.getRowHeading(idx) data_model.updateRowData(rid, (sheet2, right2)) except Exception: pass return handler = _GridHandler() for btn in ('AddRowBtn', 'DelRowBtn', 'EditRowBtn'): try: dlg.getControl(btn).addActionListener(handler) dlg.getControl(btn).setActionCommand(btn) except Exception: pass if dlg.execute() != 1: return None use_json = True try: use_json = bool(dlg.getControl('JsonChk').getState()) except Exception: pass out_blocks = [] try: n = data_model.getRowCount() except Exception: n = 0 ri = 0 while ri < n: try: row = data_model.getRowData(ri) sheet = unicode(row[0] or u'').strip() right = unicode(row[1] or u'').strip() except Exception: sheet = u'' right = u'' if right != u'': text = sheet + u' | ' + right if sheet else right try: bs = param_decode(fn_key, text) if bs: b = dict(bs[0]) if sheet: b['sheet'] = sheet else: b.pop('sheet', None) out_blocks.append(b) except Exception: pass ri += 1 return param_encode(fn_key, out_blocks) def _colorize_block_label(block): sheet = unicode(block.get('sheet') or u'').strip() keys = block.get('key_columns') or [] colors = block.get('colors') or [] parts = [] if keys: parts.append(u', '.join((unicode(k) for k in keys[:2]))) if len(keys) > 2: parts[-1] += u'…' if colors: parts.append(u'%'.join((unicode(c) for c in colors[:2]))) tail = u' → '.join(parts) if parts else u'' return _inner_block_list_label(sheet, tail) def _colorize_sort_to_combo(sort_val): if sort_val == 'asc': return u'+' if sort_val == 'desc': return u'-' return u'нет' def _colorize_combo_to_sort(combo_val): v = unicode(combo_val or u'').strip().casefold() if v in (u'+', u'asc', u'возр'): return 'asc' if v in (u'-', u'desc', u'убыв'): return 'desc' return None def _show_colorize_blocks_param_dialog(parent_dialog, initial_text, doc=None): """ Специфичный диалог «раскрасить_блоки» (§3.4.2): слева — блоки по листам; справа — ключ, цвета, сортировка, граница. """ from libre_macros_param_codec import param_decode, param_encode init = unicode(initial_text or u'').strip() _pw_dbg(u'=== colorize dialog open ===') _pw_dbg_kv(u'initial_text_raw', unicode(initial_text)) _pw_dbg_kv(u'initial_text_stripped', init) try: blocks = param_decode(u'раскрасить_блоки', init) _pw_dbg_kv(u'param_decode_blocks_len', len(blocks or [])) if _pw_cfg.PARAM_WIZARD_DEBUG: try: for i, b in enumerate(blocks or []): _pw_dbg(u'decoded[%d] keys=%s' % (i, list(b.keys()) if isinstance(b, dict) else type(b))) _pw_dbg(u'decoded[%d] repr=%s' % (i, repr(b))) except Exception as err: _pw_dbg(u'dump decoded blocks failed: %s' % unicode(err)) except Exception: blocks = [] _pw_dbg(u'param_decode failed -> blocks=[]') norm_blocks = [] for b in blocks or []: if not isinstance(b, dict): continue norm_blocks.append(dict(b)) if _pw_cfg.PARAM_WIZARD_DEBUG: try: for i, b in enumerate(norm_blocks): _pw_dbg(u'norm[%d] sheet=%s keys=%s colors=%s' % (i, b.get('sheet'), b.get('key_columns'), b.get('colors'))) c = b.get('colors') _pw_dbg(u'norm[%d] colors_type=%s' % (i, type(c))) except Exception as err: _pw_dbg(u'dump norm_blocks failed: %s' % unicode(err)) if len(norm_blocks) == 0: norm_blocks = [{'v': 1, 'fn': u'раскрасить_блоки'}] _pw_dbg(u'norm_blocks empty -> create default block') ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) dm = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialogModel', ctx) dw = 580 m = 10 col_gap = 10 left_w = 190 right_w = dw - m * 2 - left_w - col_gap lbl_w = _pw_cfg._INNER_LBL_W lbl_gap = _pw_cfg._INNER_LBL_GAP row_h = _pw_cfg._INNER_FIELD_ROW_H list_h = 88 hint_h = 32 rx = m + left_w + col_gap ctl_x = _wizard_dlg_ctl_x(rx, lbl_w, lbl_gap) ctl_w = _wizard_dlg_ctl_w(right_w, lbl_w, lbl_gap) pick_btn_w = 22 pick_gap = 4 key_cb_w = max(80, ctl_w - 2 * pick_btn_w - 2 * pick_gap) dm.PositionX = 120 dm.PositionY = 80 dm.Width = dw dm.Height = 400 dm.Title = u'Раскрасить блоки' _inner_dialog_set_sizeable(dm) y = m _wizard_dlg_add_fixed(dm, 'HintLbl', u'Блоки по листам: столбцы и цвета — выбор из списка с накоплением (запятая / %), × и ××; сортировка, граница блоков.', m, y, dw - m * 2, hint_h, multiline=True) y += hint_h + 2 _wizard_dlg_add_fixed(dm, 'BlocksLbl', u'Блоки:', m, y, left_w, 14) _wizard_dlg_add_fixed(dm, 'ParamsLbl', u'Параметры блока:', rx, y, right_w, 14) y += 14 _wizard_dlg_add_listbox(dm, 'BlocksLB', m, y, left_w, list_h, multiselect=False) fy = y _wizard_dlg_add_fixed(dm, 'KeyColsLbl', u'Столбцы ключа:', rx, fy, lbl_w, 14) _wizard_dlg_add_combo(dm, 'KeyColsCb', ctl_x, fy - 2, key_cb_w, 18) _wizard_dlg_add_button(dm, 'KeyColsDelLastBtn', u'×', ctl_x + key_cb_w + pick_gap, fy - 2, pick_btn_w, 16) _wizard_dlg_add_button(dm, 'KeyColsDelAllBtn', u'××', ctl_x + key_cb_w + pick_gap + pick_btn_w + pick_gap, fy - 2, pick_btn_w, 16) fy += row_h _wizard_dlg_add_fixed(dm, 'ColorsLbl', u'Цвета:', rx, fy, lbl_w, 14) colors_cb_w = max(80, ctl_w - 2 * pick_btn_w - 2 * pick_gap) _wizard_dlg_add_combo(dm, 'ColorsCb', ctl_x, fy - 2, colors_cb_w, 18) _wizard_dlg_add_button(dm, 'ColorsDelLastBtn', u'×', ctl_x + colors_cb_w + pick_gap, fy - 2, pick_btn_w, 16) _wizard_dlg_add_button(dm, 'ColorsDelAllBtn', u'××', ctl_x + colors_cb_w + pick_gap + pick_btn_w + pick_gap, fy - 2, pick_btn_w, 16) fy += row_h _wizard_dlg_add_fixed(dm, 'SortLbl', u'Сортировка:', rx, fy, lbl_w, 14) _wizard_dlg_add_combo(dm, 'SortCb', ctl_x, fy - 2, min(120, ctl_w), 18) fy += row_h _wizard_dlg_add_checkbox(dm, 'OutlineChk', u'Выделить блоки границей', rx, fy, right_w, 16) fy += 18 _wizard_dlg_add_fixed(dm, 'OutlineColLbl', u'Цвет границы:', rx, fy, lbl_w, 14) _wizard_dlg_add_combo(dm, 'OutlineColCb', ctl_x, fy - 2, ctl_w, 18) y2 = y + list_h + 6 _wizard_dlg_add_fixed(dm, 'SheetLbl', u'Лист (пусто = все листы):', m, y2, left_w, 14) _wizard_dlg_add_combo(dm, 'SheetEd', m, y2 + 14, left_w, 18) _wizard_dlg_add_button(dm, 'AddBlockBtn', u'+ Блок', m, y2 + 36, 80, 16) _wizard_dlg_add_button(dm, 'DelBlockBtn', u'Удалить', m + 90, y2 + 36, 80, 16) left_block_bottom = y2 + 36 + 16 btn_y = max(fy + 6, left_block_bottom + 4) btn_h = _pw_cfg._INNER_BTN_H _wizard_dlg_add_button(dm, 'OkButton', u'OK', m, btn_y, _pw_cfg._INNER_BTN_W, btn_h) _wizard_dlg_add_button(dm, 'CancelButton', u'Отмена', m + _pw_cfg._INNER_BTN_W + _pw_cfg._INNER_BTN_GAP, btn_y, _pw_cfg._INNER_BTN_W, btn_h) try: dm.getByName('OkButton').DefaultButton = True except Exception: pass try: dm.getByName('OkButton').PushButtonType = 1 except Exception: pass try: dm.getByName('CancelButton').PushButtonType = 2 except Exception: pass json_y = btn_y + btn_h + 4 _wizard_dlg_add_checkbox(dm, 'JsonChk', u'Сохранять как JSON', m, json_y, 200, _pw_cfg._INNER_CHK_H) dm.Height = json_y + _pw_cfg._INNER_CHK_H + m dlg = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialog', ctx) dlg.setModel(dm) _nested_subdialog_create_peer( dlg, toolkit, doc, parent_dialog, center_in_main_wizard=bool(parent_dialog) ) blocks_lb = dlg.getControl('BlocksLB') sheet_ed = dlg.getControl('SheetEd') key_cols_cb = dlg.getControl('KeyColsCb') colors_cb = dlg.getControl('ColorsCb') sort_cb = dlg.getControl('SortCb') outline_chk = dlg.getControl('OutlineChk') outline_col_cb = dlg.getControl('OutlineColCb') try: dlg.getControl('JsonChk').setState(1) except Exception: pass try: _set_combo_items(sort_cb, (u'нет', u'+', u'-'), u'нет') except Exception: pass color_choices = () try: color_choices = _pw_cfg._header_plus_height_color_choices() _set_combo_items(colors_cb, color_choices, select_text=u'') _set_combo_items(outline_col_cb, color_choices, select_text=u'') except Exception: pass form_block_index = [-1] sheet_field_guard = [False] form_loading = [False] col_text_prev = [u''] colors_text_prev = [u''] pick_guard = [False] def _use_json_mode(): try: return bool(dlg.getControl('JsonChk').getState()) except Exception: return True def _colors_token_to_text(val): """ Безопасно приводим элемент списка цветов к unicode-тексту. В LibreOffice/UNO иногда прилетают bytes (py2/py3), а json.dumps их не любит. """ if val is None: return u'' try: if isinstance(val, bytes): return val.decode('utf-8', 'ignore').strip() except Exception: pass try: return unicode(val).strip() except Exception: try: return str(val).strip() except Exception: return u'' def _colorize_color_sep(): return u',' if _use_json_mode() else u'%' def _colorize_color_seps(): s = _colorize_color_sep() if s == u'%': return (u'%', u',', u';') return (u',', u';', u'%') def _format_colors_for_field(colors): colors = colors or [] try: payload = [_colors_token_to_text(c) for c in (list(colors) if not isinstance(colors, (str, bytes)) else [colors])] payload = [c for c in payload if c] if len(payload) == 0: return u'' return _wizard_dlg_join_token_list(payload, sep=_colorize_color_sep()) except Exception: return u'' def _parse_colors_from_field(raw_text): raw = unicode(raw_text or u'').strip() if raw == '': return [] if _use_json_mode() and raw[:1] in (u'[', u'('): try: val = json.loads(raw) except Exception: try: val = ast.literal_eval(raw) except Exception: val = None if isinstance(val, (list, tuple)): out = [_colors_token_to_text(x) for x in val] return [x for x in out if x] if raw[:1] == u'[' and raw[-1:] == u']': inner = raw[1:-1].strip() if inner == '': return [] parts = [p.strip() for p in inner.split(u',') if p.strip()] cleaned = [] for p in parts: if p.startswith(u'"') and p.endswith(u'"') or (p.startswith(u"'") and p.endswith(u"'")): p = p[1:-1] p = p.strip() if p: cleaned.append(p) return cleaned one = _colors_token_to_text(raw) return [one] if one else [] return [_colors_token_to_text(p) for p in _wizard_dlg_split_token_list(raw, seps=_colorize_color_seps())] def _refresh_blocks(selected_index=0): form_loading[0] = True items = [_colorize_block_label(b) for b in norm_blocks] try: blocks_lb.Model.StringItemList = tuple(items) except Exception: pass try: if len(items) > 0: blocks_lb.selectItemPos(max(0, min(selected_index, len(items) - 1)), True) except Exception: pass form_loading[0] = False def _block_index_selected(): try: pos = blocks_lb.getSelectedItemPos() return int(pos) if pos is not None else -1 except Exception: return -1 def _load_block_to_form(block_index): form_loading[0] = True form_block_index[0] = block_index if block_index < 0 or block_index >= len(norm_blocks): _inner_dialog_set_sheet_field_text(sheet_ed, u'', sheet_field_guard) _refresh_column_combo_for_sheet(u'') try: _set_combo_edit_text(key_cols_cb, u'') _set_combo_edit_text(colors_cb, u'') except Exception: pass col_text_prev[0] = u'' colors_text_prev[0] = u'' try: _apply_combo_selection(sort_cb, (u'нет', u'+', u'-'), select_name=u'нет') _apply_combo_selection(outline_col_cb, color_choices, clear=True) except Exception: pass try: outline_chk.setState(0) except Exception: pass form_loading[0] = False return b = norm_blocks[block_index] _pw_dbg(u'_load_block_to_form idx=%s repr=%s' % (block_index, repr(b))) sheet = unicode(b.get('sheet') or u'').strip() _inner_dialog_set_sheet_field_text(sheet_ed, sheet, sheet_field_guard) _refresh_column_combo_for_sheet(sheet) keys = b.get('key_columns') or [] col_disp = u', '.join((unicode(k) for k in keys)) try: _set_combo_edit_text(key_cols_cb, col_disp) except Exception: pass col_text_prev[0] = col_disp colors = b.get('colors') or [] _pw_dbg(u'_load colors=%s type=%s' % (repr(colors), type(colors))) col_disp = _format_colors_for_field(colors) try: _set_combo_edit_text(colors_cb, col_disp) except Exception: pass colors_text_prev[0] = col_disp try: _apply_combo_selection(sort_cb, (u'нет', u'+', u'-'), select_name=_colorize_sort_to_combo(b.get('sort'))) except Exception: pass try: outline_chk.setState(1 if b.get('outline_blocks') else 0) except Exception: pass oc = b.get('outline_color') try: if oc in (None, u''): _apply_combo_selection(outline_col_cb, color_choices, clear=True) else: _apply_combo_selection(outline_col_cb, color_choices, select_name=unicode(oc)) except Exception: pass form_loading[0] = False def _apply_form_to_block(block_index): if block_index < 0 or block_index >= len(norm_blocks): return b = norm_blocks[block_index] _inner_dialog_set_block_sheet(b, _inner_dialog_sheet_text(sheet_ed)) raw_keys = _combo_edit_text(key_cols_cb) keys = [unicode(t).strip() for t in _vlookup_split_token_list(raw_keys) if unicode(t).strip()] b['key_columns'] = keys raw_colors = _combo_edit_text(colors_cb) _pw_dbg(u'_apply_form raw_colors_text=%s' % repr(raw_colors)) colors = _parse_colors_from_field(raw_colors) _pw_dbg(u'_apply_form parsed_colors=%s' % repr(colors)) b['colors'] = colors try: sort_sel = unicode(sort_cb.getText() or u'нет') except Exception: sort_sel = u'нет' b['sort'] = _colorize_combo_to_sort(sort_sel) try: b['outline_blocks'] = bool(outline_chk.getState()) except Exception: b['outline_blocks'] = False try: oc = _combo_edit_text(outline_col_cb) except Exception: oc = u'' b['outline_color'] = oc if oc != u'' else None b['v'] = 1 b['fn'] = u'раскрасить_блоки' def _combo_pick_items(control): try: return tuple(control.Model.StringItemList or ()) except Exception: return tuple() def _pick_append_token(control, acc_holder, token, seps=(u',', u';')): token = unicode(token or u'').strip() if token == u'' or _wizard_dlg_token_has_sep(token, seps): return False parts = _wizard_dlg_split_token_list(acc_holder[0], seps=seps) token_cf = token.casefold() for p in parts: if unicode(p).strip().casefold() == token_cf: pick_guard[0] = True try: _set_combo_edit_text(control, acc_holder[0]) finally: pick_guard[0] = False return False merged = _wizard_dlg_append_token_list(acc_holder[0], token, seps=seps) if merged == unicode(acc_holder[0] or u'').strip(): return False pick_guard[0] = True try: _set_combo_edit_text(control, merged) finally: pick_guard[0] = False acc_holder[0] = merged return True def _pick_handle_combo_text(control, acc_holder, seps=(u',', u';')): """textChanged: выбор из списка → накопление; иначе — ручной ввод.""" if pick_guard[0] or form_loading[0]: return cur = _combo_edit_text(control) acc = unicode(acc_holder[0] or u'').strip() if cur == acc: return for sep in seps: if sep in cur: acc_holder[0] = cur bi = form_block_index[0] if bi >= 0: _apply_form_to_block(bi) _inner_dialog_update_blocks_list_label(blocks_lb, bi, _colorize_block_label(norm_blocks[bi])) return items = _combo_pick_items(control) picked = _wizard_dlg_combo_single_item(cur, items) if picked: if _pick_append_token(control, acc_holder, picked, seps=seps): bi = form_block_index[0] if bi >= 0: _apply_form_to_block(bi) _inner_dialog_update_blocks_list_label(blocks_lb, bi, _colorize_block_label(norm_blocks[bi])) return acc_holder[0] = cur bi = form_block_index[0] if bi >= 0: _apply_form_to_block(bi) _inner_dialog_update_blocks_list_label(blocks_lb, bi, _colorize_block_label(norm_blocks[bi])) def _pick_handle_combo_select(control, acc_holder, ev, seps=(u',', u';')): if pick_guard[0] or form_loading[0]: return items = _combo_pick_items(control) token = _wizard_dlg_combo_dropdown_token(control, items, ev) if token == u'': try: token = _wizard_dlg_combo_dropdown_token(control, control.Model.StringItemList or (), ev) except Exception: token = u'' if token == u'' or _wizard_dlg_token_has_sep(token, seps): return if not _pick_append_token(control, acc_holder, token, seps=seps): return bi = form_block_index[0] if bi >= 0: _apply_form_to_block(bi) _inner_dialog_update_blocks_list_label(blocks_lb, bi, _colorize_block_label(norm_blocks[bi])) def _active_sheet_name(): try: if doc is None: return u'' ctrl = doc.getCurrentController() sh = ctrl.getActiveSheet() if ctrl is not None else None return unicode(getattr(sh, 'Name', u'') or u'').strip() except Exception: return u'' def _refresh_column_combo_for_sheet(sheet_name=None): name = unicode(sheet_name or u'').strip() or _active_sheet_name() cols = [] try: cols = _vlookup_list_sheet_columns(doc, name, u'1', u'1') except Exception: cols = [] base = [u''] + cols + [unicode(i) for i in range(1, 51)] base += [unichr(ord('A') + i) for i in range(0, 26)] seen = set() out = [] cur = _combo_edit_text(key_cols_cb) for item in base + ([cur] if cur else []): s = unicode(item or u'').strip() k = s.casefold() if k in seen: continue seen.add(k) out.append(s) try: key_cols_cb.Model.StringItemList = tuple(out) except Exception: pass _set_combo_edit_text(key_cols_cb, cur) col_text_prev[0] = cur def _key_cols_del_last(): merged = _wizard_dlg_remove_last_token(col_text_prev[0], seps=(u',', u';')) pick_guard[0] = True try: _set_combo_edit_text(key_cols_cb, merged) finally: pick_guard[0] = False col_text_prev[0] = merged bi = form_block_index[0] if bi >= 0: _apply_form_to_block(bi) _inner_dialog_update_blocks_list_label(blocks_lb, bi, _colorize_block_label(norm_blocks[bi])) def _key_cols_del_all(): pick_guard[0] = True try: _set_combo_edit_text(key_cols_cb, u'') finally: pick_guard[0] = False col_text_prev[0] = u'' bi = form_block_index[0] if bi >= 0: _apply_form_to_block(bi) _inner_dialog_update_blocks_list_label(blocks_lb, bi, _colorize_block_label(norm_blocks[bi])) def _colors_del_last(): merged = _wizard_dlg_remove_last_token(colors_text_prev[0], seps=_colorize_color_seps()) pick_guard[0] = True try: _set_combo_edit_text(colors_cb, merged) finally: pick_guard[0] = False colors_text_prev[0] = merged bi = form_block_index[0] if bi >= 0: _apply_form_to_block(bi) _inner_dialog_update_blocks_list_label(blocks_lb, bi, _colorize_block_label(norm_blocks[bi])) def _colors_del_all(): pick_guard[0] = True try: _set_combo_edit_text(colors_cb, u'') finally: pick_guard[0] = False colors_text_prev[0] = u'' bi = form_block_index[0] if bi >= 0: _apply_form_to_block(bi) _inner_dialog_update_blocks_list_label(blocks_lb, bi, _colorize_block_label(norm_blocks[bi])) def _sync_sheet_field_to_block(): if sheet_field_guard[0] or form_loading[0]: return bi = form_block_index[0] if bi < 0 or bi >= len(norm_blocks): return _inner_dialog_set_block_sheet(norm_blocks[bi], _inner_dialog_sheet_text(sheet_ed)) _refresh_column_combo_for_sheet(_inner_dialog_sheet_text(sheet_ed)) _inner_dialog_update_blocks_list_label(blocks_lb, bi, _colorize_block_label(norm_blocks[bi])) def _switch_colorize_block(block_index): prev = form_block_index[0] if prev >= 0 and prev != block_index: _apply_form_to_block(prev) _inner_dialog_update_blocks_list_label(blocks_lb, prev, _colorize_block_label(norm_blocks[prev])) _load_block_to_form(block_index) def _add_colorize_block_empty(): prev = form_block_index[0] if prev >= 0: _apply_form_to_block(prev) _inner_dialog_update_blocks_list_label(blocks_lb, prev, _colorize_block_label(norm_blocks[prev])) norm_blocks.append({'v': 1, 'fn': u'раскрасить_блоки', 'key_columns': [], 'colors': [], 'sort': None, 'outline_blocks': False, 'outline_color': None}) new_idx = len(norm_blocks) - 1 _refresh_blocks(selected_index=new_idx) _load_block_to_form(new_idx) class _ColorizeDialogHandler(unohelper.Base, XActionListener, XItemListener, XTextListener): def disposing(self, ev): pass def _sync_params_to_block(self): if form_loading[0]: return bi = form_block_index[0] if bi < 0: return _apply_form_to_block(bi) _inner_dialog_update_blocks_list_label(blocks_lb, bi, _colorize_block_label(norm_blocks[bi])) def textChanged(self, ev): try: src = ev.Source except Exception: return if form_loading[0]: return if _wizard_dlg_is_named_control(src, sheet_ed, u'SheetEd'): _sync_sheet_field_to_block() elif _wizard_dlg_is_named_control(src, key_cols_cb, u'KeyColsCb'): _pick_handle_combo_text(key_cols_cb, col_text_prev, seps=(u',', u';')) elif _wizard_dlg_is_named_control(src, colors_cb, u'ColorsCb'): _pick_handle_combo_text(colors_cb, colors_text_prev, seps=_colorize_color_seps()) elif _wizard_dlg_is_named_control(src, outline_col_cb, u'OutlineColCb'): self._sync_params_to_block() def itemStateChanged(self, ev): try: src = ev.Source except Exception: return if src == blocks_lb: if form_loading[0]: return _switch_colorize_block(_block_index_selected()) elif _wizard_dlg_is_named_control(src, key_cols_cb, u'KeyColsCb'): _pick_handle_combo_select(key_cols_cb, col_text_prev, ev, seps=(u',', u';')) elif _wizard_dlg_is_named_control(src, colors_cb, u'ColorsCb'): _pick_handle_combo_select(colors_cb, colors_text_prev, ev, seps=_colorize_color_seps()) elif src in (sort_cb, outline_chk, outline_col_cb): self._sync_params_to_block() def actionPerformed(self, ev): cmd = u'' try: cmd = unicode(ev.ActionCommand) except Exception: pass if cmd == u'KeyColsDelLastBtn': _key_cols_del_last() return if cmd == u'KeyColsDelAllBtn': _key_cols_del_all() return if cmd == u'ColorsDelLastBtn': _colors_del_last() return if cmd == u'ColorsDelAllBtn': _colors_del_all() return if cmd == u'AddBlockBtn': _add_colorize_block_empty() return if cmd == u'DelBlockBtn': bi = _block_index_selected() if 0 <= bi < len(norm_blocks): del norm_blocks[bi] form_block_index[0] = -1 _refresh_blocks(selected_index=max(0, bi - 1)) _switch_colorize_block(_block_index_selected()) return h = _ColorizeDialogHandler() try: blocks_lb.addItemListener(h) sheet_ed.addTextListener(h) key_cols_cb.addTextListener(h) key_cols_cb.addItemListener(h) colors_cb.addTextListener(h) colors_cb.addItemListener(h) outline_col_cb.addTextListener(h) outline_col_cb.addItemListener(h) sort_cb.addItemListener(h) outline_chk.addItemListener(h) except Exception: pass for btn in ('AddBlockBtn', 'DelBlockBtn', 'KeyColsDelLastBtn', 'KeyColsDelAllBtn', 'ColorsDelLastBtn', 'ColorsDelAllBtn', 'OkButton', 'CancelButton'): try: ctl = dlg.getControl(btn) ctl.addActionListener(h) ctl.setActionCommand(btn) except Exception: pass doc_sheet_names = [] try: doc_sheet_names = _pivot_list_doc_sheet_names(doc) except Exception: doc_sheet_names = [] if len(doc_sheet_names) > 0: try: _set_combo_items(sheet_ed, doc_sheet_names, select_text=u'') except Exception: pass _refresh_column_combo_for_sheet(_active_sheet_name()) _refresh_blocks(selected_index=0) _load_block_to_form(0 if len(norm_blocks) > 0 else -1) if dlg.execute() != 1: return None bi = form_block_index[0] if bi < 0: bi = _block_index_selected() if bi < 0 and len(norm_blocks) > 0: bi = 0 if bi >= 0: _apply_form_to_block(bi) out_blocks = [] for b in norm_blocks: keys = b.get('key_columns') or [] colors = b.get('colors') or [] if len(keys) == 0 or len(colors) == 0: continue out_blocks.append(b) return param_encode(u'раскрасить_блоки', out_blocks) def _format_columns_block_label(block): sheet = unicode(block.get('sheet') or u'').strip() rules = block.get('rules') or [] conv = u' +конверт' if block.get('convert_existing') else u'' tail = u'%d прав.' % len(rules) if len(rules) > 0 else u'(нет правил)' return _inner_block_list_label(sheet, tail + conv) def _block_rules_dialog_config(fn_key): key = _inner_codec_dialog_effective_key(fn_key) kcf = unicode(key).casefold() if kcf == u'заполнение_вниз_вычислить'.casefold(): return {'fn_key': u'заполнение_вниз_вычислить', 'title': u'Заполнение вниз (вычислить)', 'hint': u'Блок = лист + правила. Столбец: имя, буква A… или № с 1. Формула: =… или константа; {row_id}, {row-1}, R[-1]C. «Вправо» — после заполнения столбца протянуть формулу в пустые ячейки строки (до границы диапазона).', 'rule_val_key': 'formula', 'rule_val_label': u'Формула', 'rule_val_default': u'=A{row_id}', 'convert_existing': False, 'rule_expand_to_right': True} return {'fn_key': u'формат_столбцы', 'title': u'Формат столбцов', 'hint': u'Блок = лист + правила. Столбец: имя, №, буква, список через запятую, Все/*. Клик по пустому месту списка — новое правило; выбор строки — правка по «Обновить». «Включая заголовок» / «Только заголовок» — область применения правила. «Конвертировать» — текст в число/дату перед форматом.', 'rule_val_key': 'format', 'rule_val_label': u'Формат', 'rule_val_default': u'', 'convert_existing': True, 'supports_style': True, 'rule_explicit_update': True, 'rule_header_flags': True} def _block_rules_column_label(col): if col == u'all': return u'Все' if isinstance(col, (str, unicode)): low = unicode(col).strip().casefold() if low in (u'all', u'все', u'*'): return u'Все' if isinstance(col, (list, tuple)): return u', '.join((unicode(x) for x in col)) return unicode(col) def _format_columns_rule_label(rule): if not isinstance(rule, dict): return u'' col = rule.get('column') if col is None: return u'' col_s = _block_rules_column_label(col) parts = [] fmt = unicode(rule.get('format') or u'').strip() if fmt: parts.append(u'формат %s' % fmt) fill = unicode(rule.get('fill') or u'').strip() if fill: parts.append(u'заливка %s' % fill) if rule.get('font_auto'): parts.append(u'шрифт auto') else: font = unicode(rule.get('font') or u'').strip() if font: parts.append(u'шрифт %s' % font) h = unicode(rule.get('h_align') or u'').strip() v = unicode(rule.get('v_align') or u'').strip() if h or v: parts.append(u'выр. %s/%s' % (h or u'-', v or u'-')) bw = unicode(rule.get('border_width') or u'').strip() bc = unicode(rule.get('border_color') or u'').strip() if bw or bc: parts.append(u'граница %s/%s' % (bw or u'-', bc or u'-')) if rule.get('expand_to_right'): parts.append(u'+вправо') if rule.get('header_only'): parts.append(u'только заголовок') elif rule.get('include_header'): parts.append(u'+заголовок') tail = u'; '.join(parts) if parts else u'(без оформления)' return u'%s → %s' % (col_s, tail) def _block_rules_rule_label(rule, cfg): if cfg.get('supports_style'): return _format_columns_rule_label(rule) if not isinstance(rule, dict): return u'' col = rule.get('column') if col is None: return u'' col_s = _block_rules_column_label(col) val_key = cfg.get('rule_val_key') or 'format' val = unicode(rule.get(val_key) or u'').strip() if val == u'': val = unicode(cfg.get('rule_val_default') or u'').strip() or u'—' suffix = u'' if cfg.get('rule_expand_to_right') and rule.get('expand_to_right'): suffix = u' +вправо' return u'%s → %s%s' % (col_s, val, suffix) def _block_rules_block_label(block, cfg): sheet = unicode(block.get('sheet') or u'').strip() rules = block.get('rules') or [] conv = u' +конверт' if block.get('convert_existing') else u'' tail = u'%d прав.' % len(rules) if len(rules) > 0 else u'(нет правил)' return _inner_block_list_label(sheet, tail + conv) def _show_block_rules_param_dialog(parent_dialog, fn_key, initial_text, doc=None): """ SheetBlockGrid+rules (§3.4.2): блоки по листам, вложенные правила столбец/значение. """ from libre_macros_param_codec import param_decode, param_encode cfg = _block_rules_dialog_config(fn_key) fn_key = cfg['fn_key'] rule_val_key = cfg['rule_val_key'] rule_val_default = cfg.get('rule_val_default') or u'' convert_flag = bool(cfg.get('convert_existing')) expand_rule_flag = bool(cfg.get('rule_expand_to_right')) init = unicode(initial_text or u'').strip() try: blocks = param_decode(fn_key, init) except Exception: blocks = [] norm_blocks = [] for b in blocks or []: if isinstance(b, dict): nb = dict(b) if nb.get('rules') is None: nb['rules'] = [] norm_blocks.append(nb) if len(norm_blocks) == 0: norm_blocks = [{'v': 1, 'fn': fn_key, 'rules': [], 'convert_existing': False}] ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) dm = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialogModel', ctx) dw = 600 m = 8 col_gap = 10 left_w = 180 right_w = dw - m * 2 - left_w - col_gap # Подписи «Столбец:» / «Горизонт.:» шире 60 — иначе наползают на combo. lbl_w = 86 lbl_gap = 6 row_h = 22 style_flag = bool(cfg.get('supports_style')) header_flags = bool(cfg.get('rule_header_flags')) hint = cfg.get('hint') or u'' hint_h = 44 if len(hint) > 120 else 32 rules_h = 88 # Список блоков = высота списка правил (не на всю форму). list_h = rules_h dm.PositionX = 110 dm.PositionY = 50 dm.Width = dw dm.Height = 400 # уточним после раскладки dm.Title = cfg.get('title') or fn_key _inner_dialog_set_sizeable(dm) y = m _wizard_dlg_add_fixed(dm, 'HintLbl', hint, m, y, dw - m * 2, hint_h, multiline=True) y += hint_h + 2 _wizard_dlg_add_fixed(dm, 'BlocksLbl', u'Блоки (листы):', m, y, left_w, 14) _wizard_dlg_add_fixed(dm, 'RulesLbl', u'Правила:', m + left_w + col_gap, y, right_w, 14) y += 14 rx = m + left_w + col_gap ctl_x = rx + lbl_w + lbl_gap ctl_w = max(80, right_w - lbl_w - lbl_gap) _wizard_dlg_add_listbox(dm, 'BlocksLB', m, y, left_w, list_h, multiselect=False) _wizard_dlg_add_listbox(dm, 'RulesLB', rx, y, right_w, rules_h, multiselect=False) # Лист / блоки — сразу под коротким списком слева y2 = y + list_h + 6 _wizard_dlg_add_fixed(dm, 'SheetLbl', u'Лист (пусто = все листы):', m, y2, left_w, 14) _wizard_dlg_add_combo(dm, 'SheetEd', m, y2 + 14, left_w, 18) _wizard_dlg_add_button(dm, 'AddBlockBtn', u'+ Блок', m, y2 + 36, 80, 16) _wizard_dlg_add_button(dm, 'DelBlockBtn', u'Удалить', m + 90, y2 + 36, 80, 16) fy = y + rules_h + 6 _wizard_dlg_add_fixed(dm, 'ColLbl', u'Столбец:', rx, fy, lbl_w, 14) _wizard_dlg_add_combo(dm, 'ColCb', ctl_x, fy - 2, ctl_w, 18) fy += row_h _wizard_dlg_add_fixed(dm, 'FmtLbl', cfg.get('rule_val_label') or u'Значение', rx, fy, lbl_w, 14) _wizard_dlg_add_combo(dm, 'FmtCb', ctl_x, fy - 2, ctl_w, 18) fy += row_h if style_flag: _wizard_dlg_add_fixed(dm, 'FillLbl', u'Заливка:', rx, fy, lbl_w, 14) _wizard_dlg_add_combo(dm, 'FillCb', ctl_x, fy - 2, ctl_w, 18) fy += row_h _wizard_dlg_add_fixed(dm, 'FontLbl', u'Шрифт:', rx, fy, lbl_w, 14) _wizard_dlg_add_combo(dm, 'FontCb', ctl_x, fy - 2, ctl_w, 18) fy += 20 _wizard_dlg_add_checkbox(dm, 'FontAutoChk', u'Цвет шрифта автоматически', ctl_x, fy - 2, ctl_w, 16) fy += 18 half = max(90, int((ctl_w - 8) / 2)) _wizard_dlg_add_fixed(dm, 'HAlignLbl', u'Горизонт.:', rx, fy, lbl_w, 14) _wizard_dlg_add_combo(dm, 'HAlignCb', ctl_x, fy - 2, half, 18) _wizard_dlg_add_fixed(dm, 'VAlignLbl', u'Вертикал.:', ctl_x + half + 8, fy, 70, 14) _wizard_dlg_add_combo(dm, 'VAlignCb', ctl_x + half + 8 + 70 + 4, fy - 2, max(60, ctl_w - half - 8 - 70 - 4), 18) fy += row_h _wizard_dlg_add_fixed(dm, 'BorderWLbl', u'Граница:', rx, fy, lbl_w, 14) _wizard_dlg_add_combo(dm, 'BorderWCb', ctl_x, fy - 2, half, 18) _wizard_dlg_add_combo(dm, 'BorderColCb', ctl_x + half + 8, fy - 2, max(60, ctl_w - half - 8), 18) fy += row_h if header_flags: _wizard_dlg_add_checkbox(dm, 'InclHeaderChk', u'Включая заголовок', rx, fy, 160, 16) _wizard_dlg_add_checkbox(dm, 'HeaderOnlyChk', u'Только заголовок', rx + 170, fy - 2, max(120, right_w - 170), 16) fy += 18 if convert_flag: _wizard_dlg_add_checkbox(dm, 'ConvertChk', u'Конвертировать текст перед форматом', rx, fy, right_w, 16) fy += 18 if expand_rule_flag: _wizard_dlg_add_checkbox(dm, 'ExpandRightChk', u'Заполнить пустые ячейки вправо (expand_to_right)', rx, fy, right_w, 16) fy += 18 # Одна линия: OK/Отмена слева и +Правило/×/Обновить справа; JSON ниже → по ней высота окна. left_block_bottom = y2 + 36 + 16 btn_y = max(fy + 6, left_block_bottom + 4) btn_h = _pw_cfg._INNER_BTN_H _wizard_dlg_add_button(dm, 'OkButton', u'OK', m, btn_y, _pw_cfg._INNER_BTN_W, btn_h) _wizard_dlg_add_button(dm, 'CancelButton', u'Отмена', m + _pw_cfg._INNER_BTN_W + _pw_cfg._INNER_BTN_GAP, btn_y, _pw_cfg._INNER_BTN_W, btn_h) _wizard_dlg_add_button(dm, 'AddRuleBtn', u'+ Правило', rx, btn_y, 80, btn_h) _wizard_dlg_add_button(dm, 'DelRuleBtn', u'× Правило', rx + 90, btn_y, 80, btn_h) _wizard_dlg_add_button(dm, 'UpdRuleBtn', u'Обновить', rx + 180, btn_y, 90, btn_h) try: dm.getByName('OkButton').DefaultButton = True except Exception: pass try: dm.getByName('OkButton').PushButtonType = 1 except Exception: pass try: dm.getByName('CancelButton').PushButtonType = 2 except Exception: pass json_y = btn_y + btn_h + 4 _wizard_dlg_add_checkbox(dm, 'JsonChk', u'Сохранять как JSON', m, json_y, 200, _pw_cfg._INNER_CHK_H) dh = json_y + _pw_cfg._INNER_CHK_H + m dm.Height = dh dlg = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialog', ctx) dlg.setModel(dm) _nested_subdialog_create_peer( dlg, toolkit, doc, parent_dialog, center_in_main_wizard=bool(parent_dialog) ) blocks_lb = dlg.getControl('BlocksLB') rules_lb = dlg.getControl('RulesLB') sheet_ed = dlg.getControl('SheetEd') col_cb = dlg.getControl('ColCb') fmt_cb = dlg.getControl('FmtCb') fill_cb = dlg.getControl('FillCb') if style_flag else None font_cb = dlg.getControl('FontCb') if style_flag else None font_auto_chk = dlg.getControl('FontAutoChk') if style_flag else None h_align_cb = dlg.getControl('HAlignCb') if style_flag else None v_align_cb = dlg.getControl('VAlignCb') if style_flag else None border_w_cb = dlg.getControl('BorderWCb') if style_flag else None border_col_cb = dlg.getControl('BorderColCb') if style_flag else None incl_header_chk = dlg.getControl('InclHeaderChk') if header_flags else None header_only_chk = dlg.getControl('HeaderOnlyChk') if header_flags else None convert_chk = dlg.getControl('ConvertChk') if convert_flag else None expand_chk = dlg.getControl('ExpandRightChk') if expand_rule_flag else None del_rule_btn = dlg.getControl('DelRuleBtn') upd_rule_btn = dlg.getControl('UpdRuleBtn') explicit_rule_update = bool(cfg.get('rule_explicit_update')) try: dlg.getControl('JsonChk').setState(1) except Exception: pass form_block_index = [-1] last_rule_index = [-1] sheet_field_guard = [False] form_loading = [False] col_text_prev = [u''] doc_sheet_names = [] try: doc_sheet_names = _pivot_list_doc_sheet_names(doc) except Exception: doc_sheet_names = [] def _refresh_blocks(selected_index=0): form_loading[0] = True items = [_block_rules_block_label(b, cfg) for b in norm_blocks] try: blocks_lb.Model.StringItemList = tuple(items) except Exception: pass try: if len(items) > 0: blocks_lb.selectItemPos(max(0, min(selected_index, len(items) - 1)), True) except Exception: pass form_loading[0] = False def _block_index_selected(): try: pos = blocks_lb.getSelectedItemPos() return int(pos) if pos is not None else -1 except Exception: return -1 def _rule_index_selected(): try: sel = rules_lb.getSelectedItemsPos() if sel is None or len(sel) == 0: return -1 pos = sel[0] if pos is None or int(pos) < 0: return -1 return int(pos) except Exception: pass try: pos = rules_lb.getSelectedItemPos() if pos is None or int(pos) < 0: return -1 return int(pos) except Exception: return -1 def _deselect_rules_list(): try: rules_lb.selectItemPos(-1, False) except Exception: pass def _set_rule_action_buttons(has_selection): if not explicit_rule_update: return for ctl in (del_rule_btn, upd_rule_btn): if ctl is None: continue try: ctl.setEnable(bool(has_selection)) except Exception: try: ctl.Model.Enabled = bool(has_selection) except Exception: pass def _on_rule_selection_changed(ri): last_rule_index[0] = ri if ri < 0: _clear_rule_form_fields() _set_rule_action_buttons(False) return bi = _block_index_selected() if bi >= 0: _load_rule_fields(bi, ri) _set_rule_action_buttons(True) def _refresh_rules(block_index, selected_rule=0, allow_no_select=False): if block_index < 0 or block_index >= len(norm_blocks): try: rules_lb.Model.StringItemList = tuple() except Exception: pass return rules = norm_blocks[block_index].get('rules') or [] items = [_block_rules_rule_label(r, cfg) for r in rules] form_loading[0] = True try: rules_lb.Model.StringItemList = tuple(items) except Exception: pass try: if allow_no_select and selected_rule < 0: _deselect_rules_list() elif len(items) > 0: rules_lb.selectItemPos(max(0, min(selected_rule, len(items) - 1)), True) except Exception: pass form_loading[0] = False def _load_block_to_form(block_index): form_loading[0] = True form_block_index[0] = block_index last_rule_index[0] = -1 if block_index < 0 or block_index >= len(norm_blocks): _inner_dialog_set_sheet_field_text(sheet_ed, u'', sheet_field_guard) _refresh_rules(-1) if convert_chk is not None: try: convert_chk.setState(0) except Exception: pass if expand_chk is not None: try: expand_chk.setState(0) except Exception: pass form_loading[0] = False return b = norm_blocks[block_index] _inner_dialog_set_sheet_field_text(sheet_ed, unicode(b.get('sheet') or u'').strip(), sheet_field_guard) _refresh_column_combo_for_sheet(unicode(b.get('sheet') or u'').strip()) if convert_chk is not None: try: convert_chk.setState(1 if b.get('convert_existing') else 0) except Exception: pass _refresh_rules(block_index, -1 if explicit_rule_update else 0, allow_no_select=explicit_rule_update) try: _apply_combo_selection(col_cb, col_cb.Model.StringItemList, clear=True) _apply_combo_selection(fmt_cb, fmt_cb.Model.StringItemList, clear=True) if style_flag: _apply_combo_selection(fill_cb, fill_cb.Model.StringItemList, clear=True) _apply_combo_selection(font_cb, font_cb.Model.StringItemList, clear=True) _apply_combo_selection(h_align_cb, h_align_cb.Model.StringItemList, clear=True) _apply_combo_selection(v_align_cb, v_align_cb.Model.StringItemList, clear=True) _apply_combo_selection(border_w_cb, border_w_cb.Model.StringItemList, clear=True) _apply_combo_selection(border_col_cb, border_col_cb.Model.StringItemList, clear=True) try: font_auto_chk.setState(0) except Exception: pass if expand_chk is not None: expand_chk.setState(0) if incl_header_chk is not None: try: incl_header_chk.setState(0) except Exception: pass if header_only_chk is not None: try: header_only_chk.setState(0) except Exception: pass except Exception: pass if explicit_rule_update: last_rule_index[0] = -1 _set_rule_action_buttons(False) else: rules = norm_blocks[block_index].get('rules') or [] if len(rules) > 0: last_rule_index[0] = 0 _load_rule_fields(block_index, 0) form_loading[0] = False def _apply_sheet_and_convert(block_index): if block_index < 0 or block_index >= len(norm_blocks): return b = norm_blocks[block_index] _inner_dialog_set_block_sheet(b, _inner_dialog_sheet_text(sheet_ed)) try: if convert_chk is not None and convert_chk.getState(): b['convert_existing'] = True else: b.pop('convert_existing', None) except Exception: b.pop('convert_existing', None) b['v'] = 1 b['fn'] = fn_key def _parse_column_value(text): t = unicode(text or u'').strip() if t == u'': return None low = t.casefold() if low in (u'all', u'все', u'*'): return u'all' if u',' in t: parts = [] for p in t.split(u','): p = p.strip() if not p: continue pl = p.casefold() if pl in (u'all', u'все', u'*'): return u'all' if p.isdigit(): parts.append(int(p)) else: parts.append(p) if not parts: return None if len(parts) == 1: return parts[0] return parts if t.isdigit(): return int(t) return t def _column_value_display(col): if col == u'all': return u'Все' if isinstance(col, (list, tuple)): return u', '.join((unicode(x) for x in col)) return unicode(col or u'') def _active_sheet_name(): try: if doc is None: return u'' ctrl = doc.getCurrentController() sh = ctrl.getActiveSheet() if ctrl is not None else None return unicode(getattr(sh, 'Name', u'') or u'').strip() except Exception: return u'' def _refresh_column_combo_for_sheet(sheet_name=None): name = unicode(sheet_name or u'').strip() or _active_sheet_name() cols = [] try: cols = _vlookup_list_sheet_columns(doc, name, u'1', u'1') except Exception: cols = [] base = [u'', u'Все', u'*'] + cols + [unicode(i) for i in range(1, 51)] base += [unichr(ord('A') + i) for i in range(0, 26)] seen = set() out = [] cur = _combo_edit_text(col_cb) for item in base + ([cur] if cur else []): s = unicode(item or u'').strip() k = s.casefold() if k in seen: continue seen.add(k) out.append(s) try: col_cb.Model.StringItemList = tuple(out) except Exception: pass _set_combo_edit_text(col_cb, cur) col_text_prev[0] = cur _refresh_column_combo_for_sheet(_active_sheet_name()) try: formats = [u'Общий', u'# ##0,00', u'# ##0', u'0,00', u'0', u'0%', u'0,00%', u'@', u'0.00E+00'] gs_lib = _wp_import_global_settings_lib() if gs_lib: calc_fn = gs_lib.get('calc_number_format_choices') if calc_fn is not None: formats += [x for x in calc_fn() if x != gs_lib['CUSTOM_NUMBER_FORMAT_LABEL']] else: formats += list(gs_lib['NUMBER_FORMAT_CHOICES']) formats += list(gs_lib['DATE_FORMAT_CHOICES']) friendly = gs_lib.get('is_calc_friendly_number_format') if gs_lib else None seen = set() out = [] for f in formats: s = unicode(f or u'').strip() if not s: continue if friendly is not None and (not friendly(s)): continue k = s.casefold() if k in seen: continue seen.add(k) out.append(s) _set_combo_items(fmt_cb, out, select_text=u'') except Exception: pass if style_flag: try: colors = _pw_cfg._header_plus_height_color_choices() _set_combo_items(fill_cb, colors, select_text=u'') _set_combo_items(font_cb, colors, select_text=u'') _set_combo_items(border_col_cb, colors, select_text=u'') _set_combo_items(border_w_cb, (u'', u'тонкая', u'средняя', u'толстая'), select_text=u'') _set_combo_items(h_align_cb, (u'', u'left', u'center', u'right'), select_text=u'') _set_combo_items(v_align_cb, (u'', u'top', u'center', u'bottom'), select_text=u'') except Exception: pass def _sync_sheet_field_to_block(): if sheet_field_guard[0] or form_loading[0]: return bi = form_block_index[0] if bi < 0 or bi >= len(norm_blocks): return _inner_dialog_set_block_sheet(norm_blocks[bi], _inner_dialog_sheet_text(sheet_ed)) _refresh_column_combo_for_sheet(_inner_dialog_sheet_text(sheet_ed)) _inner_dialog_update_blocks_list_label(blocks_lb, bi, _block_rules_block_label(norm_blocks[bi], cfg)) def _clear_rule_form_fields(): form_loading[0] = True try: _apply_combo_selection(col_cb, col_cb.Model.StringItemList, clear=True) _apply_combo_selection(fmt_cb, fmt_cb.Model.StringItemList, clear=True) if style_flag: _apply_combo_selection(fill_cb, fill_cb.Model.StringItemList, clear=True) _apply_combo_selection(font_cb, font_cb.Model.StringItemList, clear=True) _apply_combo_selection(h_align_cb, h_align_cb.Model.StringItemList, clear=True) _apply_combo_selection(v_align_cb, v_align_cb.Model.StringItemList, clear=True) _apply_combo_selection(border_w_cb, border_w_cb.Model.StringItemList, clear=True) _apply_combo_selection(border_col_cb, border_col_cb.Model.StringItemList, clear=True) try: font_auto_chk.setState(0) except Exception: pass if expand_chk is not None: expand_chk.setState(0) if incl_header_chk is not None: try: incl_header_chk.setState(0) except Exception: pass if header_only_chk is not None: try: header_only_chk.setState(0) except Exception: pass except Exception: pass col_text_prev[0] = u'' _sync_header_only_checkbox() form_loading[0] = False def _sync_header_only_checkbox(): if header_only_chk is None or incl_header_chk is None: return try: only = bool(header_only_chk.getState()) if only: incl_header_chk.setState(1) try: incl_header_chk.Model.Enabled = False except Exception: try: incl_header_chk.setEnable(False) except Exception: pass else: try: incl_header_chk.Model.Enabled = True except Exception: try: incl_header_chk.setEnable(True) except Exception: pass except Exception: pass def _build_rule_from_form(): col_val = _parse_column_value(_combo_edit_text(col_cb)) if col_val is None: return None rule = {'column': col_val} val = unicode(_combo_control_text(fmt_cb) or u'').strip() if val: rule[rule_val_key] = val if style_flag: fill = _combo_control_text(fill_cb) font = _combo_control_text(font_cb) h_align = _combo_control_text(h_align_cb) v_align = _combo_control_text(v_align_cb) bw = _combo_control_text(border_w_cb) bc = _combo_control_text(border_col_cb) if fill: rule['fill'] = fill if font: rule['font'] = font try: if font_auto_chk.getState(): rule['font_auto'] = True except Exception: pass if h_align: rule['h_align'] = h_align if v_align: rule['v_align'] = v_align if bw: rule['border_width'] = bw if bc: rule['border_color'] = bc if header_only_chk is not None: try: if header_only_chk.getState(): rule['header_only'] = True elif incl_header_chk is not None and incl_header_chk.getState(): rule['include_header'] = True except Exception: pass if expand_chk is not None: try: if expand_chk.getState(): rule['expand_to_right'] = True except Exception: pass return rule def _append_rule_from_form(block_index): if block_index < 0 or block_index >= len(norm_blocks): return False _apply_sheet_and_convert(block_index) rule = _build_rule_from_form() if rule is None: return False rules = list(norm_blocks[block_index].get('rules') or []) rules.append(rule) norm_blocks[block_index]['rules'] = rules _refresh_rules(block_index, len(rules) - 1, allow_no_select=False) return True def _flush_pending_rule(block_index, rule_index=None): if block_index < 0 or block_index >= len(norm_blocks): return _apply_sheet_and_convert(block_index) rule = _build_rule_from_form() if rule is None: return rules = list(norm_blocks[block_index].get('rules') or []) ri = _rule_index_selected() if rule_index is None else rule_index if explicit_rule_update and ri < 0: return if 0 <= ri < len(rules): rules[ri] = rule else: if explicit_rule_update: return rules.append(rule) norm_blocks[block_index]['rules'] = rules if rule_index is None and (not explicit_rule_update): _refresh_rules(block_index, len(rules) - 1) def _load_rule_fields(bi, ri): if bi < 0 or ri < 0: return rules = norm_blocks[bi].get('rules') or [] if ri >= len(rules): return r = rules[ri] form_loading[0] = True try: col_disp = _column_value_display(r.get('column')) _set_combo_edit_text(col_cb, col_disp) col_text_prev[0] = col_disp except Exception: pass try: _apply_combo_selection(fmt_cb, fmt_cb.Model.StringItemList, select_name=unicode(r.get(rule_val_key) or u'')) except Exception: pass if style_flag: try: _apply_combo_selection(fill_cb, fill_cb.Model.StringItemList, select_name=unicode(r.get('fill') or u'')) except Exception: pass try: _apply_combo_selection(font_cb, font_cb.Model.StringItemList, select_name=unicode(r.get('font') or u'')) except Exception: pass try: _apply_combo_selection(h_align_cb, h_align_cb.Model.StringItemList, select_name=unicode(r.get('h_align') or u'')) except Exception: pass try: _apply_combo_selection(v_align_cb, v_align_cb.Model.StringItemList, select_name=unicode(r.get('v_align') or u'')) except Exception: pass try: font_auto_chk.setState(1 if r.get('font_auto') else 0) except Exception: pass try: _apply_combo_selection(border_w_cb, border_w_cb.Model.StringItemList, select_name=unicode(r.get('border_width') or u'')) except Exception: pass try: _apply_combo_selection(border_col_cb, border_col_cb.Model.StringItemList, select_name=unicode(r.get('border_color') or u'')) except Exception: pass if expand_chk is not None: try: expand_chk.setState(1 if r.get('expand_to_right') else 0) except Exception: pass if header_only_chk is not None: try: header_only_chk.setState(1 if r.get('header_only') else 0) except Exception: pass if incl_header_chk is not None: try: incl_header_chk.setState(1 if r.get('include_header') or r.get('header_only') else 0) except Exception: pass _sync_header_only_checkbox() form_loading[0] = False def _switch_format_columns_block(block_index): prev = form_block_index[0] prev_ri = last_rule_index[0] if prev >= 0 and prev != block_index: if explicit_rule_update: if prev_ri >= 0: _flush_pending_rule(prev, prev_ri) else: _flush_pending_rule(prev) _inner_dialog_update_blocks_list_label(blocks_lb, prev, _block_rules_block_label(norm_blocks[prev], cfg)) _load_block_to_form(block_index) def _add_format_columns_block_empty(): prev = form_block_index[0] if prev >= 0 and (not explicit_rule_update): _flush_pending_rule(prev) _inner_dialog_update_blocks_list_label(blocks_lb, prev, _block_rules_block_label(norm_blocks[prev], cfg)) norm_blocks.append({'v': 1, 'fn': fn_key, 'rules': [], 'convert_existing': False}) new_idx = len(norm_blocks) - 1 _refresh_blocks(selected_index=new_idx) _load_block_to_form(new_idx) class _RulesListMouseListener(unohelper.Base, XMouseListener): def disposing(self, ev): pass def mousePressed(self, ev): if not explicit_rule_update or form_loading[0]: return try: cnt = int(rules_lb.getItemCount()) except Exception: cnt = 0 if cnt <= 0: return try: y = int(ev.Y) except Exception: return row_h = 16 if y >= cnt * row_h: form_loading[0] = True try: _deselect_rules_list() except Exception: pass form_loading[0] = False _on_rule_selection_changed(-1) def mouseReleased(self, ev): pass def mouseEntered(self, ev): pass def mouseExited(self, ev): pass class _FormatColumnsHandler(unohelper.Base, XActionListener, XItemListener, XTextListener): def disposing(self, ev): pass def textChanged(self, ev): try: src = ev.Source except Exception: return if form_loading[0]: return if src == col_cb: from_list = False try: pos = col_cb.getSelectedItemPos() if pos is not None and int(pos) >= 0: from_list = True except Exception: from_list = False if not from_list: col_text_prev[0] = _combo_edit_text(col_cb) if explicit_rule_update or from_list: return if _wizard_dlg_is_named_control(src, sheet_ed, u'SheetEd'): _sync_sheet_field_to_block() elif explicit_rule_update: return elif src in (col_cb, fmt_cb, fill_cb, font_cb, h_align_cb, v_align_cb, border_w_cb, border_col_cb): bi = _block_index_selected() ri = _rule_index_selected() if bi >= 0 and ri >= 0: _flush_pending_rule(bi, ri) rules = norm_blocks[bi].get('rules') or [] if ri < len(rules): try: items = list(rules_lb.Model.StringItemList or ()) if ri < len(items): items[ri] = _block_rules_rule_label(rules[ri], cfg) rules_lb.Model.StringItemList = tuple(items) except Exception: pass elif src == expand_chk: bi = _block_index_selected() ri = _rule_index_selected() if bi >= 0 and ri >= 0: _flush_pending_rule(bi, ri) rules = norm_blocks[bi].get('rules') or [] if ri < len(rules): try: items = list(rules_lb.Model.StringItemList or ()) if ri < len(items): items[ri] = _block_rules_rule_label(rules[ri], cfg) rules_lb.Model.StringItemList = tuple(items) except Exception: pass def itemStateChanged(self, ev): try: src = ev.Source except Exception: return if src == col_cb: if form_loading[0]: return token = u'' try: pos = col_cb.getSelectedItemPos() if pos is not None and int(pos) >= 0: items = col_cb.Model.StringItemList or () if int(pos) < len(items): token = unicode(items[int(pos)]).strip() except Exception: token = u'' if token == u'' or u',' in token or u';' in token: return merged = _append_csv_token_no_dup(col_text_prev[0], token) form_loading[0] = True try: _set_combo_edit_text(col_cb, merged) finally: form_loading[0] = False col_text_prev[0] = merged if not explicit_rule_update: bi = _block_index_selected() ri = _rule_index_selected() if bi >= 0 and ri >= 0: _flush_pending_rule(bi, ri) rules = norm_blocks[bi].get('rules') or [] if ri < len(rules): try: items = list(rules_lb.Model.StringItemList or ()) if ri < len(items): items[ri] = _block_rules_rule_label(rules[ri], cfg) rules_lb.Model.StringItemList = tuple(items) except Exception: pass return if src == blocks_lb: if form_loading[0]: return _switch_format_columns_block(_block_index_selected()) elif src == rules_lb: if form_loading[0]: return if explicit_rule_update: _on_rule_selection_changed(_rule_index_selected()) return bi = _block_index_selected() prev_ri = last_rule_index[0] if bi >= 0 and prev_ri >= 0: _flush_pending_rule(bi, prev_ri) last_rule_index[0] = _rule_index_selected() _load_rule_fields(bi, last_rule_index[0]) elif src == convert_chk: bi = _block_index_selected() if bi >= 0: _apply_sheet_and_convert(bi) _inner_dialog_update_blocks_list_label(blocks_lb, bi, _block_rules_block_label(norm_blocks[bi], cfg)) elif src == header_only_chk: _sync_header_only_checkbox() elif not explicit_rule_update and src in (font_auto_chk, expand_chk, fmt_cb, fill_cb, font_cb, h_align_cb, v_align_cb, border_w_cb, border_col_cb): bi = _block_index_selected() ri = _rule_index_selected() if bi >= 0 and ri >= 0: _flush_pending_rule(bi, ri) rules = norm_blocks[bi].get('rules') or [] if ri < len(rules): try: items = list(rules_lb.Model.StringItemList or ()) if ri < len(items): items[ri] = _block_rules_rule_label(rules[ri], cfg) rules_lb.Model.StringItemList = tuple(items) except Exception: pass def actionPerformed(self, ev): cmd = u'' try: cmd = unicode(ev.ActionCommand) except Exception: pass if cmd == u'AddBlockBtn': _add_format_columns_block_empty() return if cmd == u'DelBlockBtn': bi = _block_index_selected() if 0 <= bi < len(norm_blocks): del norm_blocks[bi] form_block_index[0] = -1 _refresh_blocks(selected_index=max(0, bi - 1)) _switch_format_columns_block(_block_index_selected()) return bi = _block_index_selected() if bi < 0: return if cmd in (u'AddRuleBtn',): if explicit_rule_update: _append_rule_from_form(bi) last_rule_index[0] = -1 _deselect_rules_list() _clear_rule_form_fields() _set_rule_action_buttons(False) _inner_dialog_update_blocks_list_label(blocks_lb, bi, _block_rules_block_label(norm_blocks[bi], cfg)) else: _flush_pending_rule(bi) last_rule_index[0] = -1 _clear_rule_form_fields() _refresh_blocks(selected_index=bi) return if cmd == u'DelRuleBtn': if explicit_rule_update and _rule_index_selected() < 0: return _apply_sheet_and_convert(bi) ri = _rule_index_selected() rules = list(norm_blocks[bi].get('rules') or []) if 0 <= ri < len(rules): del rules[ri] norm_blocks[bi]['rules'] = rules if explicit_rule_update: last_rule_index[0] = -1 _refresh_rules(bi, -1, allow_no_select=True) _clear_rule_form_fields() _set_rule_action_buttons(False) elif len(rules) > 0: new_ri = min(ri, len(rules) - 1) last_rule_index[0] = new_ri _refresh_rules(bi, new_ri) _load_rule_fields(bi, new_ri) else: last_rule_index[0] = -1 _refresh_rules(bi, 0) _clear_rule_form_fields() _refresh_blocks(selected_index=bi) return if cmd == u'UpdRuleBtn': ri = _rule_index_selected() if ri >= 0: _flush_pending_rule(bi, ri) rules = norm_blocks[bi].get('rules') or [] if ri < len(rules): try: items = list(rules_lb.Model.StringItemList or ()) if ri < len(items): items[ri] = _block_rules_rule_label(rules[ri], cfg) rules_lb.Model.StringItemList = tuple(items) except Exception: pass _inner_dialog_update_blocks_list_label(blocks_lb, bi, _block_rules_block_label(norm_blocks[bi], cfg)) return h = _FormatColumnsHandler() rules_mouse = _RulesListMouseListener() try: blocks_lb.addItemListener(h) rules_lb.addItemListener(h) if explicit_rule_update: rules_lb.addMouseListener(rules_mouse) sheet_ed.addTextListener(h) col_cb.addTextListener(h) fmt_cb.addTextListener(h) if style_flag: fill_cb.addTextListener(h) font_cb.addTextListener(h) h_align_cb.addTextListener(h) v_align_cb.addTextListener(h) border_w_cb.addTextListener(h) border_col_cb.addTextListener(h) font_auto_chk.addItemListener(h) if expand_chk is not None: expand_chk.addItemListener(h) if convert_chk is not None: convert_chk.addItemListener(h) if header_only_chk is not None: header_only_chk.addItemListener(h) except Exception: pass for btn in ('AddBlockBtn', 'DelBlockBtn', 'AddRuleBtn', 'DelRuleBtn', 'UpdRuleBtn'): try: ctl = dlg.getControl(btn) ctl.addActionListener(h) ctl.setActionCommand(btn) except Exception: pass if len(doc_sheet_names) > 0: try: _set_combo_items(sheet_ed, doc_sheet_names, select_text=u'') except Exception: pass _refresh_blocks(0) _load_block_to_form(0 if len(norm_blocks) > 0 else -1) if explicit_rule_update: _set_rule_action_buttons(False) try: rc = dlg.execute() if rc != 1: return None bi = form_block_index[0] if bi < 0: bi = _block_index_selected() if bi < 0 and len(norm_blocks) > 0: bi = 0 if bi >= 0: _apply_sheet_and_convert(bi) if explicit_rule_update: ri = _rule_index_selected() if ri < 0: ri = last_rule_index[0] if ri >= 0: _flush_pending_rule(bi, ri) else: _flush_pending_rule(bi) out_blocks = [] for b in norm_blocks: rules = b.get('rules') or [] if len(rules) == 0 and (not b.get('convert_existing')): continue out_blocks.append(b) return param_encode(fn_key, out_blocks) finally: try: dlg.dispose() except Exception: pass def _show_format_columns_param_dialog(parent_dialog, initial_text, doc=None): return _show_block_rules_param_dialog(parent_dialog, u'формат_столбцы', initial_text, doc=doc) def _normalize_text_block_label(block): sheet = unicode(block.get('sheet') or u'').strip() cols = block.get('columns') or [] if isinstance(cols, (list, tuple)): col_s = u', '.join(unicode(c) for c in cols if unicode(c).strip()) else: col_s = unicode(cols or u'').strip() prefix = _inner_block_list_sheet_prefix(sheet) if col_s: return u'%s [%s]' % (prefix, col_s) return prefix def _normalize_op_list_label(op, index=None): if isinstance(op, (unicode, str)): op = {u'op': op} if not isinstance(op, dict): return u'' code = unicode(op.get('op') or u'').strip().casefold() title = _pw_cfg.NORMALIZE_OP_LABEL.get(code) or code or u'?' bits = [] if code == u'case': mode = unicode(op.get('mode') or u'').strip().casefold() mode_ru = _pw_cfg.NORMALIZE_CASE_LABEL.get(mode) or mode or u'?' bits.append(mode_ru) rf = unicode(op.get('row_filter') or u'all').strip().casefold() or u'all' try: from libre_macros_normalize_lib import normalize_row_filter rf = normalize_row_filter(rf) except Exception: pass rf_ru = _pw_cfg.NORMALIZE_ROW_FILTER_LABEL.get(rf) or rf if rf == u'lambda': lam = unicode( op.get('row_filter_lambda') or op.get('filter') or op.get('lambda') or u'' ).strip() if lam: short = lam.replace(u'\n', u' ') if len(short) > 36: short = short[:33] + u'…' bits.append(u'λ %s' % short) else: bits.append(rf_ru) elif rf != u'all': bits.append(rf_ru) if bits: title = u'%s · %s' % (title, u' · '.join(bits)) if index is not None: return u'%d. %s' % (int(index) + 1, title) return title def _normalize_code_from_label(label, code_map, default=u''): t = unicode(label or u'').strip() if t == u'': return default low = t.casefold() if low in code_map: return code_map[low] # уже код return low or default def _show_normalize_text_param_dialog(parent_dialog, initial_text, doc=None): """ Визуальный конструктор текстовые_операции (legacy: нормализация_текста): блоки по листам, столбцы, стек ops (+/−/↑/↓/Обновить), фильтр строк per-op, нетекст/ошибка по-русски. """ from libre_macros_param_codec import param_decode, param_encode from libre_macros_normalize_lib import coerce_ops_list fn_key = u'текстовые_операции' init = unicode(initial_text or u'').strip() try: blocks = param_decode(fn_key, init) except Exception: blocks = [] norm_blocks = [] for b in blocks: if not isinstance(b, dict): continue nb = dict(b) nb['fn'] = fn_key cols = nb.get('columns') if isinstance(cols, (list, tuple)): nb['columns'] = [unicode(c).strip() for c in cols if unicode(c).strip()] elif cols: nb['columns'] = [unicode(cols).strip()] else: nb['columns'] = [] nb['ops'] = coerce_ops_list(nb.get('ops')) nt = unicode(nb.get('non_text') or u'skip').strip().casefold() or u'skip' if nt in _pw_cfg.NORMALIZE_NON_TEXT_CODE: nt = _pw_cfg.NORMALIZE_NON_TEXT_CODE[nt] nb['non_text'] = nt if nt in _pw_cfg.NORMALIZE_NON_TEXT_LABEL else u'skip' oe = unicode(nb.get('on_error') or u'keep').strip().casefold() or u'keep' if oe in _pw_cfg.NORMALIZE_ON_ERROR_CODE: oe = _pw_cfg.NORMALIZE_ON_ERROR_CODE[oe] nb['on_error'] = oe if oe in _pw_cfg.NORMALIZE_ON_ERROR_LABEL else u'keep' if 'as_values' not in nb: nb['as_values'] = True norm_blocks.append(nb) if len(norm_blocks) == 0: norm_blocks = [{ 'v': 1, 'fn': fn_key, 'columns': [], 'ops': [ {u'op': u'collapse_ws', u'row_filter': u'all'}, {u'op': u'lang_homoglyphs', u'row_filter': u'all'}, {u'op': u'case', u'mode': u'proper', u'row_filter': u'all'}, ], 'non_text': u'skip', 'on_error': u'keep', 'as_values': True, }] ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) dm = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialogModel', ctx) dw = 640 m = 10 col_gap = 10 left_w = 180 right_w = dw - m * 2 - left_w - col_gap lbl_w = 90 lbl_gap = 6 row_h = 22 list_h = 100 hint_h = 40 dm.PositionX = 100 dm.PositionY = 50 dm.Width = dw dm.Height = 560 dm.Title = u'Текстовые операции' try: dm.Sizeable = True except Exception: pass y = m _wizard_dlg_add_fixed( dm, 'HintLbl', u'Стек ops слева направо. Выбор строки ops → поля Op/Регистр/Фильтр. ' u'«Обновить» записывает поля в выбранный op. Фильтр: все / непустые / пустые / λ(row). ' u'Пример λ: lambda row: int(row.get("row_id") or 0) % 2 == 0', m, y, dw - m * 2, hint_h, multiline=True, ) y += hint_h + 2 _wizard_dlg_add_fixed(dm, 'BlocksLbl', u'Листы:', m, y, left_w, 14) _wizard_dlg_add_fixed(dm, 'OpsLbl', u'Стек ops:', m + left_w + col_gap, y, right_w, 14) y += 14 rx = m + left_w + col_gap ctl_x = rx + lbl_w + lbl_gap ctl_w = max(100, right_w - lbl_w - lbl_gap) _wizard_dlg_add_listbox(dm, 'BlocksLB', m, y, left_w, list_h, multiselect=False) _wizard_dlg_add_listbox(dm, 'OpsLB', rx, y, right_w, list_h, multiselect=False) y2 = y + list_h + 6 _wizard_dlg_add_fixed(dm, 'SheetLbl', u'Лист (пусто = все):', m, y2, left_w, 14) _wizard_dlg_add_combo(dm, 'SheetEd', m, y2 + 14, left_w, 18) _wizard_dlg_add_button(dm, 'AddBlockBtn', u'+ Блок', m, y2 + 36, 80, 16) _wizard_dlg_add_button(dm, 'DelBlockBtn', u'Удалить', m + 90, y2 + 36, 80, 16) fy = y2 _wizard_dlg_add_fixed(dm, 'ColsLbl', u'Столбцы:', rx, fy, lbl_w, 14) _wizard_dlg_add_combo(dm, 'ColsCb', ctl_x, fy - 2, max(80, ctl_w - 52), 18) _wizard_dlg_add_button(dm, 'ColsDelLastBtn', u'×', ctl_x + max(80, ctl_w - 52) + 4, fy, 22, 16) _wizard_dlg_add_button(dm, 'ColsDelAllBtn', u'××', ctl_x + max(80, ctl_w - 52) + 28, fy, 22, 16) fy += row_h _wizard_dlg_add_fixed(dm, 'OpTypeLbl', u'Операция:', rx, fy, lbl_w, 14) _wizard_dlg_add_combo(dm, 'OpTypeCb', ctl_x, fy - 2, ctl_w, 18) fy += row_h _wizard_dlg_add_fixed(dm, 'CaseModeLbl', u'Регистр:', rx, fy, lbl_w, 14) _wizard_dlg_add_combo(dm, 'CaseModeCb', ctl_x, fy - 2, min(180, ctl_w), 18) fy += row_h _wizard_dlg_add_fixed(dm, 'RowFilterLbl', u'Фильтр строк:', rx, fy, lbl_w, 14) _wizard_dlg_add_combo(dm, 'RowFilterCb', ctl_x, fy - 2, min(200, ctl_w), 18) fy += row_h _wizard_dlg_add_fixed(dm, 'RowLambdaLbl', u'Лямбда:', rx, fy, lbl_w, 14) _wizard_dlg_add_edit(dm, 'RowLambdaEd', ctl_x, fy - 2, ctl_w, 18) fy += row_h _wizard_dlg_add_button(dm, 'AddOpBtn', u'+ Op', rx, fy, 58, 16) _wizard_dlg_add_button(dm, 'DelOpBtn', u'× Op', rx + 62, fy, 58, 16) _wizard_dlg_add_button(dm, 'UpOpBtn', u'Выше', rx + 124, fy, 58, 16) _wizard_dlg_add_button(dm, 'DownOpBtn', u'Ниже', rx + 186, fy, 58, 16) _wizard_dlg_add_button(dm, 'UpdOpBtn', u'Обновить', rx + 248, fy, 70, 16) fy += row_h + 4 _wizard_dlg_add_fixed(dm, 'NonTextLbl', u'Нетекст:', rx, fy, lbl_w, 14) _wizard_dlg_add_combo(dm, 'NonTextCb', ctl_x, fy - 2, min(180, ctl_w), 18) fy += row_h _wizard_dlg_add_fixed(dm, 'OnErrorLbl', u'При ошибке:', rx, fy, lbl_w, 14) _wizard_dlg_add_combo(dm, 'OnErrorCb', ctl_x, fy - 2, min(200, ctl_w), 18) fy += row_h _wizard_dlg_add_checkbox(dm, 'AsValuesChk', u'Как значения (формулы)', rx, fy, right_w, 16) fy += row_h _wizard_dlg_add_fixed(dm, 'KeyLbl', u'Ключ шифра:', rx, fy, lbl_w, 14) _wizard_dlg_add_edit(dm, 'KeyEd', ctl_x, fy - 2, ctl_w, 18) fy += row_h _wizard_dlg_add_fixed(dm, 'KeyCellLbl', u'Ключ ячейка:', rx, fy, lbl_w, 14) _wizard_dlg_add_edit(dm, 'KeyCellEd', ctl_x, fy - 2, ctl_w, 18) fy += row_h + 4 left_bottom = y2 + 36 + 16 btn_y = max(fy, left_bottom + 8) btn_h = _pw_cfg._INNER_BTN_H _wizard_dlg_add_button(dm, 'OkButton', u'OK', m, btn_y, _pw_cfg._INNER_BTN_W, btn_h) _wizard_dlg_add_button( dm, 'CancelButton', u'Отмена', m + _pw_cfg._INNER_BTN_W + _pw_cfg._INNER_BTN_GAP, btn_y, _pw_cfg._INNER_BTN_W, btn_h, ) try: dm.getByName('OkButton').DefaultButton = True except Exception: pass try: dm.getByName('OkButton').PushButtonType = 1 except Exception: pass try: dm.getByName('CancelButton').PushButtonType = 2 except Exception: pass json_y = btn_y + btn_h + 4 _wizard_dlg_add_checkbox(dm, 'JsonChk', u'Сохранять как JSON', m, json_y, 200, _pw_cfg._INNER_CHK_H) dm.Height = json_y + _pw_cfg._INNER_CHK_H + m dlg = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialog', ctx) dlg.setModel(dm) _nested_subdialog_create_peer( dlg, toolkit, doc, parent_dialog, center_in_main_wizard=bool(parent_dialog) ) blocks_lb = dlg.getControl('BlocksLB') ops_lb = dlg.getControl('OpsLB') sheet_ed = dlg.getControl('SheetEd') cols_cb = dlg.getControl('ColsCb') op_type_cb = dlg.getControl('OpTypeCb') case_mode_cb = dlg.getControl('CaseModeCb') row_filter_cb = dlg.getControl('RowFilterCb') row_lambda_ed = dlg.getControl('RowLambdaEd') non_text_cb = dlg.getControl('NonTextCb') on_error_cb = dlg.getControl('OnErrorCb') as_values_chk = dlg.getControl('AsValuesChk') key_ed = dlg.getControl('KeyEd') key_cell_ed = dlg.getControl('KeyCellEd') try: dlg.getControl('JsonChk').setState(1) except Exception: pass form_block_index = [-1] sheet_field_guard = [False] form_loading = [False] form_op_loading = [False] columns_acc = [u''] columns_sep = [u','] columns_pick_guard = [False] columns_known = [[]] # актуальный список заголовков листа op_labels = tuple(lab for _c, lab in _pw_cfg.NORMALIZE_OP_CHOICES) case_labels = tuple(lab for _c, lab in _pw_cfg.NORMALIZE_CASE_MODES) nt_labels = tuple(lab for _c, lab in _pw_cfg.NORMALIZE_NON_TEXT_CHOICES) oe_labels = tuple(lab for _c, lab in _pw_cfg.NORMALIZE_ON_ERROR_CHOICES) rf_labels = tuple(lab for _c, lab in _pw_cfg.NORMALIZE_ROW_FILTER_CHOICES) try: _set_combo_items(op_type_cb, op_labels, op_labels[0] if op_labels else u'') _set_combo_items(case_mode_cb, case_labels, _pw_cfg.NORMALIZE_CASE_LABEL.get(u'proper', u'')) _set_combo_items(non_text_cb, nt_labels, _pw_cfg.NORMALIZE_NON_TEXT_LABEL.get(u'skip', u'')) _set_combo_items(on_error_cb, oe_labels, _pw_cfg.NORMALIZE_ON_ERROR_LABEL.get(u'keep', u'')) _set_combo_items( row_filter_cb, rf_labels, _pw_cfg.NORMALIZE_ROW_FILTER_LABEL.get(u'all', u'') ) except Exception: pass try: hint_lam = getattr(_pw_cfg, 'NORMALIZE_ROW_FILTER_LAMBDA_HINT', u'') or u'' if hint_lam and not unicode(row_lambda_ed.getText() or u'').strip(): row_lambda_ed.setText(hint_lam) except Exception: pass def _set_control_enabled(ctl, enabled): try: ctl.setEnable(bool(enabled)) except Exception: try: ctl.Model.Enabled = bool(enabled) except Exception: pass def _refresh_op_field_enable(): code = _selected_op_code() _set_control_enabled(case_mode_cb, code == u'case') rf = _normalize_code_from_label( _combo_control_text(row_filter_cb), _pw_cfg.NORMALIZE_ROW_FILTER_CODE, u'all', ) if rf not in _pw_cfg.NORMALIZE_ROW_FILTER_LABEL: rf = u'all' _set_control_enabled(row_lambda_ed, rf == u'lambda') def _refresh_blocks(selected_index=0): form_loading[0] = True items = [_normalize_text_block_label(b) for b in norm_blocks] try: blocks_lb.Model.StringItemList = tuple(items) except Exception: pass try: if len(items) > 0: blocks_lb.selectItemPos(max(0, min(selected_index, len(items) - 1)), True) except Exception: pass form_loading[0] = False def _block_index_selected(): try: pos = blocks_lb.getSelectedItemPos() return int(pos) if pos is not None else -1 except Exception: return -1 def _ops_index_selected(): try: pos = ops_lb.getSelectedItemPos() return int(pos) if pos is not None else -1 except Exception: return -1 def _refresh_ops(block_index, select_idx=None): if block_index < 0 or block_index >= len(norm_blocks): try: ops_lb.Model.StringItemList = tuple() except Exception: pass return ops = norm_blocks[block_index].get('ops') or [] items = [_normalize_op_list_label(op, i) for i, op in enumerate(ops)] try: ops_lb.Model.StringItemList = tuple(items) except Exception: pass if select_idx is not None and items: try: ops_lb.selectItemPos(max(0, min(int(select_idx), len(items) - 1)), True) except Exception: pass def _active_sheet_name(): try: if doc is None: return u'' ctrl = doc.getCurrentController() sh = ctrl.getActiveSheet() if ctrl is not None else None return unicode(getattr(sh, 'Name', u'') or u'').strip() except Exception: return u'' def _refresh_column_combo(sheet_name=None): name = unicode(sheet_name or u'').strip() or _active_sheet_name() cols = [] try: cols = _vlookup_list_sheet_columns(doc, name, u'1', u'1') except Exception: cols = [] if not cols: try: for sn in _pivot_list_doc_sheet_names(doc) or []: cols = _vlookup_list_sheet_columns(doc, sn, u'1', u'1') if cols: break except Exception: cols = [] base = list(cols) seen = set() out = [] cur = columns_acc[0] for item in base: s = unicode(item or u'').strip() k = s.casefold() if not s or k in seen: continue seen.add(k) out.append(s) try: cols_cb.Model.StringItemList = tuple(out) except Exception: pass columns_known[0] = list(out) try: columns_pick_guard[0] = True _set_combo_edit_text(cols_cb, cur) except Exception: pass finally: columns_pick_guard[0] = False def _cols_parts(): try: from libre_macros_param_codec import split_column_name_tokens return split_column_name_tokens( columns_acc[0], known_names=tuple(columns_known[0] or ()) ) except Exception: return [p.strip() for p in re.split(u'[,;]', columns_acc[0] or u'') if p.strip()] def _set_cols_acc_text(text, sep=None): columns_acc[0] = unicode(text or u'') if sep in (u',', u';'): columns_sep[0] = sep elif u';' in columns_acc[0]: columns_sep[0] = u';' else: columns_sep[0] = u',' columns_pick_guard[0] = True try: _set_combo_edit_text(cols_cb, columns_acc[0]) finally: columns_pick_guard[0] = False def _set_cols_acc(parts): try: from libre_macros_param_codec import join_column_name_tokens merged = join_column_name_tokens(parts, sep=columns_sep[0]) except Exception: sep = u';' if any(u',' in unicode(p) for p in (parts or [])) else (columns_sep[0] or u',') merged = sep.join(unicode(p).strip() for p in (parts or []) if unicode(p).strip()) if u';' in merged or any(u',' in unicode(p) for p in (parts or [])): columns_sep[0] = u';' elif not parts: columns_sep[0] = u',' _set_cols_acc_text(merged, sep=columns_sep[0]) def _cols_append_token(token): """Накопить столбец; при «,» в имени — разделитель «;» (как переименовать_столбцы).""" token = unicode(token or u'').strip() if token == u'' or columns_pick_guard[0] or form_loading[0]: return False try: from libre_macros_param_codec import append_column_name_token merged, sep = append_column_name_token( columns_acc[0], token, sep=columns_sep[0], known_names=tuple(columns_known[0] or ()), ) except Exception: parts = _cols_parts() if token.casefold() in [p.casefold() for p in parts]: return False parts.append(token) if u',' in token or any(u',' in p for p in parts): columns_sep[0] = u';' merged = (columns_sep[0] or u',').join(parts) sep = columns_sep[0] if merged == columns_acc[0]: # уже было — всё равно восстановить edit после выбора из списка _set_cols_acc_text(merged, sep=sep) return False _set_cols_acc_text(merged, sep=sep) return True def _cols_is_single_dropdown_item(text): cur = unicode(text or u'').strip() if cur == u'': return u'' cur_cf = cur.casefold() for it in columns_known[0] or (): if unicode(it).strip().casefold() == cur_cf: return unicode(it).strip() # имя с запятой целиком = один known — иначе не считаем «выбором из списка» if u',' in cur or u';' in cur: return u'' return u'' def _cols_handle_text(): """textChanged: выбор одного пункта списка → накопить; иначе ручной ввод.""" if columns_pick_guard[0] or form_loading[0]: return cur = _combo_edit_text(cols_cb) picked = _cols_is_single_dropdown_item(cur) if picked: # Combo подставил одно имя вместо всего списка — дописать к накопленному. if picked.casefold() != unicode(columns_acc[0] or u'').casefold(): _cols_append_token(picked) else: _set_cols_acc_text(cur) _sync_params() return # Ручной ввод / уже накопленная строка с , или ; columns_acc[0] = cur if u';' in cur: columns_sep[0] = u';' _sync_params() def _cols_handle_select(): if columns_pick_guard[0] or form_loading[0]: return tok = u'' try: tok = unicode(cols_cb.getSelectedItem() or u'').strip() except Exception: tok = u'' if not tok: try: pos = int(cols_cb.getSelectedItemPos()) items = cols_cb.Model.StringItemList or () if 0 <= pos < len(items): tok = unicode(items[pos]).strip() except Exception: tok = u'' # не принимать накопленную строку как «выбор» if u',' in tok or u';' in tok: if not _cols_is_single_dropdown_item(tok): return if tok: _cols_append_token(tok) _sync_params() def _apply_form_to_block(bi): if bi < 0 or bi >= len(norm_blocks): return b = norm_blocks[bi] _inner_dialog_set_block_sheet(b, _inner_dialog_sheet_text(sheet_ed)) b['columns'] = _cols_parts() b['non_text'] = _normalize_code_from_label( _combo_control_text(non_text_cb), _pw_cfg.NORMALIZE_NON_TEXT_CODE, u'skip' ) if b['non_text'] not in _pw_cfg.NORMALIZE_NON_TEXT_LABEL: b['non_text'] = u'skip' b['on_error'] = _normalize_code_from_label( _combo_control_text(on_error_cb), _pw_cfg.NORMALIZE_ON_ERROR_CODE, u'keep' ) if b['on_error'] not in _pw_cfg.NORMALIZE_ON_ERROR_LABEL: b['on_error'] = u'keep' try: b['as_values'] = bool(as_values_chk.getState()) except Exception: b['as_values'] = True key = unicode(key_ed.getText() or u'').strip() key_cell = unicode(key_cell_ed.getText() or u'').strip() if key: b['key'] = key else: b.pop('key', None) if key_cell: b['key_cell'] = key_cell else: b.pop('key_cell', None) b.pop('key_file', None) def _build_op_from_form(): code = _selected_op_code() if not code: return None op = {u'op': code} if code == u'case': mode_lab = _combo_control_text(case_mode_cb) mode = _normalize_code_from_label(mode_lab, _pw_cfg.NORMALIZE_CASE_CODE, u'proper') if mode not in _pw_cfg.NORMALIZE_CASE_LABEL: mode = u'proper' op[u'mode'] = mode rf = _normalize_code_from_label( _combo_control_text(row_filter_cb), _pw_cfg.NORMALIZE_ROW_FILTER_CODE, u'all', ) if rf not in _pw_cfg.NORMALIZE_ROW_FILTER_LABEL: rf = u'all' if code == u'guid' and rf == u'all': rf = u'empty' op[u'row_filter'] = rf if rf == u'lambda': lam = unicode(row_lambda_ed.getText() or u'').strip() if lam: op[u'row_filter_lambda'] = lam return op def _load_op_to_form(oi): form_op_loading[0] = True try: bi = form_block_index[0] ops = [] if bi >= 0 and bi < len(norm_blocks): ops = norm_blocks[bi].get('ops') or [] if oi < 0 or oi >= len(ops): _refresh_op_field_enable() return op = ops[oi] if not isinstance(op, dict): op = {u'op': op} code = unicode(op.get('op') or u'').strip().casefold() lab = _pw_cfg.NORMALIZE_OP_LABEL.get(code) or code try: _apply_combo_selection(op_type_cb, op_labels, select_name=lab) except Exception: pass mode = unicode(op.get('mode') or u'proper').strip().casefold() or u'proper' mode_lab = _pw_cfg.NORMALIZE_CASE_LABEL.get(mode) or _pw_cfg.NORMALIZE_CASE_LABEL.get(u'proper', u'') try: _apply_combo_selection(case_mode_cb, case_labels, select_name=mode_lab) except Exception: pass rf = unicode(op.get('row_filter') or u'all').strip().casefold() or u'all' try: from libre_macros_normalize_lib import normalize_row_filter rf = normalize_row_filter(rf) except Exception: pass rf_lab = _pw_cfg.NORMALIZE_ROW_FILTER_LABEL.get(rf) or rf_labels[0] try: _apply_combo_selection(row_filter_cb, rf_labels, select_name=rf_lab) except Exception: pass lam = unicode( op.get('row_filter_lambda') or op.get('filter') or op.get('lambda') or u'' ).strip() try: if lam: row_lambda_ed.setText(lam) elif rf == u'lambda': row_lambda_ed.setText( getattr(_pw_cfg, 'NORMALIZE_ROW_FILTER_LAMBDA_HINT', u'') or u'' ) else: row_lambda_ed.setText(u'') except Exception: pass _refresh_op_field_enable() finally: form_op_loading[0] = False def _load_block_to_form(bi): form_loading[0] = True form_block_index[0] = bi if bi < 0 or bi >= len(norm_blocks): _inner_dialog_set_sheet_field_text(sheet_ed, u'', sheet_field_guard) columns_acc[0] = u'' columns_sep[0] = u',' columns_pick_guard[0] = True try: _set_combo_edit_text(cols_cb, u'') finally: columns_pick_guard[0] = False _refresh_ops(-1) form_loading[0] = False return b = norm_blocks[bi] _inner_dialog_set_sheet_field_text( sheet_ed, unicode(b.get('sheet') or u'').strip(), sheet_field_guard ) cols = b.get('columns') or [] if isinstance(cols, (list, tuple)): try: from libre_macros_param_codec import join_column_name_tokens columns_acc[0] = join_column_name_tokens(cols) except Exception: columns_acc[0] = u','.join( unicode(c).strip() for c in cols if unicode(c).strip() ) columns_sep[0] = u';' if u';' in columns_acc[0] else u',' else: columns_acc[0] = unicode(cols or u'') columns_sep[0] = u';' if u';' in columns_acc[0] else u',' _refresh_column_combo(unicode(b.get('sheet') or u'').strip()) columns_pick_guard[0] = True try: _set_combo_edit_text(cols_cb, columns_acc[0]) finally: columns_pick_guard[0] = False _refresh_ops(bi, 0) nt = unicode(b.get('non_text') or u'skip') oe = unicode(b.get('on_error') or u'keep') try: _apply_combo_selection( non_text_cb, nt_labels, select_name=_pw_cfg.NORMALIZE_NON_TEXT_LABEL.get(nt, nt_labels[0]), ) _apply_combo_selection( on_error_cb, oe_labels, select_name=_pw_cfg.NORMALIZE_ON_ERROR_LABEL.get(oe, oe_labels[0]), ) except Exception: pass try: as_values_chk.setState(1 if b.get('as_values', True) else 0) except Exception: pass try: key_ed.setText(unicode(b.get('key') or u'')) key_cell_ed.setText(unicode(b.get('key_cell') or u'')) except Exception: pass form_loading[0] = False _load_op_to_form(0) def _sync_params(): if form_loading[0]: return bi = form_block_index[0] if bi < 0: return _apply_form_to_block(bi) form_loading[0] = True try: _inner_dialog_update_blocks_list_label( blocks_lb, bi, _normalize_text_block_label(norm_blocks[bi]) ) finally: form_loading[0] = False def _selected_op_code(): lab = _combo_control_text(op_type_cb) code = _normalize_code_from_label(lab, _pw_cfg.NORMALIZE_OP_CODE, u'') if code in _pw_cfg.NORMALIZE_OP_LABEL: return code # label may already be code if lab.strip().casefold() in _pw_cfg.NORMALIZE_OP_LABEL: return lab.strip().casefold() return u'' def _add_op(): bi = form_block_index[0] if bi < 0 or bi >= len(norm_blocks): return op = _build_op_from_form() if not op: return ops = list(norm_blocks[bi].get('ops') or []) ops.append(op) norm_blocks[bi]['ops'] = ops _refresh_ops(bi, len(ops) - 1) _load_op_to_form(len(ops) - 1) _sync_params() def _update_op(): bi = form_block_index[0] oi = _ops_index_selected() if bi < 0 or oi < 0: return op = _build_op_from_form() if not op: return ops = list(norm_blocks[bi].get('ops') or []) if oi >= len(ops): return ops[oi] = op norm_blocks[bi]['ops'] = ops _refresh_ops(bi, oi) _load_op_to_form(oi) _sync_params() def _del_op(): bi = form_block_index[0] oi = _ops_index_selected() if bi < 0 or oi < 0: return ops = list(norm_blocks[bi].get('ops') or []) if oi >= len(ops): return ops.pop(oi) norm_blocks[bi]['ops'] = ops sel = min(oi, len(ops) - 1) if ops else None _refresh_ops(bi, sel) if sel is not None: _load_op_to_form(sel) _sync_params() def _move_op(delta): bi = form_block_index[0] oi = _ops_index_selected() if bi < 0 or oi < 0: return ops = list(norm_blocks[bi].get('ops') or []) nj = oi + int(delta) if nj < 0 or nj >= len(ops): return ops[oi], ops[nj] = ops[nj], ops[oi] norm_blocks[bi]['ops'] = ops _refresh_ops(bi, nj) _load_op_to_form(nj) _sync_params() # sheet names try: names = _pivot_list_doc_sheet_names(doc) or [] _set_combo_items(sheet_ed, [u''] + list(names), u'') except Exception: pass class _Handler(unohelper.Base, XActionListener, XItemListener, XTextListener): def disposing(self, ev): pass def actionPerformed(self, ev): try: name = str(ev.Source.getModel().Name) except Exception: return if name == 'AddBlockBtn': _sync_params() norm_blocks.append({ 'v': 1, 'fn': fn_key, 'columns': [], 'ops': [{u'op': u'trim', u'row_filter': u'all'}], 'non_text': u'skip', 'on_error': u'keep', 'as_values': True, }) _refresh_blocks(len(norm_blocks) - 1) _load_block_to_form(len(norm_blocks) - 1) return if name == 'DelBlockBtn': bi = _block_index_selected() if bi < 0 or len(norm_blocks) <= 1: return norm_blocks.pop(bi) ni = min(bi, len(norm_blocks) - 1) _refresh_blocks(ni) _load_block_to_form(ni) return if name == 'AddOpBtn': _add_op() return if name == 'UpdOpBtn': _update_op() return if name == 'DelOpBtn': _del_op() return if name == 'UpOpBtn': _move_op(-1) return if name == 'DownOpBtn': _move_op(1) return if name == 'ColsDelLastBtn': parts = _cols_parts() if parts: parts = parts[:-1] _set_cols_acc(parts) _sync_params() return if name == 'ColsDelAllBtn': _set_cols_acc([]) _sync_params() return if name == 'OkButton': _sync_params() try: dlg.endExecute() except Exception: pass return if name == 'CancelButton': try: dlg.endExecute() except Exception: pass def itemStateChanged(self, ev): if form_loading[0] or form_op_loading[0]: return try: src = ev.Source except Exception: return if _wizard_dlg_is_named_control(src, blocks_lb, u'BlocksLB'): bi = _block_index_selected() prev = form_block_index[0] if prev >= 0 and prev != bi: _apply_form_to_block(prev) _load_block_to_form(bi) return if _wizard_dlg_is_named_control(src, ops_lb, u'OpsLB'): _load_op_to_form(_ops_index_selected()) return if _wizard_dlg_is_named_control(src, cols_cb, u'ColsCb'): _cols_handle_select() return if _wizard_dlg_is_named_control(src, sheet_ed, u'SheetEd'): _sync_params() _refresh_column_combo(_inner_dialog_sheet_text(sheet_ed)) return if _wizard_dlg_is_named_control(src, op_type_cb, u'OpTypeCb'): _refresh_op_field_enable() return if _wizard_dlg_is_named_control(src, row_filter_cb, u'RowFilterCb'): _refresh_op_field_enable() return _sync_params() def textChanged(self, ev): if form_loading[0] or form_op_loading[0]: return try: src = ev.Source except Exception: return if _wizard_dlg_is_named_control(src, sheet_ed, u'SheetEd'): _sync_params() _refresh_column_combo(_inner_dialog_sheet_text(sheet_ed)) return if _wizard_dlg_is_named_control(src, cols_cb, u'ColsCb'): _cols_handle_text() return if src in (key_ed, key_cell_ed): _sync_params() handler = _Handler() for btn in ( 'OkButton', 'CancelButton', 'AddBlockBtn', 'DelBlockBtn', 'AddOpBtn', 'DelOpBtn', 'UpOpBtn', 'DownOpBtn', 'UpdOpBtn', 'ColsDelLastBtn', 'ColsDelAllBtn', ): try: dlg.getControl(btn).addActionListener(handler) except Exception: pass for ctl in ( blocks_lb, ops_lb, cols_cb, sheet_ed, op_type_cb, case_mode_cb, row_filter_cb, non_text_cb, on_error_cb, as_values_chk, ): try: ctl.addItemListener(handler) except Exception: pass for ctl in (sheet_ed, cols_cb, key_ed, key_cell_ed, row_lambda_ed): try: ctl.addTextListener(handler) except Exception: pass _refresh_blocks(0) _load_block_to_form(0) try: dlg.setVisible(True) except Exception: pass try: rc = dlg.execute() except Exception: rc = 0 result = None if rc == 1: _sync_params() try: as_json = bool(dlg.getControl('JsonChk').getState()) except Exception: as_json = True try: result = param_encode(fn_key, norm_blocks) if as_json else param_encode(fn_key, norm_blocks) except Exception: result = param_encode(fn_key, norm_blocks) try: dlg.dispose() except Exception: pass return result def _sort_block_label(block): sheet = unicode(block.get('sheet') or u'').strip() return _inner_block_list_sheet_prefix(sheet) def _sort_key_label(item): if not isinstance(item, dict): return u'' col = unicode(item.get('column') or u'').strip() if col == u'': return u'' desc = bool(item.get('desc')) return u'%s -> %s' % (col, u'-' if desc else u'+') def _show_sort_param_dialog(parent_dialog, initial_text, doc=None): """ Специфичный диалог «сортировка» (приближение к §3.4.2+keys): - слева: список блоков (листы) - справа: ключи выбранного блока - отдельные поля для добавления/редактирования ключа - нижняя строка: OK / Отмена + ☑ JSON """ from libre_macros_param_codec import param_decode, param_encode init = unicode(initial_text or u'').strip() try: blocks = param_decode(u'сортировка', init) except Exception: blocks = [] norm_blocks = [] for b in blocks: if isinstance(b, dict): nb = dict(b) nb['fn'] = u'сортировка' if 'keys' not in nb or not isinstance(nb.get('keys'), list): nb['keys'] = [] norm_blocks.append(nb) if len(norm_blocks) == 0: norm_blocks = [{'v': 1, 'fn': u'сортировка', 'keys': []}] ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) dm = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialogModel', ctx) dw = 560 m = 10 col_gap = 10 left_w = 190 right_w = dw - m * 2 - left_w - col_gap # Подписи «Столбец:» / «Напр.:» шире 70 — иначе наползают на combo. lbl_w = 86 lbl_gap = 6 row_h = 22 list_h = 88 hint_h = 32 dm.PositionX = 120 dm.PositionY = 80 dm.Width = dw dm.Height = 400 dm.Title = u'Сортировка' try: dm.Sizeable = True except Exception: pass y = m _wizard_dlg_add_fixed(dm, 'HintLbl', u'Блоки по листам и ключи сортировки. Столбцы — из списка (накопление через запятую) или вручную. Направление: + (возр.) / - (убыв.).', m, y, dw - m * 2, hint_h, multiline=True) y += hint_h + 2 _wizard_dlg_add_fixed(dm, 'BlocksLbl', u'Листы:', m, y, left_w, 14) _wizard_dlg_add_fixed(dm, 'KeysLbl', u'Ключи:', m + left_w + col_gap, y, right_w, 14) y += 14 rx = m + left_w + col_gap ctl_x = rx + lbl_w + lbl_gap ctl_w = max(80, right_w - lbl_w - lbl_gap) _wizard_dlg_add_listbox(dm, 'BlocksLB', m, y, left_w, list_h, multiselect=False) _wizard_dlg_add_listbox(dm, 'KeysLB', rx, y, right_w, list_h, multiselect=False) y2 = y + list_h + 6 _wizard_dlg_add_fixed(dm, 'SheetLbl', u'Лист (пусто = все листы):', m, y2, left_w, 14) _wizard_dlg_add_combo(dm, 'SheetEd', m, y2 + 14, left_w, 18) _wizard_dlg_add_button(dm, 'AddBlockBtn', u'+ Блок', m, y2 + 36, 80, 16) _wizard_dlg_add_button(dm, 'DelBlockBtn', u'Удалить', m + 90, y2 + 36, 80, 16) fy = y2 _wizard_dlg_add_fixed(dm, 'KeyColLbl', u'Столбец:', rx, fy, lbl_w, 14) _wizard_dlg_add_combo(dm, 'KeyColCb', ctl_x, fy - 2, ctl_w, 18) fy += row_h _wizard_dlg_add_fixed(dm, 'KeyDirLbl', u'Напр.:', rx, fy, lbl_w, 14) _wizard_dlg_add_combo(dm, 'KeyDirCb', ctl_x, fy - 2, min(120, ctl_w), 18) fy += row_h left_block_bottom = y2 + 36 + 16 btn_y = max(fy + 6, left_block_bottom + 4) btn_h = _pw_cfg._INNER_BTN_H _wizard_dlg_add_button(dm, 'OkButton', u'OK', m, btn_y, _pw_cfg._INNER_BTN_W, btn_h) _wizard_dlg_add_button(dm, 'CancelButton', u'Отмена', m + _pw_cfg._INNER_BTN_W + _pw_cfg._INNER_BTN_GAP, btn_y, _pw_cfg._INNER_BTN_W, btn_h) _wizard_dlg_add_button(dm, 'AddKeyBtn', u'+ Ключ', rx, btn_y, 80, btn_h) _wizard_dlg_add_button(dm, 'DelKeyBtn', u'× Ключ', rx + 90, btn_y, 80, btn_h) try: dm.getByName('OkButton').DefaultButton = True except Exception: pass try: dm.getByName('OkButton').PushButtonType = 1 except Exception: pass try: dm.getByName('CancelButton').PushButtonType = 2 except Exception: pass json_y = btn_y + btn_h + 4 _wizard_dlg_add_checkbox(dm, 'JsonChk', u'Сохранять как JSON', m, json_y, 200, _pw_cfg._INNER_CHK_H) dm.Height = json_y + _pw_cfg._INNER_CHK_H + m dlg = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialog', ctx) dlg.setModel(dm) _nested_subdialog_create_peer( dlg, toolkit, doc, parent_dialog, center_in_main_wizard=bool(parent_dialog) ) blocks_lb = dlg.getControl('BlocksLB') keys_lb = dlg.getControl('KeysLB') sheet_ed = dlg.getControl('SheetEd') key_col_cb = dlg.getControl('KeyColCb') key_dir_cb = dlg.getControl('KeyDirCb') try: dlg.getControl('JsonChk').setState(1) except Exception: pass try: _set_combo_items(key_dir_cb, (u'+', u'-'), u'+') except Exception: pass form_block_index = [-1] last_key_index = [-1] sheet_field_guard = [False] form_loading = [False] col_text_prev = [u''] def _refresh_blocks(selected_index=0): form_loading[0] = True items = [_sort_block_label(b) for b in norm_blocks] try: blocks_lb.Model.StringItemList = tuple(items) except Exception: pass try: if len(items) > 0: blocks_lb.selectItemPos(max(0, min(selected_index, len(items) - 1)), True) except Exception: pass form_loading[0] = False def _block_index_selected(): try: pos = blocks_lb.getSelectedItemPos() return int(pos) if pos is not None else -1 except Exception: return -1 def _key_index_selected(): try: pos = keys_lb.getSelectedItemPos() return int(pos) if pos is not None else -1 except Exception: return -1 def _refresh_keys(block_index): if block_index < 0 or block_index >= len(norm_blocks): try: keys_lb.Model.StringItemList = tuple() except Exception: pass return keys = norm_blocks[block_index].get('keys') or [] items = [t for t in (_sort_key_label(k) for k in keys) if t] try: keys_lb.Model.StringItemList = tuple(items) except Exception: pass def _active_sheet_name(): try: if doc is None: return u'' ctrl = doc.getCurrentController() sh = ctrl.getActiveSheet() if ctrl is not None else None return unicode(getattr(sh, 'Name', u'') or u'').strip() except Exception: return u'' def _refresh_column_combo_for_sheet(sheet_name=None): name = unicode(sheet_name or u'').strip() or _active_sheet_name() cols = [] try: cols = _vlookup_list_sheet_columns(doc, name, u'1', u'1') except Exception: cols = [] base = [u''] + cols + [unicode(i) for i in range(1, 51)] base += [unichr(ord('A') + i) for i in range(0, 26)] seen = set() out = [] cur = _combo_edit_text(key_col_cb) for item in base + ([cur] if cur else []): s = unicode(item or u'').strip() k = s.casefold() if k in seen: continue seen.add(k) out.append(s) try: key_col_cb.Model.StringItemList = tuple(out) except Exception: pass _set_combo_edit_text(key_col_cb, cur) col_text_prev[0] = cur def _clear_key_col_field(): form_loading[0] = True try: _set_combo_edit_text(key_col_cb, u'') except Exception: pass form_loading[0] = False col_text_prev[0] = u'' def _sync_sheet_field_to_block(): if sheet_field_guard[0] or form_loading[0]: return bi = form_block_index[0] if bi < 0 or bi >= len(norm_blocks): return sheet = _inner_dialog_sheet_text(sheet_ed) _inner_dialog_set_block_sheet(norm_blocks[bi], sheet) _refresh_column_combo_for_sheet(sheet) _inner_dialog_update_blocks_list_label(blocks_lb, bi, _sort_block_label(norm_blocks[bi])) def _load_block_to_form(block_index, clear_key_fields=True): form_loading[0] = True form_block_index[0] = block_index last_key_index[0] = -1 if block_index < 0 or block_index >= len(norm_blocks): _inner_dialog_set_sheet_field_text(sheet_ed, u'', sheet_field_guard) _refresh_keys(-1) if clear_key_fields: _set_combo_edit_text(key_col_cb, u'') col_text_prev[0] = u'' form_loading[0] = False return b = norm_blocks[block_index] sheet = unicode(b.get('sheet') or u'').strip() _inner_dialog_set_sheet_field_text(sheet_ed, sheet, sheet_field_guard) _refresh_column_combo_for_sheet(sheet) _refresh_keys(block_index) if clear_key_fields: _set_combo_edit_text(key_col_cb, u'') col_text_prev[0] = u'' try: _apply_combo_selection(key_dir_cb, (u'+', u'-'), select_name=u'+') except Exception: pass form_loading[0] = False def _flush_sort_block_from_form(block_index, key_index=None, split_csv=False): if block_index < 0 or block_index >= len(norm_blocks): return sheet = _inner_dialog_sheet_text(sheet_ed) _inner_dialog_set_block_sheet(norm_blocks[block_index], sheet) col_text = _combo_edit_text(key_col_cb) try: dirv = unicode(key_dir_cb.getText() or u'+').strip() except Exception: dirv = u'+' desc = dirv == u'-' keys = list(norm_blocks[block_index].get('keys') or []) ki = _key_index_selected() if key_index is None else key_index if split_csv or ki < 0: tokens = [unicode(t).strip() for t in _vlookup_split_token_list(col_text) if unicode(t).strip()] if len(tokens) == 0: return new_items = [{'column': t, 'desc': desc} for t in tokens] if 0 <= ki < len(keys) and (not split_csv): keys[ki] = new_items[0] j = 1 while j < len(new_items): keys.insert(ki + j, new_items[j]) j += 1 else: keys.extend(new_items) else: col = unicode(col_text or u'').strip() if col == u'': return # Живое редактирование выбранного ключа: один столбец = текст поля целиком # (накопление CSV коммитится кнопкой «+ Ключ»). if u',' in col: return if 0 <= ki < len(keys): keys[ki] = {'column': col, 'desc': desc} else: keys.append({'column': col, 'desc': desc}) norm_blocks[block_index]['keys'] = keys if key_index is None or split_csv: _refresh_keys(block_index) def _update_selected_key_label(bi, ki): keys = norm_blocks[bi].get('keys') or [] if ki < 0 or ki >= len(keys): return try: items = list(keys_lb.Model.StringItemList or ()) if ki < len(items): items[ki] = _sort_key_label(keys[ki]) keys_lb.Model.StringItemList = tuple(items) except Exception: pass def _load_key_fields(bi, ki): if bi < 0 or ki < 0: return keys = norm_blocks[bi].get('keys') or [] if ki >= len(keys): return item = keys[ki] form_loading[0] = True col_disp = unicode(item.get('column') or u'').strip() try: _set_combo_edit_text(key_col_cb, col_disp) except Exception: pass col_text_prev[0] = col_disp try: _apply_combo_selection(key_dir_cb, (u'+', u'-'), select_name=u'-' if item.get('desc') else u'+') except Exception: pass form_loading[0] = False def _switch_sort_block(block_index): prev = form_block_index[0] if prev >= 0 and prev != block_index: pending = _combo_edit_text(key_col_cb) if pending: _flush_sort_block_from_form(prev, key_index=-1, split_csv=True) _inner_dialog_update_blocks_list_label(blocks_lb, prev, _sort_block_label(norm_blocks[prev])) _load_block_to_form(block_index) def _add_sort_block_empty(): prev = form_block_index[0] if prev >= 0: pending = _combo_edit_text(key_col_cb) if pending: _flush_sort_block_from_form(prev, key_index=-1, split_csv=True) _inner_dialog_update_blocks_list_label(blocks_lb, prev, _sort_block_label(norm_blocks[prev])) norm_blocks.append({'v': 1, 'fn': u'сортировка', 'keys': []}) new_idx = len(norm_blocks) - 1 _refresh_blocks(selected_index=new_idx) _load_block_to_form(new_idx) def _apply_sheet_from_form(block_index): if block_index < 0 or block_index >= len(norm_blocks): return _inner_dialog_set_block_sheet(norm_blocks[block_index], _inner_dialog_sheet_text(sheet_ed)) class _SortDialogHandler(unohelper.Base, XActionListener, XItemListener, XTextListener): def disposing(self, ev): pass def textChanged(self, ev): try: src = ev.Source except Exception: return if form_loading[0]: return if _wizard_dlg_is_named_control(src, sheet_ed, u'SheetEd'): _sync_sheet_field_to_block() return if _wizard_dlg_is_named_control(src, key_col_cb, u'KeyColCb'): from_list = False try: pos = key_col_cb.getSelectedItemPos() if pos is not None and int(pos) >= 0: from_list = True except Exception: from_list = False if not from_list: col_text_prev[0] = _combo_edit_text(key_col_cb) if from_list: return bi = _block_index_selected() ki = _key_index_selected() if bi >= 0 and ki >= 0: _flush_sort_block_from_form(bi, ki) _update_selected_key_label(bi, ki) def itemStateChanged(self, ev): try: src = ev.Source except Exception: return if src == blocks_lb: if form_loading[0]: return _switch_sort_block(_block_index_selected()) elif src == keys_lb: if form_loading[0]: return bi = _block_index_selected() prev_ki = last_key_index[0] if bi >= 0: pending = _combo_edit_text(key_col_cb) if u',' in pending: _flush_sort_block_from_form(bi, key_index=-1, split_csv=True) _clear_key_col_field() elif prev_ki >= 0: _flush_sort_block_from_form(bi, prev_ki) last_key_index[0] = _key_index_selected() _load_key_fields(bi, last_key_index[0]) elif src == key_col_cb: if form_loading[0]: return token = u'' try: pos = key_col_cb.getSelectedItemPos() if pos is not None and int(pos) >= 0: items = key_col_cb.Model.StringItemList or () if int(pos) < len(items): token = unicode(items[int(pos)]).strip() except Exception: token = u'' if token == u'' or u',' in token or u';' in token: return merged = _append_csv_token_no_dup(col_text_prev[0], token) form_loading[0] = True try: _set_combo_edit_text(key_col_cb, merged) finally: form_loading[0] = False col_text_prev[0] = merged # Накопление в поле; в ключи — по «+ Ключ» (или один токен при выбранном ключе). bi = _block_index_selected() ki = _key_index_selected() if bi >= 0 and ki >= 0 and u',' not in merged: _flush_sort_block_from_form(bi, ki) _update_selected_key_label(bi, ki) elif src == key_dir_cb: bi = _block_index_selected() ki = _key_index_selected() if bi < 0 or ki < 0: return keys = norm_blocks[bi].get('keys') or [] if ki >= len(keys): return try: dirv = unicode(key_dir_cb.getText() or u'+').strip() except Exception: dirv = u'+' keys[ki] = dict(keys[ki]) keys[ki]['desc'] = dirv == u'-' norm_blocks[bi]['keys'] = keys _update_selected_key_label(bi, ki) def actionPerformed(self, ev): cmd = u'' try: cmd = unicode(ev.ActionCommand) except Exception: pass if cmd == u'AddBlockBtn': _add_sort_block_empty() return if cmd == u'DelBlockBtn': bi = _block_index_selected() if 0 <= bi < len(norm_blocks): del norm_blocks[bi] form_block_index[0] = -1 _refresh_blocks(selected_index=max(0, bi - 1)) _switch_sort_block(_block_index_selected()) return if cmd == u'AddKeyBtn': bi = _block_index_selected() if bi < 0 or bi >= len(norm_blocks): return try: keys_lb.selectItemPos(-1, False) except Exception: pass last_key_index[0] = -1 before = len(norm_blocks[bi].get('keys') or []) _flush_sort_block_from_form(bi, key_index=-1, split_csv=True) after = len(norm_blocks[bi].get('keys') or []) if after > before: try: keys_lb.selectItemPos(after - 1, True) last_key_index[0] = after - 1 except Exception: pass _clear_key_col_field() try: _apply_combo_selection(key_dir_cb, (u'+', u'-'), select_name=u'+') except Exception: pass return if cmd == u'DelKeyBtn': bi = _block_index_selected() ki = _key_index_selected() if bi < 0 or bi >= len(norm_blocks): return keys = norm_blocks[bi].get('keys') or [] if 0 <= ki < len(keys): del keys[ki] norm_blocks[bi]['keys'] = keys last_key_index[0] = -1 _refresh_keys(bi) _clear_key_col_field() return h = _SortDialogHandler() try: blocks_lb.addItemListener(h) keys_lb.addItemListener(h) sheet_ed.addTextListener(h) key_col_cb.addTextListener(h) key_col_cb.addItemListener(h) key_dir_cb.addItemListener(h) except Exception: pass for btn in ('AddBlockBtn', 'DelBlockBtn', 'AddKeyBtn', 'DelKeyBtn', 'OkButton', 'CancelButton'): try: ctl = dlg.getControl(btn) ctl.addActionListener(h) ctl.setActionCommand(btn) except Exception: pass doc_sheet_names = [] try: doc_sheet_names = _pivot_list_doc_sheet_names(doc) except Exception: doc_sheet_names = [] if len(doc_sheet_names) > 0: try: _set_combo_items(sheet_ed, doc_sheet_names, select_text=u'') except Exception: pass _refresh_column_combo_for_sheet(_active_sheet_name()) _refresh_blocks(selected_index=0) _load_block_to_form(0 if len(norm_blocks) > 0 else -1) if dlg.execute() != 1: return None bi = form_block_index[0] if bi < 0: bi = _block_index_selected() if bi < 0 and len(norm_blocks) > 0: bi = 0 if bi >= 0: pending = _combo_edit_text(key_col_cb) if pending: _flush_sort_block_from_form(bi, key_index=-1, split_csv=True) out_blocks = [] for b in norm_blocks: keys = b.get('keys') or [] if len(keys) == 0: continue out_blocks.append(b) return param_encode(u'сортировка', out_blocks) def _new_param_mode_status_text(new_param_mode): mode_name = u'Новый параметр' if new_param_mode else u'Редактирование' return u'Текущий режим %s (нажмите кнопку справа) чтобы переключить' % mode_name def _sync_new_param_mode_ui(btn, lbl, new_param_mode): """Подпись и кнопка переключателя режима редактирования / нового параметра.""" if btn is not None: try: btn.Model.Label = _pw_cfg._MODE_BTN_LABEL_NEW if new_param_mode else _pw_cfg._MODE_BTN_LABEL_EDIT except Exception: pass if lbl is not None: try: lbl.Model.Label = _new_param_mode_status_text(new_param_mode) except Exception: pass def _wizard_dialog_parent(): try: frame = XSCRIPTCONTEXT.getDesktop().getCurrentFrame() if frame is not None: return frame.getContainerWindow() except Exception: pass return None def _global_flag_merge_defaults(): cw = _try_import_collect() if cw is not None: try: return dict(cw.MERGE_FLAG_DEFAULTS) except Exception: pass return {} def _global_flag_rows_from_settings(settings): stored = {} if isinstance(settings, dict): raw = settings.get('merge_flags') or {} if isinstance(raw, dict): stored = dict(raw) defaults = _global_flag_merge_defaults() rows = [] for param_name, global_key in _pw_cfg.GLOBAL_BOOL_FLAG_SPECS: if global_key in stored: enabled = bool(stored[global_key]) else: enabled = bool(defaults.get(global_key, False)) rows.append({'param_name': param_name, 'global_key': global_key, 'enabled': enabled}) return rows def _global_flag_row_label(item): mark = _pw_cfg.GLOBAL_FLAG_MARK_ON if item.get('enabled') else _pw_cfg.GLOBAL_FLAG_MARK_OFF return mark + unicode(item.get('param_name') or u'') def _global_flag_hint_text(param_name): hint = _pw_cfg.WIZARD_HINTS.get(param_name, u'') if not hint: return u'' parts = unicode(hint).split(u'\n') if len(parts) <= 3: return unicode(hint).strip() return u'\n'.join(parts[:3]).strip() def _global_flags_refresh_listbox(dialog, rows, select_idx=0): ctl = _dlg_control(dialog, 'GlobalFlagsListBox') if ctl is None: return labels = [_global_flag_row_label(item) for item in rows] try: ctl.Model.StringItemList = tuple(labels) except Exception: try: ctl.Model.StringItemList = labels except Exception: pass if not rows: return idx = int(select_idx) if idx < 0: idx = 0 if idx >= len(rows): idx = len(rows) - 1 try: ctl.selectItemPos(idx, True) except Exception: pass def _global_flags_selected_index(dialog): ctl = _dlg_control(dialog, 'GlobalFlagsListBox') if ctl is None: return -1 try: return int(ctl.getSelectedItemPos()) except Exception: return -1 def _global_flags_toggle_index(rows, idx): try: i = int(idx) except Exception: return False if i < 0 or i >= len(rows): return False rows[i]['enabled'] = not bool(rows[i].get('enabled')) return True def _global_sync_flag_hint(dialog, rows, idx=None): ctl = _dlg_control(dialog, 'GlobalFlagHintLabel') if ctl is None: return if idx is None: idx = _global_flags_selected_index(dialog) text = u'' try: i = int(idx) if 0 <= i < len(rows): text = _global_flag_hint_text(rows[i].get('param_name')) except Exception: pass try: ctl.setText(text) except Exception: pass def _wizard_dialog_set_visible(dialog, name, visible, geom_cache=None): """ Показ/скрытие контрола вкладки. В AlterOffice setVisible часто no-op — паркуем за экран (как _set_control_visible). geom_cache: dict name -> (y, h, w) после soft-gray; заполняется при первом вызове. """ ctl = _dlg_control(dialog, name) if ctl is None: return try: ctl.setVisible(visible) except Exception: pass try: ctl.setEnable(visible) except Exception: pass try: m = ctl.Model if m is None: return m.Enable = visible if geom_cache is not None: if name not in geom_cache: try: geom_cache[name] = (int(m.PositionY), int(m.Height), int(m.Width)) except Exception: geom_cache[name] = (0, 0, 0) y0, h0, w0 = geom_cache[name] if visible: m.PositionY = y0 m.Height = h0 m.Width = w0 else: m.PositionY = _pw_cfg._HIDDEN_CTRL_Y m.Height = 0 m.Width = 0 except Exception: pass def _vlookup_highlight_color_is_disabled(color_text): cw = _try_import_collect() if cw is not None and hasattr(cw, 'vlookup_highlight_color_is_disabled'): return cw.vlookup_highlight_color_is_disabled(color_text) s = unicode(color_text or u'').strip() if s == u'': return True return s.casefold() == u'нет' def _vlookup_wizard_color_to_highlight(color_ui): """Текст комбобокса → значение цвета для format (None → «Нет» в F).""" s = unicode(color_ui or u'').strip() if s == u'' or s.casefold() == _pw_cfg._VLOOKUP_COLOR_PRESET_EMPTY.casefold(): return None if _vlookup_highlight_color_is_disabled(s): return None return s def _vlookup_wizard_highlight_to_preset(highlight_color): """Разобранный цвет → текст комбобокса.""" if _vlookup_highlight_color_is_disabled(highlight_color): return _pw_cfg._VLOOKUP_COLOR_PRESET_EMPTY return unicode(highlight_color or u'').strip() def _vlookup_extract_not_found_from_f_raw(raw): """Заполнитель из сырой ячейки F (в т.ч. «0»); None — метки нет.""" m = _pw_cfg._VLOOKUP_NOT_FOUND_FROM_F_RE.search(unicode(raw or u'')) if m is None: return None val = m.group(1).strip() if val == u'': return None return val def _vlookup_nf_ctl_set_text(ctl, text): """Записать «Не найдено» в Edit (в LO «0» иногда теряется через setText).""" if ctl is None: return text = unicode(text or u'') try: ctl.Model.Text = text except Exception: pass try: ctl.setText(text) except Exception: pass def _vlookup_nf_ctl_get_text(ctl, dlg=None): """Прочитать «Не найдено» из Edit; при пустом getText — из preview F.""" text = u'' if ctl is not None: try: text = unicode(ctl.getText() or u'').strip() except Exception: pass if text == u'': try: text = unicode(ctl.Model.Text or u'').strip() except Exception: pass if text == u'' and dlg is not None: prev = dlg.getControl('Ed_f_preview') if prev is not None: try: extracted = _vlookup_extract_not_found_from_f_raw(prev.getText()) if extracted is not None: return extracted except Exception: pass if text == u'': return u'#Н/Д' return text def _vlookup_init_not_found_fill(init): """Значение «Не найдено» из init (в т.ч. 0); иначе #Н/Д.""" if init is not None and 'not_found_fill' in init: v = init.get('not_found_fill') if v is None: return u'#Н/Д' if isinstance(v, bool): return u'#Н/Д' if isinstance(v, (int, float)): return unicode(v).strip() return unicode(v).strip() return u'#Н/Д' def _vlookup_coerce_not_found_fill(value, default=u'#Н/Д'): if value is None: return default if isinstance(value, bool): return default if isinstance(value, (int, float)): return unicode(value).strip() s = unicode(value).strip() if s == u'': return default return s def _vlookup_json_int_or_none(val): if val is None: return None s = unicode(val).strip() if s == u'': return None try: return int(s) except (ValueError, TypeError): return None def _vlookup_json_key_tokens(raw): if raw is None: return [] if isinstance(raw, (list, tuple)): return [unicode(k).strip() for k in raw if unicode(k).strip()] s = unicode(raw).strip() if s == u'': return [] return [k.strip() for k in s.replace(u';', u',').split(u',') if k.strip()] def _vlookup_json_extract_columns(raw): return _vlookup_json_key_tokens(raw) def _vlookup_cell_looks_like_json(text): s = unicode(text or u'').strip() return s.startswith(u'[') or s.startswith(u'{') def _vlookup_parse_json_cell_local(text): """Разбор JSON ВПР из колонки C (без collect_workbooks).""" import json s = unicode(text or u'').strip() if s == u'': return (None, u'пустой JSON') if not (s.startswith(u'[') or s.startswith(u'{')): return (None, u'не JSON') try: data = json.loads(s) except Exception as exc: return (None, u'некорректный JSON: %s' % exc) if isinstance(data, dict): items = [data] elif isinstance(data, list): items = data else: return (None, u'ожидается массив или объект') blocks = [] for item in items: if not isinstance(item, dict): return (None, u'элемент JSON должен быть объектом') fn = unicode(item.get('fn') or u'').strip().casefold() if fn not in (u'впр', u'vlookup'): return (None, u'поле fn должно быть «впр»') blocks.append(item) if len(blocks) == 0: return (None, u'пустой массив JSON') return (blocks, None) def _vlookup_json_side_dialog_fields(prefix, side): if not isinstance(side, dict): side = {} def _num_field(key): val = side.get(key) if val is None: return u'' s = unicode(val).strip() return u'' if s == u'' else s return {prefix + 'sheet': unicode(side.get('sheet') or u'').strip(), prefix + 'start_row': _num_field('start_row'), prefix + 'start_col': _num_field('start_col'), prefix + 'header_row': _num_field('header_row')} def _vlookup_json_block_to_dialog_fields_local(block, json_c_raw=None): """JSON-блок §5.2 → начальные значения диалога (локально, без collect).""" if not isinstance(block, dict): return {} keys_same = _vlookup_parse_bool_option(block.get('keys_same'), default=True) keys_left = _vlookup_json_key_tokens(block.get('keys_left')) keys_right = _vlookup_json_key_tokens(block.get('keys_right')) if keys_same or len(keys_right) == 0: keys_right = list(keys_left) extract_cols = _vlookup_json_extract_columns(block.get('extract_columns') or block.get('extract_cols')) nf_raw = block.get('not_found_fill') if nf_raw is None: nf = u'#Н/Д' else: nf = _vlookup_coerce_not_found_fill(nf_raw) hc = block.get('highlight_color') if hc is not None: hc = unicode(hc).strip() if hc == u'' or _vlookup_highlight_color_is_disabled(hc): hc = u'' json_c = unicode(json_c_raw or u'').strip() if json_c == u'': import json json_c = json.dumps([block], ensure_ascii=False, separators=(u',', u':')) fields = {'save_as_json': True, 'keys_same': keys_same, 'keys_left': u','.join(keys_left), 'keys_right': u','.join(keys_right), 'extract_e': u','.join(extract_cols), 'vlookup_color': hc or u'', 'fill_duplicates': _vlookup_parse_bool_option(block.get('fill_duplicates'), default=True), 'multi_match_one_cell': _vlookup_parse_bool_option(block.get('multi_match_one_cell'), default=False), 'not_found_fill': nf, 'trim_keys': _vlookup_parse_bool_option(block.get('trim_keys'), default=False), 'column_suffix': _vlookup_parse_bool_option(block.get('column_suffix'), default=True), 'json_c': json_c} fields.update(_vlookup_json_side_dialog_fields('left_', block.get('left') or {})) fields.update(_vlookup_json_side_dialog_fields('right_', block.get('right') or {})) return fields def _vlookup_read_json_dialog_fields(c_raw, cw=None): """JSON из колонки C → поля диалога; (dict|None, err|None).""" raw = unicode(c_raw or u'').strip() blocks = None err = None if cw is not None and hasattr(cw, 'parse_vlookup_json_cell'): blocks, err = cw.parse_vlookup_json_cell(raw) else: blocks, err = _vlookup_parse_json_cell_local(raw) if err: return (None, err) if not blocks: return (None, u'пустой JSON') block = blocks[0] if cw is not None and hasattr(cw, 'vlookup_json_block_to_dialog_fields'): fields = cw.vlookup_json_block_to_dialog_fields(block) if fields: if not fields.get('json_c'): fields['json_c'] = raw return (fields, None) fields = _vlookup_json_block_to_dialog_fields_local(block, json_c_raw=raw) if fields: return (fields, None) return (None, u'не удалось разобрать JSON ВПР') def _vlookup_pack_json_values(packed): """Собрать JSON ВПР для колонки C (без импорта collect_workbooks при OK).""" import json keys_same = bool(packed.get('keys_same', True)) keys_left = _vlookup_json_key_tokens(packed.get('keys_left') or packed.get('keys_d')) keys_right = _vlookup_json_key_tokens(packed.get('keys_right')) if keys_same or len(keys_right) == 0: keys_right = list(keys_left) def _side(prefix): return {'sheet': unicode(packed.get(prefix + '_sheet') or u'').strip(), 'start_row': _vlookup_json_int_or_none(packed.get(prefix + '_start_row')), 'start_col': _vlookup_json_int_or_none(packed.get(prefix + '_start_col')), 'header_row': _vlookup_json_int_or_none(packed.get(prefix + '_header_row'))} color = packed.get('highlight_color') if color is None: color = packed.get('vlookup_color') if color is not None: color = unicode(color).strip() if color == u'' or _vlookup_highlight_color_is_disabled(color): color = None not_found_fill = packed.get('not_found_fill') if not_found_fill is None: not_found_fill = u'#Н/Д' else: not_found_fill = unicode(not_found_fill) block = {'v': 1, 'fn': u'впр', 'left': _side('left'), 'right': _side('right'), 'keys_same': keys_same, 'keys_left': u','.join(keys_left), 'keys_right': u','.join(keys_right), 'extract_columns': _vlookup_json_extract_columns(packed.get('extract_e')), 'highlight_color': color, 'fill_duplicates': _vlookup_parse_fill_duplicates(packed.get('fill_duplicates'), default=True), 'not_found_fill': not_found_fill, 'multi_match_one_cell': _vlookup_parse_fill_duplicates(packed.get('multi_match_one_cell'), default=False), 'trim_keys': _vlookup_parse_fill_duplicates(packed.get('trim_keys'), default=False)} if not _vlookup_parse_fill_duplicates(packed.get('column_suffix'), default=True): block['column_suffix'] = False return json.dumps([block], ensure_ascii=False, separators=(u',', u':')) def _vlookup_cw_format(label, value): cw = _try_import_collect() if cw is not None and hasattr(cw, 'format_vlookup_labeled_value'): return cw.format_vlookup_labeled_value(label, value) value = unicode(value or u'').strip() if value == u'': return u'' label = unicode(label or u'').strip() if label == u'': return value return u'%s: %s' % (label, value) def _vlookup_cw_format_keys(left_keys, right_keys, keys_same=True): cw = _try_import_collect() lk = unicode(left_keys or u'').strip() rk = unicode(right_keys or u'').strip() if keys_same or rk == u'' or lk == rk: return _vlookup_cw_format(_pw_cfg._VLOOKUP_COL_HEADERS[2], lk) if cw is not None and hasattr(cw, 'format_vlookup_keys_cell'): return cw.format_vlookup_keys_cell([x.strip() for x in lk.replace(u';', u',').split(u',') if x.strip()], [x.strip() for x in rk.replace(u';', u',').split(u',') if x.strip()]) return _vlookup_cw_format(u'Ключ_сравнения_левый', lk) + u' # ' + _vlookup_cw_format(u'Ключ_сравнения_правый', rk) def _ensure_vlookup_param_column_headers(sheet): """Строка 0 — всегда «Значение N»; метки B–F только в ячейках строки ВПР.""" _restore_param_sheet_header_row(sheet, len(_pw_cfg._VLOOKUP_COL_HEADERS)) def _ensure_vlookup_param_column_formats(sheet): """Ширина столбцов B–F: минимум 80 мм; если уже шире — не трогаем.""" if sheet is None: return try: cols = sheet.getColumns() min_w = 8000 col = 1 while col <= 5: try: w = int(cols.getByIndex(col).Width) except Exception: w = min_w if w < min_w: cols.getByIndex(col).Width = int(min_w) col = col + 1 except Exception: pass def _write_vlookup_cell_hints(sheet, row): if sheet is None or row is None: return row = int(row) ci = 0 while ci < len(_pw_cfg._VLOOKUP_CELL_HINTS): _param_write_hint_annotation(sheet.getCellByPosition(ci + 1, row), _pw_cfg._VLOOKUP_CELL_HINTS[ci]) ci = ci + 1 def _vlookup_join_table_params(sheet_name, start_row=u'', start_col=u'', header_row=u''): """Собрать B или C ВПР-строки: Лист|Строка|Столбец|Заголовок.""" parts = [_vlookup_strip_sheet_field(sheet_name)] for v in (start_row, start_col, header_row): s = unicode(v or u'').strip() parts.append(s) return u'|'.join(parts) def _vlookup_strip_sheet_field(text): """Имя листа без подписи «Левая_Таблица:» / «Правая_Таблица:».""" cw = _try_import_collect() if cw is not None and hasattr(cw, 'strip_vlookup_sheet_token'): return unicode(cw.strip_vlookup_sheet_token(text)) s = unicode(text or u'').strip() known = (u'левая_таблица', u'правая_таблица') while s != u'' and u':' in s: label, _sep, rest = s.partition(u':') rest = rest.strip() key = label.strip().casefold().replace(u' ', u'_') if key in known and rest != u'': s = rest continue break return s def _vlookup_split_table_params(param_text): """Разобрать B/C ВПР; возвращает (лист, строка, столбец, заголовок) как строки.""" empty = (u'', u'', u'', u'') if param_text is None or unicode(param_text).strip() == u'': return empty text = unicode(param_text).strip() cw = _try_import_collect() if cw is not None and hasattr(cw, 'strip_vlookup_cell_value'): text = unicode(cw.strip_vlookup_cell_value(text)) else: text = _vlookup_strip_value(text) if cw is not None and hasattr(cw, 'parse_vlookup_table_params'): p = cw.parse_vlookup_table_params(text) def _fmt_num(v): if v is None: return u'' return unicode(v) return (_vlookup_strip_sheet_field(p.get('sheet') or u''), _fmt_num(p.get('start_row')), _fmt_num(p.get('start_col')), _fmt_num(p.get('header_row'))) parts = [x.strip() for x in text.split(u'|')] while len(parts) < 4: parts.append(u'') return (_vlookup_strip_sheet_field(parts[0]), parts[1], parts[2], parts[3]) def _vlookup_strip_value(text): """Снять подпись «Метка: …» (локально + collect_workbooks).""" cw = _try_import_collect() if cw is not None and hasattr(cw, 'strip_vlookup_cell_value'): return unicode(cw.strip_vlookup_cell_value(text)) s = unicode(text or u'').strip() known = set((h.casefold().replace(' ', '_') for h in _pw_cfg._VLOOKUP_COL_HEADERS)) known.add(u'ключ_сравнения_левый') known.add(u'ключ_сравнения_правый') known.add(u'заполнять_дубли') known.add(u'заполнитель_для_не_найдено') known.add(u'мульти_совпадение_в_одну_ячейку') known.add(u'сж_пробелы') while s != u'' and u':' in s: label, _sep, rest = s.partition(u':') rest = rest.strip() key = label.strip().casefold().replace(u' ', u'_') if key in known and rest != u'': if key == u'цвет_добавленных_данных': semi = rest.find(u';') val = rest[:semi].strip() if semi >= 0 else rest.strip() if _vlookup_highlight_color_is_disabled(val): return u'' return val s = rest continue break return s def _vlookup_parse_bool_option(raw, default=True): """Да/Нет для полей F: все синонимы, без учёта регистра (0/1, true/false, …).""" if raw is None: return default if isinstance(raw, bool): return raw if isinstance(raw, (int, float)): try: if float(raw) == 0.0: return False if float(raw) == 1.0: return True except Exception: pass cw = _try_import_collect() if cw is not None and hasattr(cw, 'vlookup_parse_bool_option'): return cw.vlookup_parse_bool_option(raw, default=default) lm = _try_import_lm() if lm is not None and hasattr(lm, 'lm_parse_bool_param'): try: return lm.lm_parse_bool_param(raw, default=default) except ValueError: return default s = unicode(raw).strip().lower() if s == u'': return default if s in (u'0', u'-', u'false', u'no', u'n', u'нет', u'н', u'ложь', u'выкл', u'выключено', u'off'): return False if s in (u'1', u'+', u'true', u'yes', u'y', u'да', u'д', u'истина', u'вкл', u'включено', u'on'): return True return default def _vlookup_parse_f_bool_fields(f_raw): """Bool-поля F напрямую из текста ячейки (метка: значение).""" raw = unicode(f_raw or u'').strip() specs = ((u'fill_duplicates', u'Заполнять_дубли', True), (u'multi_match_one_cell', u'Мульти_совпадение_в_одну_ячейку', False), (u'trim_keys', u'(?:Сж_пробелы|СЖ_ПРОБЕЛЫ|trim_keys)', False), (u'column_suffix', u'Суффикс_столбцов', True)) out = {} si = 0 while si < len(specs): key, label, default = specs[si] m = re.search(label + '\\s*:\\s*([^;,]+)', raw, re.IGNORECASE | re.UNICODE) if m is not None: out[key] = _vlookup_parse_bool_option(m.group(1).strip(), default=default) else: out[key] = default si = si + 1 return out def _vlookup_parse_fill_duplicates(raw, default=True): """Разобрать «Заполнять_дубли» / любой bool-параметр F.""" return _vlookup_parse_bool_option(raw, default=default) def _vlookup_read_row_values(sheet, row): """Прочитать B..F строки ВПР с листа параметров.""" if sheet is None or row is None or int(row) < 1: return {} row = int(row) vals = {} col_names = ('left_b', 'right_c', 'keys_d', 'extract_e', 'color_f') ci = 0 while ci < len(col_names): vals[col_names[ci]] = _cell_string(sheet.getCellByPosition(ci + 1, row)) ci = ci + 1 cw = _try_import_collect() c_raw = vals.get('right_c', u'') looks_json = _vlookup_cell_looks_like_json(c_raw) if looks_json or (cw is not None and hasattr(cw, 'vlookup_is_json_cell') and cw.vlookup_is_json_cell(c_raw)): fields, jerr = _vlookup_read_json_dialog_fields(c_raw, cw) if fields: return fields return {'save_as_json': True, 'json_c': unicode(c_raw).strip(), '_json_parse_error': jerr or u'ошибка разбора JSON'} left = _vlookup_split_table_params(vals.get('left_b')) right = _vlookup_split_table_params(vals.get('right_c')) vals['left_sheet'] = _vlookup_strip_sheet_field(left[0]) vals['left_start_row'] = left[1] vals['left_start_col'] = left[2] vals['left_header_row'] = left[3] vals['right_sheet'] = _vlookup_strip_sheet_field(right[0]) vals['right_start_row'] = right[1] vals['right_start_col'] = right[2] vals['right_header_row'] = right[3] keys_raw = vals.get('keys_d', u'') if cw is not None and hasattr(cw, 'parse_vlookup_keys_cell'): lk, rk = cw.parse_vlookup_keys_cell(keys_raw) vals['keys_left'] = u','.join(lk) vals['keys_right'] = u','.join(rk) vals['keys_same'] = lk == rk else: vals['keys_left'] = _vlookup_strip_value(keys_raw) vals['keys_right'] = vals['keys_left'] vals['keys_same'] = True vals['extract_e'] = _vlookup_strip_value(vals.get('extract_e')) vals['f_cell_raw'] = unicode(vals.get('color_f') or u'').strip() f_bools = _vlookup_parse_f_bool_fields(vals['f_cell_raw']) cw_opts = _try_import_collect() if cw_opts is not None and hasattr(cw_opts, 'parse_vlookup_options_cell'): opts = cw_opts.parse_vlookup_options_cell(vals['f_cell_raw']) vals['vlookup_color'] = _vlookup_wizard_highlight_to_preset(opts.get('highlight_color')) vals['fill_duplicates'] = f_bools['fill_duplicates'] vals['multi_match_one_cell'] = f_bools['multi_match_one_cell'] vals['trim_keys'] = f_bools.get('trim_keys', opts.get('trim_keys', False)) vals['column_suffix'] = f_bools.get('column_suffix', opts.get('column_suffix', True)) nf_from_raw = _vlookup_extract_not_found_from_f_raw(vals['f_cell_raw']) if nf_from_raw is not None: vals['not_found_fill'] = _vlookup_coerce_not_found_fill(nf_from_raw) else: vals['not_found_fill'] = _vlookup_coerce_not_found_fill(opts.get('not_found_fill')) else: nf_from_raw = _vlookup_extract_not_found_from_f_raw(vals['f_cell_raw']) if nf_from_raw is not None: vals['not_found_fill'] = _vlookup_coerce_not_found_fill(nf_from_raw) else: vals['not_found_fill'] = u'#Н/Д' vals['vlookup_color'] = _vlookup_strip_value(vals.get('color_f')) vals['fill_duplicates'] = f_bools['fill_duplicates'] vals['multi_match_one_cell'] = f_bools['multi_match_one_cell'] vals['trim_keys'] = f_bools.get('trim_keys', False) vals['column_suffix'] = f_bools.get('column_suffix', True) return vals def _vlookup_pack_row_values(vals): """Словарь полей формы → dict для записи B..F (с подписями-метками).""" left_pipe = _vlookup_join_table_params(_vlookup_strip_sheet_field(vals.get('left_sheet')), vals.get('left_start_row'), vals.get('left_start_col'), vals.get('left_header_row')) right_pipe = _vlookup_join_table_params(_vlookup_strip_sheet_field(vals.get('right_sheet')), vals.get('right_start_row'), vals.get('right_start_col'), vals.get('right_header_row')) keys_same = bool(vals.get('keys_same', True)) keys_left = _vlookup_strip_value(vals.get('keys_left') or vals.get('keys_d') or u'') keys_right = _vlookup_strip_value(vals.get('keys_right') or keys_left) if keys_same: keys_right = keys_left extract_val = _vlookup_strip_value(vals.get('extract_e')) f_cell = unicode(vals.get('f_cell') or u'').strip() if f_cell == u'': cw_pack = _try_import_collect() color_val = _vlookup_wizard_color_to_highlight(vals.get('vlookup_color') or vals.get('color_f')) fill_duplicates = _vlookup_parse_fill_duplicates(vals.get('fill_duplicates'), default=True) multi_match_one_cell = _vlookup_parse_fill_duplicates(vals.get('multi_match_one_cell'), default=False) trim_keys = _vlookup_parse_fill_duplicates(vals.get('trim_keys'), default=False) column_suffix = _vlookup_parse_fill_duplicates(vals.get('column_suffix'), default=True) not_found_fill = _vlookup_coerce_not_found_fill(vals.get('not_found_fill')) cw_nf = _try_import_collect() if cw_nf is not None and hasattr(cw_nf, 'vlookup_not_found_fill_is_empty_cell'): if cw_nf.vlookup_not_found_fill_is_empty_cell(not_found_fill): not_found_fill = unicode(cw_nf.VLOOKUP_NOT_FOUND_EMPTY) if cw_pack is not None and hasattr(cw_pack, 'format_vlookup_options_cell'): f_cell = unicode(cw_pack.format_vlookup_options_cell(color_val, fill_duplicates, not_found_fill, multi_match_one_cell, trim_keys, column_suffix)) else: f_cell = _vlookup_cw_format(_pw_cfg._VLOOKUP_COL_HEADERS[4], color_val) return {'left_b': _vlookup_cw_format(_pw_cfg._VLOOKUP_COL_HEADERS[0], left_pipe), 'right_c': _vlookup_cw_format(_pw_cfg._VLOOKUP_COL_HEADERS[1], right_pipe), 'keys_d': _vlookup_cw_format_keys(keys_left, keys_right, keys_same), 'extract_e': _vlookup_cw_format(_pw_cfg._VLOOKUP_COL_HEADERS[3], extract_val), 'color_f': f_cell} def _vlookup_parse_f_options(f_raw): """Разобрать строку ячейки F для контролов диалога.""" raw = unicode(f_raw or u'').strip() bools = _vlookup_parse_f_bool_fields(raw) cw = _try_import_collect() nf_from_raw = _vlookup_extract_not_found_from_f_raw(raw) if cw is not None and hasattr(cw, 'parse_vlookup_options_cell'): opts = cw.parse_vlookup_options_cell(raw) if nf_from_raw is not None: nf_val = _vlookup_coerce_not_found_fill(nf_from_raw) else: nf_val = _vlookup_coerce_not_found_fill(opts.get('not_found_fill')) return {'highlight_color': unicode(opts.get('highlight_color') or u'').strip(), 'fill_duplicates': bools['fill_duplicates'], 'multi_match_one_cell': bools['multi_match_one_cell'], 'trim_keys': bools.get('trim_keys', opts.get('trim_keys', False)), 'column_suffix': bools.get('column_suffix', opts.get('column_suffix', True)), 'not_found_fill': nf_val} if nf_from_raw is not None: nf_val = _vlookup_coerce_not_found_fill(nf_from_raw) else: nf_val = u'#Н/Д' return {'highlight_color': _vlookup_strip_value(raw) or u'', 'fill_duplicates': bools['fill_duplicates'], 'multi_match_one_cell': bools['multi_match_one_cell'], 'trim_keys': bools.get('trim_keys', False), 'column_suffix': bools.get('column_suffix', True), 'not_found_fill': nf_val} def _vlookup_build_f_cell_text(color, fill_duplicates, not_found_fill, multi_match_one_cell=False, trim_keys=False, column_suffix=True): """Каноническая сборка строки для ячейки F.""" color_val = _vlookup_wizard_color_to_highlight(color) nf = _vlookup_coerce_not_found_fill(not_found_fill) fill_dup = _vlookup_parse_fill_duplicates(fill_duplicates, default=True) multi = _vlookup_parse_fill_duplicates(multi_match_one_cell, default=False) trim = _vlookup_parse_fill_duplicates(trim_keys, default=False) suffix = _vlookup_parse_fill_duplicates(column_suffix, default=True) cw = _try_import_collect() if cw is not None and hasattr(cw, 'format_vlookup_options_cell'): return unicode(cw.format_vlookup_options_cell(color_val, fill_dup, nf, multi, trim, suffix)) fill_lbl = u'Да' if fill_dup else u'Нет' multi_lbl = u'Да' if multi else u'Нет' color_lbl = unicode(cw.VLOOKUP_HIGHLIGHT_COLOR_NONE) if cw is not None and hasattr(cw, 'VLOOKUP_HIGHLIGHT_COLOR_NONE') and (color_val is None) else color_val if color_val is not None else u'Нет' return u'Цвет_добавленных_данных: %s; Заполнять_дубли: %s; Мульти_совпадение_в_одну_ячейку: %s; Сж_пробелы: %s; Суффикс_столбцов: %s; Заполнитель_для_не_найдено: %s' % (color_lbl, fill_lbl, multi_lbl, u'Да' if trim else u'Нет', u'Да' if suffix else u'Нет', nf) def _vlookup_dialog_collect_f_controls(dlg, result): """Считать контролы секции F (цвет, заполнитель, дубли).""" color = u'' nf = u'#Н/Д' preset_ctl = dlg.getControl('ColorPreset') if preset_ctl is not None: try: color = preset_ctl.getText().strip() except Exception: pass nf = _vlookup_nf_ctl_get_text(dlg.getControl('Ed_not_found_fill'), dlg) return (color, result.get('fill_duplicates', True), nf, result.get('multi_match_one_cell', False), result.get('trim_keys', False), result.get('column_suffix', True)) def _vlookup_dialog_set_f_preview(dlg, text): ctl = dlg.getControl('Ed_f_preview') if ctl is not None: try: ctl.setText(unicode(text or u'')) except Exception: pass def _vlookup_dialog_rebuild_f_from_controls(dlg, result): """Пересобрать disabled-поле F из контролов.""" if result.get('_f_block_rebuild'): return color, fill_dup, nf, multi, trim, suffix = _vlookup_dialog_collect_f_controls(dlg, result) _vlookup_dialog_set_f_preview(dlg, _vlookup_build_f_cell_text(color, fill_dup, nf, multi, trim, suffix)) def _vlookup_dialog_set_bool_btn(dlg, btn_name, checked, label_on, label_off): btn = dlg.getControl(btn_name) if btn is None: return try: btn.Model.Label = label_on if checked else label_off except Exception: pass def _vlookup_dialog_apply_f_to_controls(dlg, result, f_raw): """Загрузить ячейку F с листа: preview = как на листе, контролы — из парсинга.""" result['_f_block_rebuild'] = True try: raw = unicode(f_raw or u'').strip() opts = _vlookup_parse_f_options(raw) _vlookup_dialog_set_f_preview(dlg, raw) result['fill_duplicates'] = opts['fill_duplicates'] result['multi_match_one_cell'] = opts['multi_match_one_cell'] result['trim_keys'] = opts.get('trim_keys', False) result['column_suffix'] = opts.get('column_suffix', True) preset_ctl = dlg.getControl('ColorPreset') if preset_ctl is not None: try: preset_ctl.setText(_vlookup_wizard_highlight_to_preset(opts.get('highlight_color'))) except Exception: pass nf_ctl = dlg.getControl('Ed_not_found_fill') nf_val = _vlookup_extract_not_found_from_f_raw(raw) if nf_val is None: nf_val = opts.get('not_found_fill') _vlookup_nf_ctl_set_text(nf_ctl, _vlookup_coerce_not_found_fill(nf_val)) btn = dlg.getControl('ChkFillDup') if btn is not None: _vlookup_dialog_set_bool_btn(dlg, 'ChkFillDup', result['fill_duplicates'], u'☑ Дубли', u'☐ Дубли') btn_multi = dlg.getControl('ChkMultiMatch') if btn_multi is not None: _vlookup_dialog_set_bool_btn(dlg, 'ChkMultiMatch', result['multi_match_one_cell'], u'☑ Мульти', u'☐ Мульти') btn_trim = dlg.getControl('ChkTrimKeys') if btn_trim is not None: _vlookup_dialog_set_bool_btn(dlg, 'ChkTrimKeys', result.get('trim_keys', False), u'☑ СЖ_ПРОБЕЛЫ', u'☐ СЖ_ПРОБЕЛЫ') btn_suffix = dlg.getControl('ChkColumnSuffix') if btn_suffix is not None: _vlookup_dialog_set_bool_btn(dlg, 'ChkColumnSuffix', result.get('column_suffix', True), u'☑ Суффикс _R', u'☐ Суффикс _R') finally: result['_f_block_rebuild'] = False def _vlookup_dialog_parent(doc=None, parent_dialog=None): """Родитель для вложенного диалога ВПР: окно документа, не модальный визард.""" parent = _wp_doc_parent_window(doc) if parent is not None: return parent parent = _wizard_dialog_parent() if parent is not None: return parent if parent_dialog is not None: try: peer = parent_dialog.getPeer() if peer is not None: return peer except Exception: pass return None def _wizard_toolkit_work_area(toolkit=None): """Рабочая область экрана (x, y, width, height) или None.""" wa = None if toolkit is not None: for meth in ('getWorkArea', 'getDisplayWorkArea'): if not hasattr(toolkit, meth): continue try: wa = getattr(toolkit, meth)() if wa is not None: break except Exception: wa = None if wa is None: try: ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() tk = sm.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) if tk is not None and hasattr(tk, 'getWorkArea'): wa = tk.getWorkArea() except Exception: wa = None if wa is None: return None try: x = int(getattr(wa, 'X', 0) or 0) y = int(getattr(wa, 'Y', 0) or 0) w = int(getattr(wa, 'Width', 0) or 0) h = int(getattr(wa, 'Height', 0) or 0) except Exception: return None if w <= 0 or h <= 0: return None return (x, y, w, h) def _nested_subdialog_peer_fill_workarea(dlg, toolkit): """После createPeer — растянуть окно диалога на рабочую область.""" wa = _wizard_toolkit_work_area(toolkit) if wa is None or dlg is None: return False wx, wy, ww, wh = wa try: from com.sun.star.awt.PosSize import POSSIZE except Exception: POSSIZE = 15 try: peer = dlg.getPeer() if peer is not None: peer.setPosSize(wx, wy, ww, wh, POSSIZE) return True except Exception: pass return False def _nested_subdialog_create_peer( dlg, toolkit, doc=None, parent_dialog=None, center_in_main_wizard=False ): """ createPeer для поддиалога (ВПР, сводная) поверх модального визарда. center_in_main_wizard: из основного визарда — только развернуть окно на экран (без сдвига контролов); из ячейки C — компактное окно как раньше. """ if dlg is None or toolkit is None: return False try: from libre_macros_ui_theme import prepare_dialog_soft_gray prepare_dialog_soft_gray(dlg) except Exception: pass if center_in_main_wizard and parent_dialog is not None: try: dm = dlg.getModel() _inner_dialog_set_sizeable(dm) except Exception: pass parents = [] parent_doc = _wp_doc_parent_window(doc) if parent_doc is not None: parents.append(parent_doc) parent_desk = _wizard_dialog_parent() if parent_desk is not None and parent_desk not in parents: parents.append(parent_desk) if parent_dialog is not None: try: peer = parent_dialog.getPeer() if peer is not None and peer not in parents: parents.append(peer) except Exception: pass parents.append(None) i = 0 ok = False while i < len(parents): try: dlg.createPeer(toolkit, parents[i]) ok = True break except Exception: pass i = i + 1 if ok: if center_in_main_wizard and parent_dialog is not None: try: _nested_subdialog_peer_fill_workarea(dlg, toolkit) except Exception: pass try: _wizard_try_paint_titlebar(dlg) except Exception: pass return ok def _vlookup_split_token_list(text): out = [] for p in re.split('[,;]', unicode(text or u'')): p = p.strip() if p != u'': out.append(p) return out def _vlookup_join_token_list(tokens): parts = [] for t in tokens or []: s = unicode(t or u'').strip() if s != u'': parts.append(s) return u','.join(parts) def _vlookup_remove_last_token(text): tokens = _vlookup_split_token_list(text) if len(tokens) == 0: return u'' return _vlookup_join_token_list(tokens[:-1]) def _vlookup_append_token(text, new_token): token = unicode(new_token or u'').strip() if token == u'': return unicode(text or u'') tokens = _vlookup_split_token_list(text) tokens.append(token) return _vlookup_join_token_list(tokens) def _combo_edit_text(control): """Текст редактируемого ComboBox (ввод пользователя), без подмены выбранным пунктом списка.""" if control is None: return u'' try: t = control.getText() if t is not None: return unicode(t).strip() except Exception: pass try: t = control.Model.Text if t is not None: return unicode(t).strip() except Exception: pass return u'' def _append_csv_token_no_dup(text, new_token): """Добавить токен; без дублей. «Все»/* — заменяет список. Разделитель , или ;.""" token = unicode(new_token or u'').strip() if token == u'': return unicode(text or u'').strip() if u',' in token or u';' in token: return unicode(text or u'').strip() token_cf = token.casefold() if token_cf in (u'all', u'все', u'*'): return u'Все' try: from libre_macros_param_codec import split_column_name_tokens, join_column_name_tokens tokens = split_column_name_tokens(text) join_fn = join_column_name_tokens except Exception: tokens = _vlookup_split_token_list(text) join_fn = _vlookup_join_token_list if len(tokens) == 1 and tokens[0].casefold() in (u'all', u'все', u'*'): tokens = [] for t in tokens: if unicode(t).strip().casefold() == token_cf: return join_fn(tokens) tokens.append(token) return join_fn(tokens) def _set_combo_edit_text(control, text): """Записать текст в ComboBox и сбросить индекс выбранного пункта списка.""" if control is None: return val = unicode(text or u'') try: control.selectItemPos(-1, False) except Exception: pass try: control.setText(val) except Exception: pass try: control.Model.Text = val except Exception: pass def _vlookup_dialog_ctl_text(dlg, name): ctl = dlg.getControl(name) if ctl is None: return u'' try: return unicode(ctl.getText() or u'').strip() except Exception: return u'' def _vlookup_list_sheet_columns(doc, sheet_name, header_row=u'', start_col=u''): sheet = _get_sheet_by_name(doc, _vlookup_strip_sheet_field(sheet_name)) if sheet is None: return [] hri = 0 if unicode(header_row or u'').strip().isdigit(): hri = max(int(header_row) - 1, 0) col0 = 0 if unicode(start_col or u'').strip().isdigit(): col0 = max(int(start_col) - 1, 0) names = [] seen = set() col = col0 empty_run = 0 while col < col0 + 256: try: t = unicode(_cell_string(sheet.getCellByPosition(col, hri))).strip() except Exception: t = u'' if t == u'': empty_run += 1 if empty_run > 8 and col > col0 + 2: break else: empty_run = 0 k = t.casefold() if k not in seen: seen.add(k) names.append(t) col += 1 return names def _vlookup_intersect_column_names(left_names, right_names): right_keys = set() for n in right_names or []: k = unicode(n or u'').strip().casefold() if k != u'': right_keys.add(k) out = [] seen = set() for n in left_names or []: k = unicode(n or u'').strip().casefold() if k != u'' and k in right_keys and (k not in seen): seen.add(k) out.append(unicode(n).strip()) return out def _vlookup_dialog_refresh_token_combos(dlg, doc, keys_same): left_cols = _vlookup_list_sheet_columns(doc, _vlookup_dialog_ctl_text(dlg, 'Ed_left_sheet'), _vlookup_dialog_ctl_text(dlg, 'Ed_left_header_row'), _vlookup_dialog_ctl_text(dlg, 'Ed_left_start_col')) right_cols = _vlookup_list_sheet_columns(doc, _vlookup_dialog_ctl_text(dlg, 'Ed_right_sheet'), _vlookup_dialog_ctl_text(dlg, 'Ed_right_header_row'), _vlookup_dialog_ctl_text(dlg, 'Ed_right_start_col')) if keys_same: keys_items = _vlookup_intersect_column_names(left_cols, right_cols) else: keys_items = left_cols for ctl_name, items in (('Cb_keys_left', keys_items), ('Cb_keys_right', right_cols if not keys_same else ()), ('Cb_extract_e', right_cols)): ctl = dlg.getControl(ctl_name) if ctl is None: continue try: cur = unicode(ctl.getText() or u'').strip() except Exception: cur = u'' _set_combo_items(ctl, items, select_text=cur if cur in items else None) def _vlookup_dialog_add_token_row(dm, label, lbl_id, edit_id, combo_id, del_btn_id, add_btn_id, x, row_y, label_w, total_w): rx = x + label_w + 4 edit_w = int(total_w * 0.6) btn_w = 22 gap = 4 combo_w = max(48, total_w - edit_w - 2 * btn_w - 3 * gap) _wizard_dlg_add_fixed(dm, lbl_id, label, x, row_y, label_w, 14) _wizard_dlg_add_edit(dm, edit_id, rx, row_y - 2, edit_w, 16) _wizard_dlg_add_button(dm, del_btn_id, u'×', rx + edit_w + gap, row_y - 2, btn_w, 16) _wizard_dlg_add_combo(dm, combo_id, rx + edit_w + gap + btn_w + gap, row_y - 2, combo_w, 16) _wizard_dlg_add_button(dm, add_btn_id, u'+', rx + edit_w + gap + btn_w + gap + combo_w + gap, row_y - 2, btn_w, 16) def _merge_sheets_parse_initial(initial): """initial (JSON C или dict) → dict полей диалога объединения.""" out = { u"sheets": u"Отчет_Q1,Отчет_Q2,Отчет_Q3", u"columns": u"", u"dest_sheet": u"Отчет_Q_All", u"header_row": u"1", u"with_formatting": False, u"auto_format": True, } if initial is None: return out if isinstance(initial, dict) and u"json_c" not in initial and u"sheets" in initial: sheets = initial.get(u"sheets") if isinstance(sheets, (list, tuple)): out[u"sheets"] = u",".join(unicode(x) for x in sheets if unicode(x).strip()) elif sheets is not None: out[u"sheets"] = unicode(sheets).strip() cols = initial.get(u"columns") if isinstance(cols, (list, tuple)): out[u"columns"] = u",".join( unicode(x) for x in cols if unicode(x).strip() ) elif cols not in (None, u""): out[u"columns"] = unicode(cols).strip() if initial.get(u"dest_sheet"): out[u"dest_sheet"] = unicode(initial.get(u"dest_sheet")).strip() if initial.get(u"header_row") not in (None, u""): out[u"header_row"] = unicode(initial.get(u"header_row")).strip() out[u"with_formatting"] = bool(initial.get(u"with_formatting")) if u"auto_format" in initial: out[u"auto_format"] = bool(initial.get(u"auto_format")) elif u"автоформат" in initial: out[u"auto_format"] = bool(initial.get(u"автоформат")) return out text = unicode(initial or u"").strip() if text == u"": return out try: from libre_macros_param_codec import param_decode blocks = param_decode(u"объединить_листы_в_один", text) or [] if blocks and isinstance(blocks[0], dict): return _merge_sheets_parse_initial(blocks[0]) except Exception: pass return out def _merge_combo_set_list_keep_text(control, items, text): """Обновить выпадающий список ComboBox, сохранив текст поля.""" if control is None: return keep = unicode(text or u"") try: control.Model.StringItemList = tuple( [unicode(x) for x in (items or []) if unicode(x).strip()] ) except Exception: pass try: control.setText(keep) except Exception: try: control.Model.Text = keep except Exception: pass def _merge_list_columns_for_sheets(doc, sheets_text, header_row): """Объединение имён столбцов по листам из CSV-списка (без дублей).""" names = [] seen = set() for sheet_name in _vlookup_split_token_list(sheets_text): for col in _vlookup_list_sheet_columns(doc, sheet_name, header_row): k = unicode(col or u"").strip().lower() if k == u"" or k in seen: continue seen.add(k) names.append(unicode(col).strip()) return names def show_merge_sheets_param_dialog(parent_dialog=None, initial=None, doc=None): """ Диалог «объединить_листы_в_один» (как ВПР — отдельный mode, JSON в C). Листы и столбцы — ComboBox с append по выбору; кнопки x / xx. Возвращает {"json_c": "..."} или None при отмене. """ init = _merge_sheets_parse_initial(initial) try: from com.sun.star.awt.PushButtonType import OK as BTN_OK from com.sun.star.awt.PushButtonType import CANCEL as BTN_CANCEL except Exception: BTN_OK = 1 BTN_CANCEL = 2 try: ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext("com.sun.star.awt.Toolkit", ctx) dm = sm.createInstanceWithContext( "com.sun.star.awt.UnoControlDialogModel", ctx ) dw = 460 m = 10 lw = 130 btn_x_w = 22 btn_xx_w = 28 gap = 3 combo_w = dw - m * 2 - lw - 4 - btn_x_w - gap - btn_xx_w - gap row_h = 24 y = 12 dm.PositionX = 80 dm.PositionY = 60 dm.Width = dw dm.Title = u"Объединить листы в один" _wizard_dlg_add_fixed( dm, u"LblHint", u"Режет пайплайн до/после (как ВПР). Результат — один лист.", m, y, dw - 2 * m, 28, ) y = y + 32 def _add_csv_combo_row(lbl, lbl_id, cb_id, x_id, xx_id, row_y): _wizard_dlg_add_fixed(dm, lbl_id, lbl, m, row_y + 2, lw, 14) cx = m + lw + 4 _wizard_dlg_add_combo(dm, cb_id, cx, row_y, combo_w, 18) _wizard_dlg_add_button( dm, x_id, u"x", cx + combo_w + gap, row_y, btn_x_w, 18 ) _wizard_dlg_add_button( dm, xx_id, u"xx", cx + combo_w + gap + btn_x_w + gap, row_y, btn_xx_w, 18, ) _add_csv_combo_row( u"Листы (через ,)", u"Lbl_sheets", u"Cb_sheets", u"Btn_sheets_x", u"Btn_sheets_xx", y, ) y = y + row_h _add_csv_combo_row( u"Имена столбцов", u"Lbl_cols", u"Cb_cols", u"Btn_cols_x", u"Btn_cols_xx", y, ) y = y + row_h _wizard_dlg_add_fixed( dm, u"Lbl_dest", u"Лист результата", m, y + 2, lw, 14 ) _wizard_dlg_add_edit(dm, u"Ed_dest", m + lw + 4, y, combo_w, 18) y = y + row_h _wizard_dlg_add_fixed( dm, u"Lbl_hdr", u"Строка заголовков", m, y + 2, lw, 14 ) _wizard_dlg_add_edit(dm, u"Ed_hdr", m + lw + 4, y, 80, 18) y = y + row_h _wizard_dlg_add_checkbox( dm, u"CbFmt", u"С форматированием (стили со источников)", m, y, dw - 2 * m, 16, ) y = y + row_h _wizard_dlg_add_checkbox( dm, u"CbAutoFmt", u"Автоформат (оформление по умолчанию)", m, y, dw - 2 * m, 16, ) y = y + row_h + 8 btn_ok = _wizard_dlg_add_button(dm, u"BtnOk", u"OK", dw - m - 140, y, 64, 24) btn_cancel = _wizard_dlg_add_button( dm, u"BtnCancel", u"Отмена", dw - m - 70, y, 64, 24 ) try: btn_ok.PushButtonType = BTN_OK btn_cancel.PushButtonType = BTN_CANCEL btn_ok.DefaultButton = True except Exception: pass dm.Height = y + 36 dlg = sm.createInstanceWithContext("com.sun.star.awt.UnoControlDialog", ctx) dlg.setModel(dm) dlg.setVisible(False) if not _nested_subdialog_create_peer( dlg, toolkit, doc, parent_dialog, center_in_main_wizard=bool(parent_dialog) ): try: dlg.createPeer(toolkit, None) except Exception: pass sheets_cb = dlg.getControl(u"Cb_sheets") cols_cb = dlg.getControl(u"Cb_cols") hdr_ed = dlg.getControl(u"Ed_hdr") # Скрытые CSV-аккумуляторы — источник истины (комбо при выборе затирает Text). sheets_acc = [unicode(init.get(u"sheets") or u"").strip()] cols_acc = [unicode(init.get(u"columns") or u"").strip()] sheets_items = [] cols_items = [] loading = [False] def _ctl_enabled(ctl, enabled): if ctl is None: return try: ctl.Model.Enabled = bool(enabled) except Exception: pass def _sync_combo_from_acc(control, acc_text, items): """Записать аккумулятор в комбо, список пунктов сохранить.""" if control is None: return was_loading = loading[0] loading[0] = True try: try: control.Model.StringItemList = tuple( [unicode(x) for x in (items or []) if unicode(x).strip()] ) except Exception: pass _set_combo_edit_text(control, acc_text) finally: loading[0] = was_loading def _dropdown_token(control, items, ev=None): """Имя выбранного пункта списка (не Text поля).""" pos = -1 if ev is not None: try: pos = int(ev.Selected) except Exception: pos = -1 if pos < 0: try: p = control.getSelectedItemPos() if p is not None: pos = int(p) except Exception: pos = -1 seq = items or () if pos >= 0 and pos < len(seq): return unicode(seq[pos]).strip() try: seq2 = control.Model.StringItemList or () if pos >= 0 and pos < len(seq2): return unicode(seq2[pos]).strip() except Exception: pass return u"" def _is_single_dropdown_item(text, items): cur = unicode(text or u"").strip() if cur == u"" or u"," in cur or u";" in cur: return u"" cur_cf = cur.lower() for it in items or []: if unicode(it).strip().lower() == cur_cf: return unicode(it).strip() return u"" def _append_to_acc(acc_holder, token): acc_holder[0] = _append_csv_token_no_dup(acc_holder[0], token) return acc_holder[0] def _refresh_columns_combo(): sheets_txt = sheets_acc[0] has_sheets = sheets_txt != u"" for name in (u"Cb_cols", u"Btn_cols_x", u"Btn_cols_xx"): _ctl_enabled(dlg.getControl(name), has_sheets) if not has_sheets: cols_acc[0] = u"" cols_items[:] = [] _sync_combo_from_acc(cols_cb, u"", ()) return hdr = u"1" try: hdr = unicode(hdr_ed.getText() or u"").strip() or u"1" except Exception: pass cols_items[:] = _merge_list_columns_for_sheets(doc, sheets_txt, hdr) _sync_combo_from_acc(cols_cb, cols_acc[0], cols_items) sheets_items[:] = _pivot_list_doc_sheet_names(doc) loading[0] = True try: _sync_combo_from_acc(sheets_cb, sheets_acc[0], sheets_items) dlg.getControl(u"Ed_dest").setText( unicode(init.get(u"dest_sheet") or u"") ) dlg.getControl(u"Ed_hdr").setText( unicode(init.get(u"header_row") or u"1") ) dlg.getControl(u"CbFmt").State = ( 1 if init.get(u"with_formatting") else 0 ) dlg.getControl(u"CbAutoFmt").State = ( 1 if init.get(u"auto_format", True) else 0 ) cols_items[:] = [] _sync_combo_from_acc(cols_cb, cols_acc[0], cols_items) except Exception: pass finally: loading[0] = False _refresh_columns_combo() class _MergeSheetsHandler( unohelper.Base, XActionListener, XItemListener, XTextListener ): def disposing(self, ev): pass def textChanged(self, ev): if loading[0]: return try: src = ev.Source except Exception: return if src == sheets_cb: cur = _combo_edit_text(sheets_cb) # LO/AlterOffice при выборе из списка подменяет CSV одним пунктом. picked = _is_single_dropdown_item(cur, sheets_items) if picked and cur.lower() != sheets_acc[0].lower(): _append_to_acc(sheets_acc, picked) _sync_combo_from_acc(sheets_cb, sheets_acc[0], sheets_items) _refresh_columns_combo() return sheets_acc[0] = cur _refresh_columns_combo() return if src == cols_cb: cur = _combo_edit_text(cols_cb) picked = _is_single_dropdown_item(cur, cols_items) if picked and cur.lower() != cols_acc[0].lower(): _append_to_acc(cols_acc, picked) _sync_combo_from_acc(cols_cb, cols_acc[0], cols_items) return cols_acc[0] = cur return if src == hdr_ed: _refresh_columns_combo() def itemStateChanged(self, ev): if loading[0]: return try: src = ev.Source except Exception: return if src == sheets_cb: token = _dropdown_token(sheets_cb, sheets_items, ev) if token == u"": return _append_to_acc(sheets_acc, token) _sync_combo_from_acc(sheets_cb, sheets_acc[0], sheets_items) _refresh_columns_combo() return if src == cols_cb: token = _dropdown_token(cols_cb, cols_items, ev) if token == u"": return _append_to_acc(cols_acc, token) _sync_combo_from_acc(cols_cb, cols_acc[0], cols_items) def actionPerformed(self, ev): try: name = unicode(ev.Source.getModel().Name) except Exception: return if name == u"Btn_sheets_x": sheets_acc[0] = _vlookup_remove_last_token(sheets_acc[0]) _sync_combo_from_acc(sheets_cb, sheets_acc[0], sheets_items) _refresh_columns_combo() return if name == u"Btn_sheets_xx": sheets_acc[0] = u"" _sync_combo_from_acc(sheets_cb, u"", sheets_items) _refresh_columns_combo() return if name == u"Btn_cols_x": cols_acc[0] = _vlookup_remove_last_token(cols_acc[0]) _sync_combo_from_acc(cols_cb, cols_acc[0], cols_items) return if name == u"Btn_cols_xx": cols_acc[0] = u"" _sync_combo_from_acc(cols_cb, u"", cols_items) handler = _MergeSheetsHandler() try: sheets_cb.addItemListener(handler) sheets_cb.addTextListener(handler) cols_cb.addItemListener(handler) cols_cb.addTextListener(handler) hdr_ed.addTextListener(handler) for btn_name in ( u"Btn_sheets_x", u"Btn_sheets_xx", u"Btn_cols_x", u"Btn_cols_xx", ): ctl = dlg.getControl(btn_name) if ctl is not None: ctl.addActionListener(handler) except Exception: pass rc = dlg.execute() if int(rc) != 1: try: dlg.dispose() except Exception: pass return None # Источник истины — аккумуляторы; контрол подстраховываем на случай ручного ввода. sheets = sheets_acc[0] or _combo_edit_text(sheets_cb) columns = cols_acc[0] or _combo_edit_text(cols_cb) dest = unicode(dlg.getControl(u"Ed_dest").getText() or u"").strip() hdr = unicode(dlg.getControl(u"Ed_hdr").getText() or u"").strip() or u"1" with_fmt = bool(int(dlg.getControl(u"CbFmt").State)) auto_fmt = bool(int(dlg.getControl(u"CbAutoFmt").State)) try: dlg.dispose() except Exception: pass sheets_list = [ x.strip() for x in sheets.replace(u";", u",").split(u",") if x.strip() ] columns_list = [ x.strip() for x in columns.replace(u";", u",").split(u",") if x.strip() ] try: hdr_n = int(float(hdr.replace(u",", u"."))) except Exception: hdr_n = 1 import json as _json block = { u"v": 1, u"fn": u"объединить_листы_в_один", u"sheets": sheets_list, u"dest_sheet": dest or u"Merge", u"header_row": hdr_n, u"with_formatting": with_fmt, u"auto_format": auto_fmt, } if columns_list: block[u"columns"] = columns_list return {u"json_c": unicode(_json.dumps([block], ensure_ascii=False))} except Exception as err: try: _show_message(u"Диалог объединения листов: %s" % err) except Exception: pass return None def _param_sheet_uses_xml_excel_ods(doc): """True, если на листе параметров Способ_переноса = xml_excel_ods.""" sheet = _find_param_sheet(doc) if sheet is None: return False p_transfer = _transfer_param_name() row_tr = _find_param_row(sheet, p_transfer) if row_tr >= 0: b_val = _cell_string(sheet.getCellByPosition(1, row_tr)) if b_val != u"": return b_val.casefold() == u"xml_excel_ods" transfer = unicode(_read_transfer_wizard_value(sheet) or u"").strip() return transfer.casefold() == u"xml_excel_ods" def show_split_sheets_param_dialog(parent_dialog=None, initial=None, doc=None): """ Диалог «разделить_листы» (как объединить_листы / ВПР — JSON в C). Возвращает {"json_c": "..."} или None при отмене. """ init = _split_sheets_parse_initial(initial) xml_transfer = _param_sheet_uses_xml_excel_ods(doc) try: from com.sun.star.awt.PushButtonType import OK as BTN_OK from com.sun.star.awt.PushButtonType import CANCEL as BTN_CANCEL except Exception: BTN_OK = 1 BTN_CANCEL = 2 try: ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext("com.sun.star.awt.Toolkit", ctx) dm = sm.createInstanceWithContext( "com.sun.star.awt.UnoControlDialogModel", ctx ) dw = 600 m = 14 lw = 130 gap = 4 list_w = 162 mid_x = m + list_w + gap mid_w = 48 sel_x = mid_x + mid_w + gap sel_w = dw - m - sel_x row_h = 20 y = 6 dm.PositionX = 70 dm.PositionY = 50 dm.Width = dw dm.Title = u"Разделить листы" field_x = m + lw + 4 field_w = dw - m - field_x - 6 if xml_transfer: hint_txt = ( u"xml_excel_ods: точка запуска только «После финальной обработки» " u"(after_final)." ) else: hint_txt = ( u"Порядок шагов при «После постобработки» — номером строки " u"на листе параметров." ) _wizard_dlg_add_fixed(dm, u"LblHint", hint_txt, m, y, dw - 2 * m, 22) y = y + 24 _wizard_dlg_add_fixed(dm, u"LblPhase", u"Точка запуска", m, y + 2, lw, 14) _wizard_dlg_add_combo(dm, u"CbPhase", field_x, y, field_w, 18) y = y + row_h _wizard_dlg_add_fixed(dm, u"LblSheet", u"Лист-источник", m, y + 2, lw, 14) _wizard_dlg_add_combo(dm, u"CbSheet", field_x, y, field_w, 18) y = y + row_h _wizard_dlg_add_fixed(dm, u"LblHdr", u"Строка заголовков", m, y + 2, lw, 14) _wizard_dlg_add_edit(dm, u"EdHdr", field_x, y, 80, 18) y = y + row_h + 2 def _add_transfer_block(title, lbl_id, avail_id, sel_id, btn_r, btn_l, btn_u, btn_d, block_y): # Подпись полностью над списками (иначе верхняя рамка listbox перекрывается). _wizard_dlg_add_fixed(dm, lbl_id, title, m, block_y, dw - 2 * m, 12) by = block_y + 14 list_h = 88 btn_h = 18 btn_gap = 2 center_y = by + list_h // 2 top_y = center_y - btn_h - btn_gap - btn_h bot_y = center_y + btn_gap _wizard_dlg_add_button(dm, btn_r, u">", mid_x, top_y, mid_w, btn_h) _wizard_dlg_add_button(dm, btn_l, u"<", mid_x, top_y + btn_h + btn_gap, mid_w, btn_h) _wizard_dlg_add_button(dm, btn_u, u"Up", mid_x, bot_y, mid_w, btn_h) _wizard_dlg_add_button(dm, btn_d, u"Down", mid_x, bot_y + btn_h + btn_gap, mid_w, btn_h) # Listbox после кнопок + raise — поверх смежных подписей/рамок. _wizard_dlg_add_listbox(dm, avail_id, m, by, list_w, list_h) _wizard_dlg_add_listbox(dm, sel_id, sel_x, by, sel_w, list_h) return by + list_h + 5 y = _add_transfer_block( u"Создавать новую книгу при изменении", u"LblBooks", u"LbBooksAvail", u"LbBooksSel", u"BtnBookR", u"BtnBookL", u"BtnBookU", u"BtnBookD", y, ) y = _add_transfer_block( u"Создавать новый лист при изменении", u"LblSheets", u"LbSheetsAvail", u"LbSheetsSel", u"BtnSheetR", u"BtnSheetL", u"BtnSheetU", u"BtnSheetD", y, ) for _lb_raise in ( u"LbBooksAvail", u"LbBooksSel", u"LbSheetsAvail", u"LbSheetsSel", ): _wizard_dlg_raise_control(dm, _lb_raise) _wizard_dlg_add_fixed(dm, u"LblBookTpl", u"Шаблон книги", m, y + 2, lw, 14) _wizard_dlg_add_combo(dm, u"CbBookTpl", field_x, y, field_w, 18) y = y + row_h _wizard_dlg_add_fixed(dm, u"LblSheetTpl", u"Шаблон листа", m, y + 2, lw, 14) _wizard_dlg_add_combo(dm, u"CbSheetTpl", field_x, y, field_w, 18) y = y + row_h _wizard_dlg_add_fixed(dm, u"LblMode", u"Способ переноса", m, y + 2, lw, 14) mode_w = min(200, max(140, field_w - 180)) _wizard_dlg_add_combo(dm, u"CbMode", field_x, y, mode_w, 18) paste_chk_x = field_x + mode_w + 6 paste_chk_w = max(120, dw - m - paste_chk_x) _wizard_dlg_add_checkbox( dm, u"CbPasteFmt", u"Форматы с 1-й строки данных", paste_chk_x, y + 1, paste_chk_w, 16, ) y = y + row_h + 2 chk_specs = ( (u"CbPreSort", u"Предварительная сортировка"), (u"CbCopyHdr", u"Копировать строку заголовков"), (u"CbMkDir", u"Создавать отсутствующие каталоги"), (u"CbOverwrite", u"Перезаписывать существующие листы"), (u"CbAppend", u"Дописывать в существующие книги"), (u"CbSaveNow", u"Сохранять книгу сразу после записи"), ) chk_h = 14 chk_gap = 1 chk_cols = 3 chk_area_w = dw - 2 * m col_w = chk_area_w // chk_cols for i, (cid, clbl) in enumerate(chk_specs): col = i % chk_cols row = i // chk_cols cx = m + col * col_w cy = y + row * (chk_h + chk_gap) _wizard_dlg_add_checkbox(dm, cid, clbl, cx, cy, col_w - 4, chk_h) y = y + 2 * (chk_h + chk_gap) + 2 btn_row_y = y btn_ok = _wizard_dlg_add_button(dm, u"BtnOk", u"OK", dw - m - 140, btn_row_y, 64, 22) btn_cancel = _wizard_dlg_add_button( dm, u"BtnCancel", u"Отмена", dw - m - 70, btn_row_y, 64, 22 ) try: btn_ok.PushButtonType = BTN_OK btn_cancel.PushButtonType = BTN_CANCEL btn_ok.DefaultButton = True except Exception: pass dm.Height = btn_row_y + 26 dlg = sm.createInstanceWithContext("com.sun.star.awt.UnoControlDialog", ctx) dlg.setModel(dm) dlg.setVisible(False) if not _nested_subdialog_create_peer( dlg, toolkit, doc, parent_dialog, center_in_main_wizard=bool(parent_dialog) ): try: dlg.createPeer(toolkit, None) except Exception: pass phase_final = u"После финальной обработки (after_final)" phase_items_all = ( u"После первичного сбора (after_collect)", u"После постобработки (after_postprocess)", phase_final, ) phase_map = { phase_items_all[0]: u"after_collect", phase_items_all[1]: u"after_postprocess", phase_final: u"after_final", } phase_rev = {v: k for k, v in phase_map.items()} if xml_transfer: phase_items = (phase_final,) default_phase = u"after_final" else: phase_items = phase_items_all default_phase = unicode(init.get(u"run_phase") or u"after_postprocess") cb_phase = dlg.getControl(u"CbPhase") try: cb_phase.Model.StringItemList = phase_items try: cb_phase.Model.Editable = False except Exception: pass if xml_transfer: try: cb_phase.Model.Enabled = False except Exception: pass cb_phase.setText(phase_final) else: cb_phase.setText( phase_rev.get(default_phase, phase_items_all[1]) ) except Exception: pass cb_sheet = dlg.getControl(u"CbSheet") sheet_items = _pivot_list_doc_sheet_names(doc) try: cb_sheet.Model.StringItemList = tuple(sheet_items) cb_sheet.setText(unicode(init.get(u"source_sheet") or u"")) except Exception: pass dlg.getControl(u"EdHdr").setText(unicode(init.get(u"header_row") or u"1")) cb_book_tpl = dlg.getControl(u"CbBookTpl") cb_sheet_tpl = dlg.getControl(u"CbSheetTpl") book_tpl_acc = [unicode(init.get(u"book_template") or u"эта_книга")] sheet_tpl_acc = [unicode(init.get(u"sheet_template") or u"[Подразделение]-[Участок]")] tpl_guard = [False] for _cb_tpl in (cb_book_tpl, cb_sheet_tpl): try: _cb_tpl.Model.Editable = True except Exception: pass try: cb_book_tpl.setText(book_tpl_acc[0]) cb_sheet_tpl.setText(sheet_tpl_acc[0]) except Exception: pass cb_mode = dlg.getControl(u"CbMode") cb_paste_fmt = dlg.getControl(u"CbPasteFmt") mode_items = (u"copy_range (рекомендуется)", u"copy_sheet") try: cb_mode.Model.StringItemList = mode_items mode = unicode(init.get(u"split_mode") or u"copy_range").casefold() cb_mode.setText(mode_items[1] if mode == u"copy_sheet" else mode_items[0]) except Exception: pass def _sync_paste_fmt_enabled(): mode_txt = unicode(cb_mode.getText() or u"").casefold() is_range = u"copy_sheet" not in mode_txt try: cb_paste_fmt.Model.Enabled = bool(is_range) except Exception: pass if not is_range: try: cb_paste_fmt.State = 0 except Exception: pass try: cb_paste_fmt.State = ( 1 if init.get(u"paste_formats") and u"copy_sheet" not in unicode(init.get(u"split_mode") or u"copy_range").casefold() else 0 ) except Exception: pass _sync_paste_fmt_enabled() _pivot_listbox_set_items(dlg.getControl(u"LbBooksSel"), init.get(u"split_books") or []) _pivot_listbox_set_items(dlg.getControl(u"LbSheetsSel"), init.get(u"split_sheets") or []) dlg.getControl(u"CbPreSort").State = 1 if init.get(u"pre_sort", True) else 0 dlg.getControl(u"CbCopyHdr").State = 1 if init.get(u"copy_header", True) else 0 dlg.getControl(u"CbMkDir").State = 1 if init.get(u"create_folders", True) else 0 dlg.getControl(u"CbOverwrite").State = 1 if init.get(u"overwrite_sheets", True) else 0 dlg.getControl(u"CbAppend").State = 1 if init.get(u"append_if_exists") else 0 dlg.getControl(u"CbSaveNow").State = 1 if init.get(u"save_immediately") else 0 def _current_field_columns(): hdr = unicode(dlg.getControl(u"EdHdr").getText() or u"1").strip() or u"1" sheet = unicode(cb_sheet.getText() or u"").strip() if not sheet: return [] try: return list(_vlookup_list_sheet_columns(doc, sheet, hdr) or []) except Exception: return [] def _refresh_template_combos(cols=None): fields = list(cols) if cols is not None else _current_field_columns() items = tuple(fields) for cb in (cb_book_tpl, cb_sheet_tpl): try: cur = unicode(cb.getText() or u"") cb.Model.StringItemList = items cb.setText(cur) except Exception: pass def _append_template_field(cb, acc_holder, field_name): field = unicode(field_name or u"").strip() if field == u"": return if field.startswith(u"[") and field.endswith(u"]") and len(field) >= 2: token = field bare = field[1:-1].strip() else: bare = field token = u"[%s]" % field if bare == u"": return base = unicode(acc_holder[0] or u"") if base.endswith(token): # Уже добавлено (повтор textChanged/itemStateChanged) — вернуть текст. tpl_guard[0] = True try: _set_combo_edit_text(cb, base) finally: tpl_guard[0] = False return merged = base + token tpl_guard[0] = True try: _set_combo_edit_text(cb, merged) finally: tpl_guard[0] = False acc_holder[0] = merged def _refresh_avail(): cols = _current_field_columns() used_b = set(_pivot_listbox_items(dlg.getControl(u"LbBooksSel"))) used_s = set(_pivot_listbox_items(dlg.getControl(u"LbSheetsSel"))) avail_b = [c for c in cols if c not in used_b] avail_s = [c for c in cols if c not in used_s] _pivot_listbox_set_items(dlg.getControl(u"LbBooksAvail"), avail_b) _pivot_listbox_set_items(dlg.getControl(u"LbSheetsAvail"), avail_s) _refresh_template_combos(cols) _refresh_avail() def _move_list_item(lb, direction): idxs = _pivot_listbox_selected_indices(lb) if len(idxs) != 1: return items = _pivot_listbox_items(lb) i = int(idxs[0]) j = i + int(direction) if j < 0 or j >= len(items): return items[i], items[j] = items[j], items[i] _pivot_listbox_set_items(lb, items) try: lb.selectItemPos(j, True) except Exception: pass class _SplitHandler(unohelper.Base, XActionListener, XItemListener, XTextListener): def disposing(self, ev): pass def textChanged(self, ev): try: src = ev.Source except Exception: return if src in (cb_sheet, dlg.getControl(u"EdHdr")): _refresh_avail() return if src == cb_mode: _sync_paste_fmt_enabled() return if tpl_guard[0]: return if src == cb_book_tpl: cur = _combo_edit_text(cb_book_tpl) items = tuple(cb_book_tpl.Model.StringItemList or ()) picked = _wizard_dlg_combo_single_item(cur, items) if picked: _append_template_field(cb_book_tpl, book_tpl_acc, picked) else: book_tpl_acc[0] = cur return if src == cb_sheet_tpl: cur = _combo_edit_text(cb_sheet_tpl) items = tuple(cb_sheet_tpl.Model.StringItemList or ()) picked = _wizard_dlg_combo_single_item(cur, items) if picked: _append_template_field(cb_sheet_tpl, sheet_tpl_acc, picked) else: sheet_tpl_acc[0] = cur def itemStateChanged(self, ev): try: src = ev.Source except Exception: return if src == cb_mode: _sync_paste_fmt_enabled() return if tpl_guard[0]: return if src == cb_book_tpl: token = _wizard_dlg_combo_dropdown_token( cb_book_tpl, cb_book_tpl.Model.StringItemList or (), ev ) if token: _append_template_field(cb_book_tpl, book_tpl_acc, token) return if src == cb_sheet_tpl: token = _wizard_dlg_combo_dropdown_token( cb_sheet_tpl, cb_sheet_tpl.Model.StringItemList or (), ev ) if token: _append_template_field(cb_sheet_tpl, sheet_tpl_acc, token) def actionPerformed(self, ev): try: name = unicode(ev.Source.getModel().Name) except Exception: return pairs = ( (u"BtnBookR", u"LbBooksAvail", u"LbBooksSel"), (u"BtnBookL", u"LbBooksSel", u"LbBooksAvail"), (u"BtnSheetR", u"LbSheetsAvail", u"LbSheetsSel"), (u"BtnSheetL", u"LbSheetsSel", u"LbSheetsAvail"), ) for btn, src_id, dst_id in pairs: if name == btn: _pivot_listbox_move_selected( dlg.getControl(src_id), dlg.getControl(dst_id) ) _refresh_avail() return if name == u"BtnBookU": _move_list_item(dlg.getControl(u"LbBooksSel"), -1) return if name == u"BtnBookD": _move_list_item(dlg.getControl(u"LbBooksSel"), 1) return if name == u"BtnSheetU": _move_list_item(dlg.getControl(u"LbSheetsSel"), -1) return if name == u"BtnSheetD": _move_list_item(dlg.getControl(u"LbSheetsSel"), 1) return handler = _SplitHandler() try: cb_sheet.addItemListener(handler) cb_sheet.addTextListener(handler) dlg.getControl(u"EdHdr").addTextListener(handler) cb_mode.addItemListener(handler) cb_mode.addTextListener(handler) cb_book_tpl.addItemListener(handler) cb_book_tpl.addTextListener(handler) cb_sheet_tpl.addItemListener(handler) cb_sheet_tpl.addTextListener(handler) for nm in ( u"BtnBookR", u"BtnBookL", u"BtnBookU", u"BtnBookD", u"BtnSheetR", u"BtnSheetL", u"BtnSheetU", u"BtnSheetD", ): ctl = dlg.getControl(nm) if ctl is not None: ctl.addActionListener(handler) except Exception: pass rc = dlg.execute() if int(rc) != 1: try: dlg.dispose() except Exception: pass return None if xml_transfer: run_phase = u"after_final" else: phase_txt = unicode(cb_phase.getText() or u"").strip() run_phase = phase_map.get(phase_txt, u"after_postprocess") source_sheet = unicode(cb_sheet.getText() or u"").strip() hdr = unicode(dlg.getControl(u"EdHdr").getText() or u"1").strip() or u"1" try: hdr_n = int(float(hdr.replace(u",", u"."))) except Exception: hdr_n = 1 mode_txt = unicode(cb_mode.getText() or u"").casefold() split_mode = u"copy_sheet" if u"copy_sheet" in mode_txt else u"copy_range" paste_formats = False if split_mode == u"copy_range": try: paste_formats = bool(int(cb_paste_fmt.State)) except Exception: paste_formats = False split_books = _pivot_listbox_items(dlg.getControl(u"LbBooksSel")) split_sheets = _pivot_listbox_items(dlg.getControl(u"LbSheetsSel")) import json as _json block = { u"v": 1, u"fn": u"разделить_листы", u"run_phase": run_phase, u"source": {u"book": u"эта_книга", u"sheet": source_sheet}, u"split_books": split_books, u"split_sheets": split_sheets, u"header_row": hdr_n, u"pre_sort": bool(int(dlg.getControl(u"CbPreSort").State)), u"book_template": unicode( book_tpl_acc[0] or _combo_edit_text(cb_book_tpl) or u"" ).strip() or u"эта_книга", u"sheet_template": unicode( sheet_tpl_acc[0] or _combo_edit_text(cb_sheet_tpl) or u"" ).strip() or u"[Split]", u"split_mode": split_mode, u"paste_formats": paste_formats, u"copy_header": bool(int(dlg.getControl(u"CbCopyHdr").State)), u"create_folders": bool(int(dlg.getControl(u"CbMkDir").State)), u"overwrite_sheets": bool(int(dlg.getControl(u"CbOverwrite").State)), u"append_if_exists": bool(int(dlg.getControl(u"CbAppend").State)), u"save_immediately": bool(int(dlg.getControl(u"CbSaveNow").State)), } try: dlg.dispose() except Exception: pass return {u"json_c": unicode(_json.dumps([block], ensure_ascii=False))} except Exception as err: try: _show_message(u"Диалог разделения листов: %s" % err) except Exception: pass return None def _split_sheets_parse_initial(initial): """initial (JSON C или dict) → dict полей диалога разделения.""" out = { u"run_phase": u"after_postprocess", u"source_sheet": u"", u"header_row": u"1", u"split_books": [], u"split_sheets": [], u"book_template": u"эта_книга", u"sheet_template": u"[Подразделение]-[Участок]", u"split_mode": u"copy_range", u"paste_formats": False, u"pre_sort": True, u"copy_header": True, u"create_folders": True, u"overwrite_sheets": True, u"append_if_exists": False, u"save_immediately": False, } if initial is None: return out if isinstance(initial, dict) and u"json_c" not in initial and ( u"source_sheet" in initial or u"split_sheets" in initial ): for key in out: if key in initial: out[key] = initial.get(key) return out text = unicode(initial or u"").strip() if text == u"": return out try: from libre_macros_param_codec import param_decode blocks = param_decode(u"разделить_листы", text) or [] if blocks and isinstance(blocks[0], dict): b = blocks[0] src = b.get(u"source") or {} out[u"run_phase"] = unicode(b.get(u"run_phase") or u"after_postprocess") out[u"source_sheet"] = unicode( src.get(u"sheet") or b.get(u"source_sheet") or u"" ).strip() if b.get(u"header_row") not in (None, u""): out[u"header_row"] = unicode(b.get(u"header_row")) out[u"split_books"] = list(b.get(u"split_books") or []) out[u"split_sheets"] = list(b.get(u"split_sheets") or []) out[u"book_template"] = unicode(b.get(u"book_template") or u"эта_книга") out[u"sheet_template"] = unicode( b.get(u"sheet_template") or u"[Подразделение]-[Участок]" ) out[u"split_mode"] = unicode(b.get(u"split_mode") or u"copy_range") for flag in ( u"pre_sort", u"copy_header", u"create_folders", u"overwrite_sheets", u"append_if_exists", u"save_immediately", u"paste_formats", ): if flag in b: out[flag] = bool(b.get(flag)) except Exception: pass return out def show_vlookup_param_dialog(parent_dialog=None, initial=None, doc=None): """ Диалог ввода параметров ВПР (отдельные поля → объединение через | в B/C). Возвращает dict для B..F или None при отмене. """ init = initial or {} json_err = init.get('_json_parse_error') if json_err: _show_message(u'Ошибка разбора JSON ВПР в колонке C:\n%s' % unicode(json_err)) return None try: ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) dm = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialogModel', ctx) dw = int(round(440 * 1.3)) m = 8 col_gap = 10 lw = 155 ew = dw - lw - m * 3 row_h = 22 hdr_step = 18 y = 10 edits = {} keys_same_init = bool(init.get('keys_same', True)) if init.get('keys_left') in (None, u'') and init.get('keys_d'): init['keys_left'] = init.get('keys_d') if init.get('keys_right') in (None, u''): init['keys_right'] = init.get('keys_left') or init.get('keys_d') or u'' content_w = dw - m * 2 half_w = (content_w - col_gap) // 2 lw_side = 118 ew_side = max(60, half_w - lw_side - 6) def add_row(label, key, default=u'', x=m, row_y=y, label_w=lw, edit_w=ew, use_combo=False): _wizard_dlg_add_fixed(dm, 'Lbl_' + key, label, x, row_y, label_w, 14) if use_combo: _wizard_dlg_add_combo(dm, 'Ed_' + key, x + label_w + 4, row_y - 2, edit_w, 16) else: _wizard_dlg_add_edit(dm, 'Ed_' + key, x + label_w + 4, row_y - 2, edit_w, 16) edits[key] = unicode(init.get(key) or default) def add_table_block(hdr_id, hdr_label, field_specs, x0, y0): _wizard_dlg_add_fixed(dm, hdr_id, hdr_label, x0, y0, half_w, 14) ry = y0 + hdr_step fi = 0 while fi < len(field_specs): label, key, default = field_specs[fi] add_row(label, key, default, x0, ry, lw_side, ew_side, use_combo=key.endswith('_sheet')) ry = ry + row_h fi = fi + 1 return ry left_x = m right_x = m + half_w + col_gap y_tables = 10 left_fields = ((u'Лист:', 'left_sheet', u''), (u'Начало строки:', 'left_start_row', u''), (u'Начало столбца:', 'left_start_col', u''), (u'Строка заголовков:', 'left_header_row', u'')) right_fields = ((u'Лист:', 'right_sheet', u''), (u'Начало строки:', 'right_start_row', u''), (u'Начало столбца:', 'right_start_col', u''), (u'Строка заголовков:', 'right_header_row', u'')) y_left_end = add_table_block('HdrLeft', u'— Левая таблица —', left_fields, left_x, y_tables) y_right_end = add_table_block('HdrRight', u'— Правая таблица —', right_fields, right_x, y_tables) y = max(y_left_end, y_right_end) _wizard_dlg_add_fixed(dm, 'HdrJoin', u'— Соединение —', m, y, dw - m * 2, 14) y = y + hdr_step _wizard_dlg_add_button(dm, 'ChkSameKeys', u'☑ Одинаковые ключи', m, y, dw - m * 2, 18) y = y + 20 _vlookup_dialog_add_token_row(dm, u'Ключи (общие):', 'Lbl_keys_left', 'Ed_keys_left', 'Cb_keys_left', 'BtnDel_keys_left', 'BtnAdd_keys_left', m, y, lw, ew) edits['keys_left'] = unicode(init.get('keys_left') or u'') y = y + row_h _vlookup_dialog_add_token_row(dm, u'Ключи (прав.):', 'Lbl_keys_right', 'Ed_keys_right', 'Cb_keys_right', 'BtnDel_keys_right', 'BtnAdd_keys_right', m, y, lw, ew) edits['keys_right'] = unicode(init.get('keys_right') or u'') y = y + row_h _vlookup_dialog_add_token_row(dm, u'Столбцы (E):', 'Lbl_extract_e', 'Ed_extract_e', 'Cb_extract_e', 'BtnDel_extract_e', 'BtnAdd_extract_e', m, y, lw, ew) edits['extract_e'] = unicode(init.get('extract_e') or u'') y = y + row_h save_json_init = bool(init.get('save_as_json', False)) trim_keys_init = bool(init.get('trim_keys', False)) column_suffix_init = _vlookup_parse_bool_option(init.get('column_suffix'), default=True) _wizard_dlg_add_fixed(dm, 'HdrF', u'— Параметры F (ячейка) —', m, y, dw - m * 2, 14) y = y + hdr_step _wizard_dlg_add_fixed(dm, 'LblColorPreset', u'Цвет:', m, y, lw, 14) _wizard_dlg_add_combo(dm, 'ColorPreset', m + lw + 4, y - 2, ew, 16) y = y + row_h _wizard_dlg_add_fixed(dm, 'Lbl_not_found_fill', u'Не найдено:', m, y, lw, 14) nf_w = max(72, int(ew * 0.32)) _wizard_dlg_add_edit(dm, 'Ed_not_found_fill', m + lw + 4, y - 2, nf_w, 16) _wizard_dlg_add_button(dm, 'ChkFillDup', u'☑ Дубли', m + lw + 4 + nf_w + 6, y - 1, 88, 18) _wizard_dlg_add_button(dm, 'ChkMultiMatch', u'☑ Мульти', m + lw + 4 + nf_w + 6 + 88 + 6, y - 1, 88, 18) _wizard_dlg_add_button(dm, 'ChkTrimKeys', u'☑ СЖ_ПРОБЕЛЫ' if trim_keys_init else u'☐ СЖ_ПРОБЕЛЫ', m + lw + 4 + nf_w + 6 + 88 + 6 + 88 + 6, y - 1, 72, 18) y = y + row_h _wizard_dlg_add_fixed(dm, 'Lbl_f_preview', u'Ячейка F (итог):', m, y, lw, 14) _wizard_dlg_add_edit(dm, 'Ed_f_preview', m + lw + 4, y - 2, ew, 36, multiline=True, readonly=True) y = y + 40 _wizard_dlg_add_button(dm, 'ChkColumnSuffix', u'☑ Суффикс _R' if column_suffix_init else u'☐ Суффикс _R', m, y, 130, 18) _wizard_dlg_add_button(dm, 'ChkSaveJson', u'☑ Сохранять как JSON' if save_json_init else u'☐ Сохранять как JSON', m + 140, y, 200, 18) y = y + 22 btn_h = 22 btn_gap = 10 by = y + btn_gap dh = by + btn_h + m dm.PositionX = 120 dm.PositionY = 80 dm.Width = dw dm.Height = dh dm.Title = u'Параметры ВПР' _wizard_dlg_add_button(dm, 'VOkBtn', u'OK', m, by, 90, btn_h) _wizard_dlg_add_button(dm, 'VCancelBtn', u'Отмена', m + 100, by, 90, btn_h) dlg = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialog', ctx) dlg.setModel(dm) _nested_subdialog_create_peer( dlg, toolkit, doc, parent_dialog, center_in_main_wizard=bool(parent_dialog) ) for key in edits: ctl = dlg.getControl('Ed_' + key) if ctl is not None: try: ctl.setText(edits[key]) except Exception: pass nf_init_ctl = dlg.getControl('Ed_not_found_fill') f_raw_init = unicode(init.get('f_cell_raw') or u'').strip() if nf_init_ctl is not None and init is not None and (f_raw_init == u'') and ('not_found_fill' in init): try: nf_init_ctl.setText(_vlookup_init_not_found_fill(init)) except Exception: pass def _sync_column_suffix(checked): result['column_suffix'] = checked _vlookup_dialog_set_bool_btn(dlg, 'ChkColumnSuffix', checked, u'☑ Суффикс _R', u'☐ Суффикс _R') _vlookup_dialog_rebuild_f_from_controls(dlg, result) def _sync_trim_keys(checked): result['trim_keys'] = checked _vlookup_dialog_set_bool_btn(dlg, 'ChkTrimKeys', checked, u'☑ СЖ_ПРОБЕЛЫ', u'☐ СЖ_ПРОБЕЛЫ') _vlookup_dialog_rebuild_f_from_controls(dlg, result) def _sync_fill_dup(checked): result['fill_duplicates'] = checked _vlookup_dialog_set_bool_btn(dlg, 'ChkFillDup', checked, u'☑ Дубли', u'☐ Дубли') _vlookup_dialog_rebuild_f_from_controls(dlg, result) def _sync_multi_match(checked): result['multi_match_one_cell'] = checked _vlookup_dialog_set_bool_btn(dlg, 'ChkMultiMatch', checked, u'☑ Мульти', u'☐ Мульти') _vlookup_dialog_rebuild_f_from_controls(dlg, result) def _sync_keys_mode(same): lbl_l = dlg.getControl('Lbl_keys_left') ed_r = dlg.getControl('Ed_keys_right') lbl_r = dlg.getControl('Lbl_keys_right') btn = dlg.getControl('ChkSameKeys') right_token_ctls = ('Ed_keys_right', 'Lbl_keys_right', 'Cb_keys_right', 'BtnDel_keys_right', 'BtnAdd_keys_right') try: if lbl_l is not None: lbl_l.Model.Label = u'Ключи (общие):' if same else u'Ключи (лев.):' except Exception: pass for ctl_name in right_token_ctls: ctl = dlg.getControl(ctl_name) if ctl is not None: try: ctl.setVisible(not same) ctl.setEnable(not same) except Exception: pass if btn is not None: try: btn.Model.Label = u'☑ Одинаковые ключи' if same else u'☐ Разные ключи' except Exception: pass _vlookup_dialog_refresh_token_combos(dlg, doc, same) preset_ctl = dlg.getControl('ColorPreset') if preset_ctl is not None: try: preset_ctl.addItem(_pw_cfg._VLOOKUP_COLOR_PRESET_EMPTY, 0) pi = 0 while pi < len(_pw_cfg._VLOOKUP_COLOR_PRESETS): preset_ctl.addItem(_pw_cfg._VLOOKUP_COLOR_PRESETS[pi], pi + 1) pi = pi + 1 except Exception: pass result = {'cancel': True, 'keys_same': keys_same_init, 'save_as_json': save_json_init, 'trim_keys': trim_keys_init, 'column_suffix': column_suffix_init} f_init_raw = unicode(init.get('f_cell_raw') or u'').strip() if f_init_raw != u'': f_bools_init = _vlookup_parse_f_bool_fields(f_init_raw) result['fill_duplicates'] = f_bools_init['fill_duplicates'] result['multi_match_one_cell'] = f_bools_init['multi_match_one_cell'] result['trim_keys'] = f_bools_init.get('trim_keys', False) result['column_suffix'] = f_bools_init.get('column_suffix', True) else: result['fill_duplicates'] = _vlookup_parse_bool_option(init.get('fill_duplicates'), default=True) result['multi_match_one_cell'] = _vlookup_parse_bool_option(init.get('multi_match_one_cell'), default=False) result['trim_keys'] = _vlookup_parse_bool_option(init.get('trim_keys'), default=False) result['column_suffix'] = _vlookup_parse_bool_option(init.get('column_suffix'), default=True) class _VHandler(unohelper.Base, XActionListener, XTextListener): def textChanged(self, event): try: src = event.Source mdl = src.getModel() name = unicode(mdl.Name) except Exception: name = u'' if name in ('Ed_left_sheet', 'Ed_right_sheet', 'Ed_left_header_row', 'Ed_right_header_row', 'Ed_left_start_col', 'Ed_right_start_col'): _vlookup_dialog_refresh_token_combos(dlg, doc, result.get('keys_same', True)) _vlookup_dialog_rebuild_f_from_controls(dlg, result) def disposing(self, event): pass def actionPerformed(self, event): src = event.Source try: name = src.getModel().Name except Exception: return if name == 'BtnDel_keys_left': ed = dlg.getControl('Ed_keys_left') if ed is not None: ed.setText(_vlookup_remove_last_token(ed.getText())) return if name == 'BtnAdd_keys_left': ed = dlg.getControl('Ed_keys_left') cb = dlg.getControl('Cb_keys_left') if ed is not None and cb is not None: ed.setText(_vlookup_append_token(ed.getText(), cb.getText())) return if name == 'BtnDel_keys_right': ed = dlg.getControl('Ed_keys_right') if ed is not None: ed.setText(_vlookup_remove_last_token(ed.getText())) return if name == 'BtnAdd_keys_right': ed = dlg.getControl('Ed_keys_right') cb = dlg.getControl('Cb_keys_right') if ed is not None and cb is not None: ed.setText(_vlookup_append_token(ed.getText(), cb.getText())) return if name == 'BtnDel_extract_e': ed = dlg.getControl('Ed_extract_e') if ed is not None: ed.setText(_vlookup_remove_last_token(ed.getText())) return if name == 'BtnAdd_extract_e': ed = dlg.getControl('Ed_extract_e') cb = dlg.getControl('Cb_extract_e') if ed is not None and cb is not None: ed.setText(_vlookup_append_token(ed.getText(), cb.getText())) return if name == 'ChkSameKeys': result['keys_same'] = not result.get('keys_same', True) _sync_keys_mode(result['keys_same']) return if name == 'ChkFillDup': result['fill_duplicates'] = not result.get('fill_duplicates', True) _sync_fill_dup(result['fill_duplicates']) return if name == 'ChkMultiMatch': result['multi_match_one_cell'] = not result.get('multi_match_one_cell', False) _sync_multi_match(result['multi_match_one_cell']) return if name == 'ChkSaveJson': result['save_as_json'] = not result.get('save_as_json', False) _vlookup_dialog_set_bool_btn(dlg, 'ChkSaveJson', result['save_as_json'], u'☑ Сохранять как JSON', u'☐ Сохранять как JSON') return if name == 'ChkTrimKeys': result['trim_keys'] = not result.get('trim_keys', False) _sync_trim_keys(result['trim_keys']) return if name == 'ChkColumnSuffix': result['column_suffix'] = not result.get('column_suffix', True) _sync_column_suffix(result['column_suffix']) return if name == 'VCancelBtn': dlg.endExecute() return if name == 'VOkBtn': packed = {} for k in edits: ctl = dlg.getControl('Ed_' + k) packed[k] = ctl.getText().strip() if ctl is not None else u'' packed['keys_same'] = result.get('keys_same', True) f_ctl = dlg.getControl('Ed_f_preview') packed['f_cell'] = f_ctl.getText().strip() if f_ctl is not None else u'' if packed.get('left_sheet', u'') == u'': _show_message(u'Укажите левый лист.', dialog=dlg) return if packed.get('right_sheet', u'') == u'': _show_message(u'Укажите правый лист.', dialog=dlg) return if packed.get('keys_left', u'') == u'': _show_message(u'Укажите ключи соединения.', dialog=dlg) return if not packed['keys_same'] and packed.get('keys_right', u'') == u'': _show_message(u'Укажите ключи правой таблицы.', dialog=dlg) return if packed.get('extract_e', u'') == u'': _show_message(u'Укажите столбцы для извлечения.', dialog=dlg) return preset_ctl_ok = dlg.getControl('ColorPreset') nf_ctl_ok = dlg.getControl('Ed_not_found_fill') packed['fill_duplicates'] = result.get('fill_duplicates', True) packed['multi_match_one_cell'] = result.get('multi_match_one_cell', False) packed['trim_keys'] = result.get('trim_keys', False) packed['column_suffix'] = result.get('column_suffix', True) packed['highlight_color'] = _vlookup_wizard_color_to_highlight(preset_ctl_ok.getText().strip() if preset_ctl_ok is not None else u'') packed['not_found_fill'] = _vlookup_coerce_not_found_fill(_vlookup_nf_ctl_get_text(nf_ctl_ok, dlg)) result['cancel'] = False try: if result.get('save_as_json'): result['values'] = {'save_as_json': True, 'json_c': _vlookup_pack_json_values(packed)} else: result['values'] = _vlookup_pack_row_values(packed) except Exception as pack_exc: _show_message(u'Ошибка сборки JSON ВПР: %s' % pack_exc, dialog=None) return dlg.endExecute() handler = _VHandler() dlg.getControl('VOkBtn').addActionListener(handler) dlg.getControl('VCancelBtn').addActionListener(handler) dlg.getControl('ChkSameKeys').addActionListener(handler) dlg.getControl('ChkFillDup').addActionListener(handler) dlg.getControl('ChkMultiMatch').addActionListener(handler) dlg.getControl('ChkSaveJson').addActionListener(handler) dlg.getControl('ChkTrimKeys').addActionListener(handler) dlg.getControl('ChkColumnSuffix').addActionListener(handler) for btn_name in ('BtnDel_keys_left', 'BtnAdd_keys_left', 'BtnDel_keys_right', 'BtnAdd_keys_right', 'BtnDel_extract_e', 'BtnAdd_extract_e'): try: dlg.getControl(btn_name).addActionListener(handler) except Exception: pass doc_sheet_names = _pivot_list_doc_sheet_names(doc) if len(doc_sheet_names) > 0: for sheet_key in ('left_sheet', 'right_sheet'): try: _set_combo_items(dlg.getControl('Ed_' + sheet_key), doc_sheet_names, select_text=edits.get(sheet_key) or u'') except Exception: pass for listen_key in ('left_sheet', 'right_sheet', 'left_header_row', 'right_header_row', 'left_start_col', 'right_start_col'): try: dlg.getControl('Ed_' + listen_key).addTextListener(handler) except Exception: pass if preset_ctl is not None: try: preset_ctl.addTextListener(handler) except Exception: pass class _PresetHandler(unohelper.Base, XItemListener): def disposing(self, event): pass def itemStateChanged(self, event): try: pos = event.Source.getSelectedItemPos() if pos is None or int(pos) < 0: return ed = dlg.getControl('ColorPreset') if int(pos) < 1: if ed is not None: ed.setText(_pw_cfg._VLOOKUP_COLOR_PRESET_EMPTY) else: val = _pw_cfg._VLOOKUP_COLOR_PRESETS[int(pos) - 1] if ed is not None: ed.setText(val) _vlookup_dialog_rebuild_f_from_controls(dlg, result) except Exception: pass preset_ctl.addItemListener(_PresetHandler()) _sync_keys_mode(keys_same_init) _vlookup_dialog_refresh_token_combos(dlg, doc, keys_same_init) _vlookup_dialog_set_bool_btn(dlg, 'ChkColumnSuffix', column_suffix_init, u'☑ Суффикс _R', u'☐ Суффикс _R') _vlookup_dialog_set_bool_btn(dlg, 'ChkSaveJson', save_json_init, u'☑ Сохранять как JSON', u'☐ Сохранять как JSON') _vlookup_dialog_set_bool_btn(dlg, 'ChkTrimKeys', trim_keys_init, u'☑ СЖ_ПРОБЕЛЫ', u'☐ СЖ_ПРОБЕЛЫ') if save_json_init: _vlookup_dialog_set_bool_btn(dlg, 'ChkFillDup', result.get('fill_duplicates', True), u'☑ Дубли', u'☐ Дубли') _vlookup_dialog_set_bool_btn(dlg, 'ChkMultiMatch', result.get('multi_match_one_cell', False), u'☑ Мульти', u'☐ Мульти') nf_json_ctl = dlg.getControl('Ed_not_found_fill') if nf_json_ctl is not None and 'not_found_fill' in init: _vlookup_nf_ctl_set_text(nf_json_ctl, _vlookup_init_not_found_fill(init)) preset_json = dlg.getControl('ColorPreset') if preset_json is not None and init.get('vlookup_color') not in (None, u''): try: preset_json.setText(_vlookup_wizard_highlight_to_preset(init.get('vlookup_color'))) except Exception: pass f_init = init.get('f_cell_raw') if f_init in (None, u''): f_init = _vlookup_build_f_cell_text(init.get('vlookup_color') or u'', init.get('fill_duplicates', True), _vlookup_init_not_found_fill(init), init.get('multi_match_one_cell', False), init.get('trim_keys', False), init.get('column_suffix', True)) _vlookup_dialog_apply_f_to_controls(dlg, result, f_init) nf_ctl = dlg.getControl('Ed_not_found_fill') if nf_ctl is not None: try: nf_ctl.addTextListener(handler) except Exception: pass dlg.execute() if result.get('cancel'): return None return result.get('values') except Exception as exc: _show_message(u'Ошибка диалога ВПР: %s' % exc) return None for _code, _label in _pw_cfg._PIVOT_DATA_FUNCTIONS: _pw_cfg._PIVOT_FN_CODE[_code.casefold()] = _code def _pivot_lm(): return _try_import_lm() def _pivot_header_row_from_params(doc): """Строка_Заголовков с листа параметров (1-based) или пусто.""" if doc is None: return None try: ps = _find_param_sheet(doc) if ps is None: return None row = _find_param_row(ps, u'Строка_Заголовков') if row < 0: row = _find_param_row(ps, u'Строка_заголовков') if row < 0: return None val = _cell_string(ps.getCellByPosition(1, row)).strip() return val if val != u'' else None except Exception: return None def _pivot_listbox_items(lb): if lb is None: return [] try: n = lb.getItemCount() except Exception: return [] out = [] i = 0 while i < n: try: out.append(unicode(lb.getItem(i))) except Exception: pass i = i + 1 return out def _pivot_listbox_set_items(lb, items): if lb is None: return try: lb.removeItems(0, lb.getItemCount()) except Exception: pass i = 0 while i < len(items): try: lb.addItem(unicode(items[i]), i) except Exception: pass i = i + 1 def _pivot_listbox_selected_indices(lb): if lb is None: return [] try: sel = lb.getSelectedItemsPos() if sel is None: return [] return list(sel) except Exception: return [] def _pivot_listbox_move_selected(src, dst): indices = sorted(_pivot_listbox_selected_indices(src), reverse=True) if len(indices) == 0: return dst_items = _pivot_listbox_items(dst) moved = [] for idx in indices: try: moved.append(unicode(src.getItem(idx))) src.removeItems(idx, 1) except Exception: pass for text in reversed(moved): dst_items.append(text) _pivot_listbox_set_items(dst, dst_items) def _pivot_data_field_label(field_name, fn_code): label = _pw_cfg._PIVOT_FN_LABEL.get(fn_code, fn_code) if fn_code == u'LIST_ROWS': return label return u'%s — %s' % (label, field_name) def _pivot_parse_data_field_item(text): raw = unicode(text or u'').strip() if raw == u'': return {u'field': u'', u'function': u'SUM'} for c, lbl in _pw_cfg._PIVOT_DATA_FUNCTIONS: if c == u'LIST_ROWS' and raw.casefold() == lbl.casefold(): return {u'field': u'', u'function': u'LIST_ROWS'} if u' — ' in raw: left, field = raw.split(u' — ', 1) code = _pw_cfg._PIVOT_FN_CODE.get(left.strip().casefold(), u'SUM') for c, lbl in _pw_cfg._PIVOT_DATA_FUNCTIONS: if lbl == left.strip(): code = c break if code == u'LIST_ROWS': return {u'field': field.strip(), u'function': u'LIST_ROWS'} return {u'field': field.strip(), u'function': code} return {u'field': raw, u'function': u'SUM'} def _pivot_data_items_have_list_rows(items): for text in items or []: if _pivot_parse_data_field_item(text).get(u'function') == u'LIST_ROWS': return True return False def _pivot_dialog_sync_list_rows_as_values(dlg, force_as_values=False): """При Список_строк — as_values включён и заблокирован.""" chk = _dlg_control(dlg, u'ChkValues') if chk is None: return has_lr = _pivot_data_items_have_list_rows( _pivot_listbox_items(_dlg_control(dlg, u'LbData')) ) lock = bool(force_as_values or has_lr) try: if lock: chk.Model.State = 1 chk.Model.Enabled = not lock except Exception: try: if lock: chk.setState(1) chk.setEnable(not lock) except Exception: pass try: if has_lr and (not force_as_values): chk.Model.Label = u'Как значения (Список_строк требует материализации)' elif force_as_values: chk.Model.Label = u'Как значения (в Постобработка_xml всегда материализация)' else: chk.Model.Label = u'Как значения (копировать результат без формул)' except Exception: pass def _pivot_dialog_default_list_fields(dlg, count_field=u''): """До 3 полей не из ключей; иначе [count_field].""" used = set() for nm in (u'LbFilter', u'LbColumn', u'LbRow'): for item in _pivot_listbox_items(_dlg_control(dlg, nm)): s = unicode(item or u'').strip() if s != u'': used.add(s.casefold()) out = [] for item in _pivot_listbox_items(_dlg_control(dlg, u'LbAvail')): s = unicode(item or u'').strip() if s == u'' or s.casefold() in used: continue out.append(s) if len(out) >= 3: break if len(out) == 0: cf = unicode(count_field or u'').strip() if cf != u'': out = [cf] return out def _pivot_extract_list_fields_from_config(config): for item in (config or {}).get(u'data_fields') or []: if not isinstance(item, dict): continue fn = unicode(item.get(u'function') or u'').strip() code = _pw_cfg._PIVOT_FN_CODE.get(fn.casefold(), fn.upper().replace(u'-', u'_')) if code != u'LIST_ROWS' and fn.upper().replace(u'-', u'_') != u'LIST_ROWS': continue for key in getattr(_pw_cfg, u'_PIVOT_LIST_FIELDS_KEYS', (u'list_fields',)): if key not in item: continue val = item.get(key) if isinstance(val, (list, tuple)): return [unicode(x).strip() for x in val if unicode(x).strip()] s = unicode(val or u'').strip() if s == u'': return [] return [p.strip() for p in s.replace(u';', u',').split(u',') if p.strip()] return [] return [] def _pivot_dialog_used_field_names(dlg): used = [] for nm in (u'LbFilter', u'LbColumn', u'LbRow', u'LbData'): for item in _pivot_listbox_items(_dlg_control(dlg, nm)): if nm == u'LbData': item = _pivot_parse_data_field_item(item)[u'field'] if item not in used: used.append(item) return used def _pivot_resolve_doc(doc): if doc is not None: return doc try: return XSCRIPTCONTEXT.getDocument() except Exception: return None def _pivot_collect_field_names(lm, doc, source_sheet, header_row=u'', data_start=u'', data_end=u''): """Вызов lm_pp_pivot_collect_field_names с совместимостью со старой lib.""" if lm is None or doc is None: return [] spec = unicode(source_sheet or u'').strip() if spec == u'': return [] fn = getattr(lm, 'lm_pp_pivot_collect_field_names', None) if fn is None: return [] hr = unicode(header_row or u'').strip() ds = unicode(data_start or u'').strip() de = unicode(data_end or u'').strip() api_ver = int(getattr(lm, 'PIVOT_COLLECT_API', 0) or 0) kwargs = {} if hr != u'': kwargs['header_row'] = hr if ds != u'': kwargs['data_start'] = ds if de != u'': kwargs['data_end'] = de if api_ver >= 2 or len(kwargs) > 0: try: return list(fn(doc, spec, **kwargs)) except TypeError: pass except Exception: return [] try: return list(fn(doc, spec)) except TypeError: pass except Exception: pass return _pivot_collect_field_names_fallback(lm, doc, spec, hr, ds, de) def _pivot_collect_field_names_fallback(lm, doc, source_sheet, header_row, data_start, data_end): """Минимальный сбор заголовков, если lib без нового API.""" resolve = getattr(lm, '_lm_pp_pivot_resolve_source_sheet', None) if resolve is None: return [] sh = resolve(doc, source_sheet) if sh is None: return [] bounds = getattr(lm, '_lm_pp_pivot_resolve_bounds', None) read_hdr = getattr(lm, '_lm_pp_pivot_read_header_titles', None) cell_txt = getattr(lm, 'cell_text', None) if bounds is not None and read_hdr is not None: try: sc, sr, ec, er, hri = bounds(sh, header_row=header_row or None, data_start=data_start or None, data_end=data_end or None) ec = max(int(ec), int(sc) + 15) titles = read_hdr(sh, hri, sc, ec) names = [] seen = set() for title in titles: t = unicode(title or u'').strip() if t == u'': continue k = t.casefold() if k != u'data' and k not in seen: seen.add(k) names.append(t) return names except Exception: pass if cell_txt is None: return [] hri = 0 if unicode(header_row or u'').strip().isdigit(): hri = max(int(header_row) - 1, 0) names = [] seen = set() col = 0 while col < 64: try: t = unicode(cell_txt(sh.getCellByPosition(col, hri))).strip() except Exception: t = u'' if t == u'': if col > 4: break else: k = t.casefold() if k != u'data' and k not in seen: seen.add(k) names.append(t) col = col + 1 return names def _pivot_list_doc_sheet_names(doc): """Имена листов книги (для комбобокса источника).""" names = [] if doc is None: return names try: sheets = doc.getSheets() i = 0 while i < sheets.getCount(): try: names.append(unicode(sheets.getByIndex(i).Name)) except Exception: pass i = i + 1 except Exception: pass return names def _pivot_source_sheet_text(dlg): ctl = _dlg_control(dlg, u'CbSourceSheet') if ctl is None: ctl = _dlg_control(dlg, u'EdSourceSheet') if ctl is None: return u'' try: return unicode(ctl.getText()).strip() except Exception: return u'' def _pivot_canonical_storage_json(text): """Компактный JSON-массив для колонки C (без pretty-print).""" s = unicode(text or u'').strip() if s == u'': return u'' try: from libre_macros_param_codec import param_decode, param_encode blocks = param_decode(u'сводная_таблица', s) if not blocks: return s return param_encode(u'сводная_таблица', blocks) except Exception: return s def _pivot_write_json_to_param_row(sheet, row, pivot_json, primary=u'сводная_таблица'): """Записать JSON сводной в колонку C строки параметров.""" if sheet is None or pivot_json in (None, u''): return False try: r = int(row) except Exception: return False if r < 1: return False try: sheet.getCellByPosition(1, r).String = unicode(primary or u'сводная_таблица') sheet.getCellByPosition(2, r).String = _pivot_canonical_storage_json(pivot_json) return True except Exception as err: print(u'сводная: запись в лист параметров: %s' % err) return False def _pivot_dialog_read_range_args(dlg, doc=None, config_header=None): """Аргументы диапазона из полей диалога (пусто → авто при чтении).""" def _txt(name): ctl = _dlg_control(dlg, name) if ctl is None: return u'' try: return unicode(ctl.getText()).strip() except Exception: return u'' header_row = _txt(u'EdHeaderRow') if header_row == u'': if config_header not in (None, u''): header_row = unicode(config_header) else: hr = _pivot_header_row_from_params(doc) header_row = hr if hr not in (None, u'') else u'' return {'header_row': header_row, 'data_start': _txt(u'EdRangeStart'), 'data_end': _txt(u'EdRangeEnd')} def _pivot_dialog_refresh_avail_fields(dlg, lm, doc, lb_avail, config_header=None): dlg_id = id(dlg) if _pw_cfg._PIVOT_REFRESH_BUSY.get(dlg_id): return _pw_cfg._PIVOT_REFRESH_BUSY[dlg_id] = True try: ed = _dlg_control(dlg, u'EdSourceSheet') spec = _pivot_source_sheet_text(dlg) names = [] args = {} if spec != u'': args = _pivot_dialog_read_range_args(dlg, doc=doc, config_header=config_header) names = _pivot_collect_field_names(lm, doc, spec, header_row=args.get('header_row', u''), data_start=args.get('data_start', u''), data_end=args.get('data_end', u'')) used = set(_pivot_dialog_used_field_names(dlg)) avail = [n for n in names if n not in used] _pivot_listbox_set_items(lb_avail, avail) lbl_hint = _dlg_control(dlg, u'LblHint') if lbl_hint is not None: try: if spec == u'': lbl_hint.Model.Label = u'Укажите лист-источник — подтянутся заголовки столбцов.' elif len(names) == 0: sh = None try: sh = lm._lm_pp_pivot_resolve_source_sheet(doc, spec) except Exception: pass if sh is None: lbl_hint.Model.Label = u'Лист «%s» не найден — укажите имя или шаблон (Заказы, 1_Заказы).' % spec else: hr = args.get('header_row') or u'1' lbl_hint.Model.Label = u'Лист «%s» найден, заголовки не прочитаны — проверьте строку %s или укажите A1…K в начале/конце.' % (spec, hr) else: lbl_hint.Model.Label = u'Перетащите поля кнопками → в строки, столбцы, данные или фильтры.' except Exception: pass except Exception as err: print(u'сводная: обновление полей: %s' % err) finally: _pw_cfg._PIVOT_REFRESH_BUSY[dlg_id] = False def show_pivot_table_param_dialog(parent_dialog=None, initial=None, doc=None, param_sheet=None, param_row=None, param_primary=None, force_as_values=False): """ Диалог макета сводной таблицы (как show_vlookup_param_dialog — встроен в param_wizard). Возвращает JSON-строку для колонки C или None при отмене. Заголовки столбцов — с листа source_sheet из JSON (не с листа параметров). force_as_values — для Постобработка_xml: галка «Как значения» включена и заблокирована (в xml только материализованный отчёт, не DataPilot). """ lm = _pivot_lm() if lm is None: _wp_message_box_execute(doc, parent_dialog, MESSAGEBOX, MSG_BUTTONS.BUTTONS_OK, u'Сводная таблица', u'Не удалось загрузить libre_macros_lib.') return None doc = _pivot_resolve_doc(doc) force_as_values = bool(force_as_values) try: if initial in (None, u''): config = lm.lm_pp_pivot_default_config() else: from libre_macros_param_codec import param_decode try: blocks = param_decode(u'сводная_таблица', unicode(initial).strip()) if blocks: config = dict(blocks[0]) else: raise ValueError('empty pivot config') except Exception as err: try: config = lm.lm_pp_pivot_config_from_json(initial) except Exception: _wp_message_box_execute(doc, parent_dialog, MESSAGEBOX, MSG_BUTTONS.BUTTONS_OK, u'Сводная таблица', u'Не удалось разобрать JSON (%s). Открыт пустой макет.' % err) config = lm.lm_pp_pivot_default_config() if force_as_values: config[u'as_values'] = True source_hint = unicode(config.get(u'source_sheet') or u'').strip() config_header_row = config.get(u'header_row') config_data_start = unicode(config.get(u'data_start') or u'').strip() config_data_end = unicode(config.get(u'data_end') or u'').strip() ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) dm = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialogModel', ctx) dw, m = (700, 8) g = 4 gg = 10 lbl_w = 140 hdr_w = 48 range_w = hdr_w * 2 x_f = m + lbl_w + g list_h = 168 hint_h = 24 row_h = 20 btn_h = 22 dm.PositionX = 60 dm.PositionY = 40 dm.Width = dw dm.Title = u'Параметры сводной таблицы' y = m hint_text = u'Диапазон: A1, K100 или пусто (значащая область). Перетащите поля кнопками в нужный блок справа.' _wizard_dlg_add_fixed(dm, u'LblHint', hint_text, m, y, dw - 2 * m, hint_h) y = y + hint_h + 2 _wizard_dlg_add_fixed(dm, u'LblSource', u'Лист-источник (данные):', m, y + 2, lbl_w, 14) _wizard_dlg_add_combo(dm, u'CbSourceSheet', x_f, y, dw - m - x_f, 18) y = y + row_h + 2 _wizard_dlg_add_fixed(dm, u'LblHeaderRow', u'Строка заголовков (пусто=1):', m, y + 2, lbl_w, 14) _wizard_dlg_add_edit(dm, u'EdHeaderRow', x_f, y, hdr_w, 18) y = y + row_h + 2 _wizard_dlg_add_fixed(dm, u'LblRangeStart', u'Начало (A1):', m, y + 2, lbl_w, 14) x_start = x_f _wizard_dlg_add_edit(dm, u'EdRangeStart', x_start, y, range_w, 18) x_end_lbl = x_start + range_w + gg _wizard_dlg_add_fixed(dm, u'LblRangeEnd', u'Конец (K100):', x_end_lbl, y + 2, 78, 14) x_end = x_end_lbl + 78 + g _wizard_dlg_add_edit(dm, u'EdRangeEnd', x_end, y, range_w, 18) y = y + row_h + 4 left_w = 172 mid_w = 118 right_w = dw - 2 * m - left_w - mid_w - 8 # Левая колонка: подпись и список «Доступные поля». _wizard_dlg_add_fixed(dm, u'LblAvail', u'Доступные поля', m, y, left_w, 14) avail_list_y = y + 14 _wizard_dlg_add_listbox(dm, u'LbAvail', m, avail_list_y, left_w, list_h) # Средние кнопки — равномерно по высоте списка доступных полей. bx = m + left_w + 4 mid_btns = ( (u'BtnToFilter', u'→ Фильтры'), (u'BtnToCol', u'→ Столбцы'), (u'BtnToRow', u'→ Строки'), (u'BtnToData', u'→ Данные'), (u'BtnFrom', u'← Назад'), ) n_btn = len(mid_btns) mid_btn_h = 20 if n_btn <= 1: mid_step = 0.0 else: mid_step = float(list_h - mid_btn_h) / float(n_btn - 1) bi = 0 while bi < n_btn: by = int(round(avail_list_y + mid_step * bi)) _wizard_dlg_add_button(dm, mid_btns[bi][0], mid_btns[bi][1], bx, by, mid_w, mid_btn_h) bi = bi + 1 # Правые зоны: верх «Фильтры» = верх «Доступные поля»; низ = низ списка. rx = m + left_w + mid_w + 8 right_zones = ( (u'LblFilterHdr', u'Фильтры (страницы)', u'LbFilter'), (u'LblColHdr', u'Столбцы', u'LbColumn'), (u'LblRowHdr', u'Строки', u'LbRow'), (u'LblDataHdr', u'Данные', u'LbData'), ) right_top = y right_bottom = avail_list_y + list_h n_z = len(right_zones) zone_gap = 3 lbl_hh = 12 usable = right_bottom - right_top - n_z * lbl_hh - (n_z - 1) * zone_gap box_h = max(22, int(usable / n_z)) used = n_z * (lbl_hh + box_h) + (n_z - 1) * zone_gap extra = right_bottom - right_top - used ry = right_top zi = 0 while zi < n_z: z = right_zones[zi] h = box_h + (extra if zi == n_z - 1 else 0) _wizard_dlg_add_fixed(dm, z[0], z[1], rx, ry, right_w, lbl_hh) _wizard_dlg_add_listbox(dm, z[2], rx, ry + lbl_hh, right_w, h) ry = ry + lbl_hh + h + zone_gap zi = zi + 1 y3 = avail_list_y + list_h + 6 x3 = m lbl_fn_w = 58 _wizard_dlg_add_fixed(dm, u'LblFn', u'Функция:', x3, y3 + 2, lbl_fn_w, 14) x3 = x3 + lbl_fn_w + g cb_fn_w = 108 _wizard_dlg_add_combo(dm, u'CbFn', x3, y3, cb_fn_w, 18) x3 = x3 + cb_fn_w + gg lbl_sheet_w = 98 _wizard_dlg_add_fixed(dm, u'LblSheet', u'Лист сводной:', x3, y3 + 2, lbl_sheet_w, 14) x3 = x3 + lbl_sheet_w + g _wizard_dlg_add_edit(dm, u'EdSheet', x3, y3, dw - m - x3, 18) y3 = y3 + row_h + 4 list_fields_h = 36 _wizard_dlg_add_fixed(dm, u'LblListFields', u'Поля списка (Список_строк):', m, y3, dw - 2 * m - 100, 14) y3 = y3 + 14 lf_btn_w = 52 lf_list_w = dw - 2 * m - lf_btn_w - 8 _wizard_dlg_add_listbox(dm, u'LbListFields', m, y3, lf_list_w, list_fields_h, multiselect=True) _wizard_dlg_add_button(dm, u'BtnListAdd', u'+', m + lf_list_w + 4, y3, lf_btn_w, 18) _wizard_dlg_add_button(dm, u'BtnListDel', u'×', m + lf_list_w + 4, y3 + 18, lf_btn_w, 18) y3 = y3 + list_fields_h + 4 # Две галочки в один ряд: разворот записей + показывать имена полей. half = int((dw - 2 * m - 8) / 2) _wizard_dlg_add_checkbox( dm, u'ChkListExpand', u'Каждая запись на отдельной строке', m, y3, half, 16, ) _wizard_dlg_add_checkbox( dm, u'ChkListNames', u'Имена полей (Имя: значение)', m + half + 8, y3, half, 16, ) y3 = y3 + 18 # Компактные разделители. _wizard_dlg_add_fixed(dm, u'LblNameSep', u'Имя↔знач.:', m, y3 + 2, 64, 14) _wizard_dlg_add_edit(dm, u'EdNameSep', m + 64, y3, 56, 18) _wizard_dlg_add_fixed(dm, u'LblRecSep', u'Записи:', m + 128, y3 + 2, 48, 14) _wizard_dlg_add_edit(dm, u'EdRecSep', m + 176, y3, 72, 18) _wizard_dlg_add_fixed(dm, u'LblSepHint', u'\\n = перенос строки', m + 256, y3 + 2, 140, 14) y3 = y3 + 22 chk_label = u'Как значения (в Постобработка_xml всегда материализация)' if force_as_values else u'Как значения (Список_строк требует материализации)' _wizard_dlg_add_checkbox(dm, u'ChkValues', chk_label, m, y3, dw - 2 * m, 16) btn_y = y3 + 20 dm.Height = btn_y + btn_h + m _wizard_dlg_add_button(dm, u'BtnOk', u'OK', dw - m - 180, btn_y, 80, btn_h) _wizard_dlg_add_button(dm, u'BtnCancel', u'Отмена', dw - m - 90, btn_y, 80, btn_h) dlg = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialog', ctx) dlg.setModel(dm) if not _nested_subdialog_create_peer( dlg, toolkit, doc, parent_dialog, center_in_main_wizard=bool(parent_dialog) ): _wp_message_box_execute(doc, parent_dialog, MESSAGEBOX, MSG_BUTTONS.BUTTONS_OK, u'Сводная таблица', u'Не удалось создать окно диалога (createPeer).') return None result = {u'cancel': True} lb_avail = _dlg_control(dlg, u'LbAvail') ed_sheet = _dlg_control(dlg, u'EdSheet') cb_source = _dlg_control(dlg, u'CbSourceSheet') ed_header = _dlg_control(dlg, u'EdHeaderRow') ed_rstart = _dlg_control(dlg, u'EdRangeStart') ed_rend = _dlg_control(dlg, u'EdRangeEnd') if cb_source is not None: sheet_names = _pivot_list_doc_sheet_names(doc) try: cb_source.Model.StringItemList = tuple(sheet_names) except Exception: pass if source_hint != u'': try: cb_source.setText(source_hint) except Exception: pass elif len(sheet_names) > 0: try: cb_source.setText(sheet_names[0]) except Exception: pass if ed_header is not None and config_header_row not in (None, u''): ed_header.setText(unicode(config_header_row)) if ed_rstart is not None and config_data_start != u'': ed_rstart.setText(config_data_start) if ed_rend is not None and config_data_end != u'': ed_rend.setText(config_data_end) if ed_sheet is not None: ed_sheet.setText(unicode(config.get(u'sheet_name') or u'')) _pivot_listbox_set_items(_dlg_control(dlg, u'LbFilter'), config.get(u'filter_fields') or []) _pivot_listbox_set_items(_dlg_control(dlg, u'LbColumn'), config.get(u'column_fields') or []) _pivot_listbox_set_items(_dlg_control(dlg, u'LbRow'), config.get(u'row_fields') or []) data_items = [] for item in config.get(u'data_fields') or []: if isinstance(item, dict): data_items.append(_pivot_data_field_label(item.get(u'field', u''), item.get(u'function', u'SUM'))) _pivot_listbox_set_items(_dlg_control(dlg, u'LbData'), data_items) init_list_fields = _pivot_extract_list_fields_from_config(config) _pivot_listbox_set_items(_dlg_control(dlg, u'LbListFields'), init_list_fields) chk_expand = _dlg_control(dlg, u'ChkListExpand') if chk_expand is not None: try: chk_expand.Model.State = 1 if config.get(u'list_rows_expand') else 0 except Exception: pass chk_names = _dlg_control(dlg, u'ChkListNames') if chk_names is not None: try: include_names = config.get(u'list_rows_include_names') if include_names is None: include_names = True chk_names.Model.State = 1 if include_names else 0 except Exception: pass def _pivot_sep_to_edit(raw, default_val): s = unicode(raw if raw is not None else default_val) if s == u'\n': return u'\\n' if s == u'\t': return u'\\t' return s.replace(u'\n', u'\\n').replace(u'\t', u'\\t') ed_name_sep = _dlg_control(dlg, u'EdNameSep') if ed_name_sep is not None: try: ed_name_sep.setText( _pivot_sep_to_edit( config.get(u'list_rows_name_sep'), u': ', ) ) except Exception: pass ed_rec_sep = _dlg_control(dlg, u'EdRecSep') if ed_rec_sep is not None: try: ed_rec_sep.setText( _pivot_sep_to_edit( config.get(u'list_rows_record_sep'), u'\\n', ) ) except Exception: pass if initial in (None, u''): try: _ra = _pivot_dialog_read_range_args(dlg, doc=doc, config_header=config_header_row) probe = _pivot_collect_field_names(lm, doc, source_hint, header_row=_ra.get('header_row', u''), data_start=_ra.get('data_start', u''), data_end=_ra.get('data_end', u'')) if len(probe) > 0: config = lm.lm_pp_pivot_default_config(probe) except Exception: pass _pivot_dialog_refresh_avail_fields(dlg, lm, doc, lb_avail, config_header=config_header_row) cb_fn = _dlg_control(dlg, u'CbFn') if cb_fn is not None: labels = [lbl for _c, lbl in _pw_cfg._PIVOT_DATA_FUNCTIONS] try: cb_fn.Model.StringItemList = tuple(labels) cb_fn.setText(labels[0]) except Exception: pass ed_sheet = _dlg_control(dlg, u'EdSheet') chk = _dlg_control(dlg, u'ChkValues') if chk is not None: try: chk.Model.State = 1 if force_as_values or config.get(u'as_values') else 0 except Exception: pass _pivot_dialog_sync_list_rows_as_values(dlg, force_as_values=force_as_values) if force_as_values and chk is not None: try: chk.Model.Enabled = False except Exception: try: chk.setEnable(False) except Exception: pass class _PivotSourceListener(unohelper.Base, XTextListener, XItemListener): def disposing(self, event): pass def itemStateChanged(self, event): try: nm = event.Source.getModel().Name except Exception: nm = u'' if nm == u'CbSourceSheet': try: _pivot_dialog_refresh_avail_fields(dlg, lm, doc, lb_avail, config_header=None) except Exception as exc: print(u'сводная: itemChanged: %s' % exc) def textChanged(self, event): try: nm = event.Source.getModel().Name except Exception: try: nm = event.Source.Model.Name except Exception: nm = u'' if nm in (u'CbSourceSheet', u'EdHeaderRow', u'EdRangeStart', u'EdRangeEnd'): try: _pivot_dialog_refresh_avail_fields(dlg, lm, doc, lb_avail, config_header=None) except Exception as exc: print(u'сводная: textChanged: %s' % exc) class _PivotHandler(unohelper.Base, XActionListener): def disposing(self, event): pass def actionPerformed(self, event): try: btn = event.Source.getModel().Name except Exception: try: btn = event.Source.Model.Name except Exception: return if btn == u'BtnCancel': result[u'cancel'] = True dlg.endExecute() return if btn == u'BtnOk': result[u'cancel'] = False dlg.endExecute() return fn_code = u'SUM' if cb_fn is not None: try: txt = unicode(cb_fn.getText()).strip() fn_code = _pw_cfg._PIVOT_FN_CODE.get(txt.casefold(), u'SUM') except Exception: pass if btn == u'BtnToFilter': _pivot_listbox_move_selected(lb_avail, _dlg_control(dlg, u'LbFilter')) elif btn == u'BtnToCol': _pivot_listbox_move_selected(lb_avail, _dlg_control(dlg, u'LbColumn')) elif btn == u'BtnToRow': _pivot_listbox_move_selected(lb_avail, _dlg_control(dlg, u'LbRow')) elif btn == u'BtnToData': indices = sorted(_pivot_listbox_selected_indices(lb_avail), reverse=True) dst = _dlg_control(dlg, u'LbData') if fn_code == u'LIST_ROWS': # Список_строк — псевдомера: не забирает поле из «Доступные». _pivot_listbox_set_items( dst, [_pivot_data_field_label(u'', u'LIST_ROWS')] ) lb_lf = _dlg_control(dlg, u'LbListFields') if lb_lf is not None and len(_pivot_listbox_items(lb_lf)) == 0: _pivot_listbox_set_items( lb_lf, _pivot_dialog_default_list_fields(dlg, u''), ) chk_exp = _dlg_control(dlg, u'ChkListExpand') if chk_exp is not None: try: chk_exp.Model.State = 1 except Exception: pass _pivot_dialog_sync_list_rows_as_values( dlg, force_as_values=force_as_values ) else: dst_items = _pivot_listbox_items(dst) # Убрать прежний LIST_ROWS при добавлении обычной меры. cleaned = [] for it in dst_items: if _pivot_parse_data_field_item(it).get(u'function') != u'LIST_ROWS': cleaned.append(it) dst_items = cleaned for idx in indices: try: nm = unicode(lb_avail.getItem(idx)) dst_items.append(_pivot_data_field_label(nm, fn_code)) lb_avail.removeItems(idx, 1) except Exception: pass _pivot_listbox_set_items(dst, dst_items) _pivot_dialog_sync_list_rows_as_values( dlg, force_as_values=force_as_values ) elif btn == u'BtnListAdd': lb_lf = _dlg_control(dlg, u'LbListFields') if lb_lf is None: return cur = _pivot_listbox_items(lb_lf) for idx in _pivot_listbox_selected_indices(lb_avail): try: nm = unicode(lb_avail.getItem(idx)) if nm not in cur: cur.append(nm) except Exception: pass _pivot_listbox_set_items(lb_lf, cur) elif btn == u'BtnListDel': lb_lf = _dlg_control(dlg, u'LbListFields') if lb_lf is None: return indices = sorted(_pivot_listbox_selected_indices(lb_lf), reverse=True) for idx in indices: try: lb_lf.removeItems(idx, 1) except Exception: pass elif btn == u'BtnFrom': for src_name in (u'LbFilter', u'LbColumn', u'LbRow', u'LbData'): src = _dlg_control(dlg, src_name) indices = sorted(_pivot_listbox_selected_indices(src), reverse=True) for idx in indices: try: text = unicode(src.getItem(idx)) if src_name == u'LbData': parsed = _pivot_parse_data_field_item(text) if parsed.get(u'function') == u'LIST_ROWS': src.removeItems(idx, 1) continue text = parsed[u'field'] if text == u'': src.removeItems(idx, 1) continue avail_items = _pivot_listbox_items(lb_avail) if text not in avail_items: avail_items.append(text) _pivot_listbox_set_items(lb_avail, avail_items) src.removeItems(idx, 1) except Exception: pass _pivot_dialog_sync_list_rows_as_values( dlg, force_as_values=force_as_values ) handler = _PivotHandler() source_listener = _PivotSourceListener() for nm in (u'BtnOk', u'BtnCancel', u'BtnToFilter', u'BtnToCol', u'BtnToRow', u'BtnToData', u'BtnFrom', u'BtnListAdd', u'BtnListDel'): ctl = _dlg_control(dlg, nm) if ctl is not None: ctl.addActionListener(handler) if cb_source is not None: try: cb_source.addTextListener(source_listener) except Exception: pass try: cb_source.addItemListener(source_listener) except Exception: pass for _ed in (ed_header, ed_rstart, ed_rend): if _ed is not None: try: _ed.addTextListener(source_listener) except Exception: pass dlg.execute() if result.get(u'cancel'): return None out = lm.lm_pp_pivot_default_config() out[u'filter_fields'] = _pivot_listbox_items(_dlg_control(dlg, u'LbFilter')) out[u'column_fields'] = _pivot_listbox_items(_dlg_control(dlg, u'LbColumn')) out[u'row_fields'] = _pivot_listbox_items(_dlg_control(dlg, u'LbRow')) out[u'data_fields'] = [_pivot_parse_data_field_item(x) for x in _pivot_listbox_items(_dlg_control(dlg, u'LbData'))] list_fields = _pivot_listbox_items(_dlg_control(dlg, u'LbListFields')) has_list_rows = False for item in out[u'data_fields']: if item.get(u'function') == u'LIST_ROWS': has_list_rows = True if len(list_fields) == 0: _wp_message_box_execute(doc, parent_dialog, MESSAGEBOX, MSG_BUTTONS.BUTTONS_OK, u'Сводная таблица', u'Для «Список_строк» укажите хотя бы одно поле в «Поля списка».') return None item[u'list_fields'] = list(list_fields) # COUNT-поле подберёт исполнитель; в JSON можно оставить пустым. if not unicode(item.get(u'field') or u'').strip(): item[u'field'] = list_fields[0] if has_list_rows and len(out[u'data_fields']) > 1: _wp_message_box_execute(doc, parent_dialog, MESSAGEBOX, MSG_BUTTONS.BUTTONS_OK, u'Сводная таблица', u'Список_строк в v1 — единственное поле данных. Уберите остальные меры.') return None chk_expand = _dlg_control(dlg, u'ChkListExpand') if has_list_rows and chk_expand is not None: try: out[u'list_rows_expand'] = int(chk_expand.Model.State) == 1 except Exception: out[u'list_rows_expand'] = False elif u'list_rows_expand' in out: del out[u'list_rows_expand'] chk_names = _dlg_control(dlg, u'ChkListNames') if has_list_rows and chk_names is not None: try: out[u'list_rows_include_names'] = int(chk_names.Model.State) == 1 except Exception: out[u'list_rows_include_names'] = True if out.get(u'list_rows_include_names') is not False: # default True — в JSON не пишем, если True if u'list_rows_include_names' in out and out[u'list_rows_include_names']: del out[u'list_rows_include_names'] elif u'list_rows_include_names' in out: del out[u'list_rows_include_names'] def _pivot_edit_to_sep(raw, default_val): s = unicode(raw or u'').strip() if s == u'': return default_val # одиночные escape-последовательности if s == u'\\n': return u'\n' if s == u'\\t': return u'\t' return s.replace(u'\\n', u'\n').replace(u'\\t', u'\t') if has_list_rows: ed_name_sep = _dlg_control(dlg, u'EdNameSep') if ed_name_sep is not None: try: name_sep = _pivot_edit_to_sep(ed_name_sep.getText(), u': ') except Exception: name_sep = u': ' if name_sep != u': ': out[u'list_rows_name_sep'] = name_sep elif u'list_rows_name_sep' in out: del out[u'list_rows_name_sep'] ed_rec_sep = _dlg_control(dlg, u'EdRecSep') if ed_rec_sep is not None: try: rec_sep = _pivot_edit_to_sep(ed_rec_sep.getText(), u'\n') except Exception: rec_sep = u'\n' if rec_sep != u'\n': out[u'list_rows_record_sep'] = rec_sep elif u'list_rows_record_sep' in out: del out[u'list_rows_record_sep'] else: for _k in ( u'list_rows_include_names', u'list_rows_name_sep', u'list_rows_record_sep', ): if _k in out: del out[_k] if ed_sheet is not None: out[u'sheet_name'] = unicode(ed_sheet.getText()).strip() src_txt = _pivot_source_sheet_text(dlg) if src_txt != u'': out[u'source_sheet'] = src_txt if ed_header is not None: hr_txt = unicode(ed_header.getText()).strip() if hr_txt != u'': out[u'header_row'] = hr_txt if ed_rstart is not None: rs_txt = unicode(ed_rstart.getText()).strip() if rs_txt != u'': out[u'data_start'] = rs_txt if ed_rend is not None: re_txt = unicode(ed_rend.getText()).strip() if re_txt != u'': out[u'data_end'] = re_txt if force_as_values or has_list_rows: out[u'as_values'] = True elif chk is not None: try: out[u'as_values'] = int(chk.Model.State) == 1 except Exception: out[u'as_values'] = False if len(out[u'data_fields']) == 0: _wp_message_box_execute(doc, parent_dialog, MESSAGEBOX, MSG_BUTTONS.BUTTONS_OK, u'Сводная таблица', u'Укажите хотя бы одно поле в «Данные».') return None from libre_macros_param_codec import param_encode json_out = param_encode(u'сводная_таблица', [out]) json_out = _pivot_canonical_storage_json(json_out) or json_out if param_sheet is not None and param_row is not None: primary_save = unicode(param_primary or u'сводная_таблица') _pivot_write_json_to_param_row(param_sheet, param_row, json_out, primary=primary_save) return json_out except Exception as exc: print(u'Ошибка диалога сводной таблицы: %s' % exc) _wp_message_box_execute(doc, parent_dialog, MESSAGEBOX, MSG_BUTTONS.BUTTONS_OK, u'Ошибка', u'Ошибка диалога сводной таблицы: %s' % exc) return None def create_param_wizard_dialog(doc, sheet, catalog, initial=None, sheet_names=None): try: ctx = XSCRIPTCONTEXT.getComponentContext() sm = ctx.getServiceManager() toolkit = sm.createInstanceWithContext('com.sun.star.awt.Toolkit', ctx) dialog_model = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialogModel', ctx) dialog_model.PositionX = 80 dialog_model.PositionY = 60 dialog_model.Width = _pw_cfg._DLG_W dialog_model.Height = _pw_cfg._DLG_H dialog_model.Title = u'Параметр collect_workbooks' g = _pw_cfg._ui_v(8) tab_w = 84 tab_x = _pw_cfg._M + _pw_cfg._SHEET_COMBO_W + g preset_btn_h = _pw_cfg._CTRL_H content_w = _pw_cfg._FIELD_W preset_y = _pw_cfg._PAGE_TOP preset_bw = (content_w - g) // 2 _wizard_dlg_add_combo(dialog_model, 'SheetParamCombo', _pw_cfg._M, _pw_cfg._TAB_Y, _pw_cfg._SHEET_COMBO_W, _pw_cfg._CTRL_H) _wizard_dlg_add_button(dialog_model, 'TabBtnParam', u'Параметр', tab_x, _pw_cfg._TAB_Y, tab_w, _pw_cfg._CTRL_H) _wizard_dlg_add_button(dialog_model, 'TabBtnPresets', u'Пресеты', tab_x + tab_w + g, _pw_cfg._TAB_Y, tab_w, _pw_cfg._CTRL_H) _wizard_dlg_add_button(dialog_model, 'TabBtnGlobal', u'Глобальные', tab_x + 2 * (tab_w + g), _pw_cfg._TAB_Y, tab_w, _pw_cfg._CTRL_H) _wizard_dlg_add_fixed(dialog_model, 'LblNewParamMode', _new_param_mode_status_text(False), _pw_cfg._M, _pw_cfg._NEW_MODE_Y, _pw_cfg._MODE_LBL_W, _pw_cfg._MODE_ROW_H, align=0, vertical_align=VERT_ALIGN_MIDDLE) _wizard_dlg_add_button(dialog_model, 'NewParamModeBtn', _pw_cfg._MODE_BTN_LABEL_EDIT, _pw_cfg._MODE_BTN_X, _pw_cfg._NEW_MODE_Y, _pw_cfg._MODE_BTN_W, _pw_cfg._MODE_ROW_H) lbl_param = dialog_model.createInstance('com.sun.star.awt.UnoControlFixedTextModel') lbl_param.PositionX = _pw_cfg._M lbl_param.PositionY = _pw_cfg._PAGE_TOP lbl_param.Width = 100 lbl_param.Height = _pw_cfg._LBL_H lbl_param.Label = u'Параметр:' lbl_param.Name = 'LblParam' param_combo = dialog_model.createInstance('com.sun.star.awt.UnoControlComboBoxModel') param_combo.PositionX = _pw_cfg._M param_combo.PositionY = _pw_cfg._PAGE_TOP + _pw_cfg._ui_v(20) param_combo.Width = _pw_cfg._FIELD_W param_combo.Height = _pw_cfg._CTRL_H param_combo.Name = 'ParamCombo' param_combo.Dropdown = True param_combo.LineCount = 12 _wizard_dlg_add_checkbox(dialog_model, 'ParamDisabledCheck', u'Отключить параметр (__ в колонке A)', _pw_cfg._M, _pw_cfg._PARAM_DISABLED_CHK_Y, _pw_cfg._FIELD_W, _pw_cfg._PARAM_DISABLED_CHK_H) lbl_value = dialog_model.createInstance('com.sun.star.awt.UnoControlFixedTextModel') lbl_value.PositionX = _pw_cfg._M lbl_value.PositionY = _pw_cfg._VALUE_LBL_Y lbl_value.Width = _pw_cfg._FIELD_W lbl_value.Height = _pw_cfg._LBL_H lbl_value.Label = u'Значение (B):' lbl_value.Name = 'LblValue' value_edit = dialog_model.createInstance('com.sun.star.awt.UnoControlEditModel') value_edit.PositionX = _pw_cfg._M value_edit.PositionY = _pw_cfg._VALUE_CTRL_Y value_edit.Width = _pw_cfg._FIELD_W value_edit.Height = _pw_cfg._VALUE_CTRL_H value_edit.Name = 'ValueEdit' value_combo = dialog_model.createInstance('com.sun.star.awt.UnoControlComboBoxModel') value_combo.PositionX = _pw_cfg._M value_combo.PositionY = _pw_cfg._VALUE_CTRL_Y value_combo.Width = _pw_cfg._FIELD_W value_combo.Height = _pw_cfg._CTRL_H value_combo.Name = 'ValueCombo' value_combo.Dropdown = True value_combo.LineCount = 12 lbl_transfer = dialog_model.createInstance('com.sun.star.awt.UnoControlFixedTextModel') lbl_transfer.PositionX = _pw_cfg._M lbl_transfer.PositionY = _pw_cfg._TRANSFER_PAIR_LBL_Y lbl_transfer.Width = _pw_cfg._FIELD_W lbl_transfer.Height = _pw_cfg._LBL_H lbl_transfer.Label = u'Способ_переноса (C):' lbl_transfer.Name = 'LblTransfer' transfer_combo = dialog_model.createInstance('com.sun.star.awt.UnoControlComboBoxModel') transfer_combo.PositionX = _pw_cfg._M transfer_combo.PositionY = _pw_cfg._TRANSFER_PAIR_CTRL_Y transfer_combo.Width = _pw_cfg._FIELD_W transfer_combo.Height = _pw_cfg._CTRL_H transfer_combo.Name = 'TransferCombo' transfer_combo.Dropdown = True transfer_combo.LineCount = 8 browse_btn = dialog_model.createInstance('com.sun.star.awt.UnoControlButtonModel') browse_btn.PositionX = _pw_cfg._M browse_btn.PositionY = _pw_cfg._VALUE_BTNS_Y browse_btn.Width = _pw_cfg._BROWSE_W browse_btn.Height = _pw_cfg._FILES_BTN_H browse_btn.Label = u'Обзор…' browse_btn.Name = 'BrowseButton' files_clear_last_btn = dialog_model.createInstance('com.sun.star.awt.UnoControlButtonModel') files_clear_last_btn.PositionX = _pw_cfg._M + _pw_cfg._BROWSE_W + _pw_cfg._FILES_BTN_GAP files_clear_last_btn.PositionY = _pw_cfg._VALUE_BTNS_Y files_clear_last_btn.Width = _pw_cfg._FILES_BTN_W files_clear_last_btn.Height = _pw_cfg._FILES_BTN_H files_clear_last_btn.Label = u'Очистить последний' files_clear_last_btn.Name = 'FilesClearLastBtn' files_clear_all_btn = dialog_model.createInstance('com.sun.star.awt.UnoControlButtonModel') files_clear_all_btn.PositionX = _pw_cfg._M + _pw_cfg._BROWSE_W + _pw_cfg._FILES_BTN_GAP + _pw_cfg._FILES_BTN_W + _pw_cfg._FILES_BTN_GAP files_clear_all_btn.PositionY = _pw_cfg._VALUE_BTNS_Y files_clear_all_btn.Width = _pw_cfg._FILES_BTN_W files_clear_all_btn.Height = _pw_cfg._FILES_BTN_H files_clear_all_btn.Label = u'Очистить все' files_clear_all_btn.Name = 'FilesClearAllBtn' lbl_extra = dialog_model.createInstance('com.sun.star.awt.UnoControlFixedTextModel') lbl_extra.PositionX = _pw_cfg._M lbl_extra.PositionY = _pw_cfg._EXTRA_LBL_Y lbl_extra.Width = 160 lbl_extra.Height = _pw_cfg._LBL_H lbl_extra.Label = u'Доп. аргументы (C):' lbl_extra.Name = 'LblExtra' lbl_plugin = dialog_model.createInstance('com.sun.star.awt.UnoControlFixedTextModel') lbl_plugin.PositionX = _pw_cfg._M lbl_plugin.PositionY = _pw_cfg._EXTRA_LBL_Y lbl_plugin.Width = 200 lbl_plugin.Height = _pw_cfg._LBL_H lbl_plugin.Label = u'Функция/код (C):' lbl_plugin.Name = 'LblPlugin' plugin_edit = dialog_model.createInstance('com.sun.star.awt.UnoControlEditModel') plugin_edit.PositionX = _pw_cfg._M plugin_edit.PositionY = _pw_cfg._EXTRA_EDIT_Y plugin_edit.Width = _pw_cfg._FIELD_W - 110 plugin_edit.Height = _pw_cfg._CTRL_H plugin_edit.Name = 'PluginEdit' plugin_editor_btn = dialog_model.createInstance('com.sun.star.awt.UnoControlButtonModel') plugin_editor_btn.PositionX = _pw_cfg._M + _pw_cfg._FIELD_W - 100 plugin_editor_btn.PositionY = _pw_cfg._EXTRA_EDIT_Y plugin_editor_btn.Width = 100 plugin_editor_btn.Height = _pw_cfg._CTRL_H plugin_editor_btn.Label = u'Редактор...' plugin_editor_btn.Name = 'PluginEditorBtn' extra_edit = dialog_model.createInstance('com.sun.star.awt.UnoControlEditModel') extra_edit.PositionX = _pw_cfg._M extra_edit.PositionY = _pw_cfg._EXTRA_EDIT_Y extra_edit.Width = _pw_cfg._FIELD_W - 110 extra_edit.Height = _pw_cfg._CTRL_H extra_edit.Name = 'ExtraEdit' pivot_params_btn = dialog_model.createInstance('com.sun.star.awt.UnoControlButtonModel') pivot_params_btn.PositionX = _pw_cfg._M + _pw_cfg._FIELD_W - 100 pivot_params_btn.PositionY = _pw_cfg._EXTRA_EDIT_Y pivot_params_btn.Width = 100 pivot_params_btn.Height = _pw_cfg._CTRL_H pivot_params_btn.Label = u'Параметры...' pivot_params_btn.Name = 'PivotParamsBtn' col_c_params_btn = dialog_model.createInstance('com.sun.star.awt.UnoControlButtonModel') col_c_params_btn.PositionX = _pw_cfg._M + _pw_cfg._FIELD_W - 100 col_c_params_btn.PositionY = _pw_cfg._VALUE_CTRL_Y col_c_params_btn.Width = 100 col_c_params_btn.Height = _pw_cfg._CTRL_H col_c_params_btn.Label = u'Параметры...' col_c_params_btn.Name = 'ColCParamsBtn' _wizard_dlg_add_fixed(dialog_model, 'SourceExtraLbl', u'Столбцы источников (B, C, D…):', _pw_cfg._M, _pw_cfg._VALUE_LBL_Y, _pw_cfg._FIELD_W, _pw_cfg._LBL_H) _wizard_dlg_add_listbox(dialog_model, 'SourceExtraList', _pw_cfg._M, _pw_cfg._SOURCE_EXTRA_LIST_Y, _pw_cfg._FIELD_W, _pw_cfg._SOURCE_EXTRA_LIST_H, multiselect=False) source_extra_btn = dialog_model.createInstance('com.sun.star.awt.UnoControlButtonModel') source_extra_btn.PositionX = _pw_cfg._M source_extra_btn.PositionY = _pw_cfg._SOURCE_EXTRA_BTN_Y source_extra_btn.Width = _pw_cfg._SOURCE_EXTRA_BTN_W source_extra_btn.Height = _pw_cfg._CTRL_H source_extra_btn.Label = u'Параметры…' source_extra_btn.Name = 'SourceExtraParamsBtn' lbl_desc = dialog_model.createInstance('com.sun.star.awt.UnoControlFixedTextModel') lbl_desc.PositionX = _pw_cfg._M lbl_desc.PositionY = _pw_cfg._DESC_Y - _pw_cfg._LBL_H lbl_desc.Width = 200 lbl_desc.Height = _pw_cfg._LBL_H lbl_desc.Label = u'Описание (прокрутка):' lbl_desc.Name = 'LblDesc' desc_field = dialog_model.createInstance('com.sun.star.awt.UnoControlEditModel') desc_field.PositionX = _pw_cfg._M desc_field.PositionY = _pw_cfg._DESC_Y desc_field.Width = _pw_cfg._FIELD_W desc_field.Height = _pw_cfg._DESC_H desc_field.Name = 'DescField' desc_field.MultiLine = True desc_field.ReadOnly = True try: desc_field.VScroll = True desc_field.HScroll = False except Exception: pass _wizard_dlg_add_fixed(dialog_model, 'PresetListLabel', u'Пресет (набор параметров collect_workbooks):', _pw_cfg._M, preset_y, content_w, _pw_cfg._LBL_H) _wizard_dlg_add_combo(dialog_model, 'PresetCombo', _pw_cfg._M, preset_y + _pw_cfg._ui_v(18), content_w, _pw_cfg._CTRL_H) _wizard_dlg_add_fixed(dialog_model, 'PresetNameLabel', u'Имя для сохранения:', _pw_cfg._M, preset_y + _pw_cfg._ui_v(48), content_w, _pw_cfg._LBL_H) _wizard_dlg_add_edit(dialog_model, 'PresetNameEdit', _pw_cfg._M, preset_y + _pw_cfg._ui_v(66), content_w, _pw_cfg._CTRL_H) preset_comment_y = preset_y + _pw_cfg._ui_v(96) _wizard_dlg_add_fixed(dialog_model, 'PresetCommentLabel', u'Комментарий пресета:', _pw_cfg._M, preset_comment_y, content_w, _pw_cfg._LBL_H) _wizard_dlg_add_edit(dialog_model, 'PresetCommentField', _pw_cfg._M, preset_comment_y + _pw_cfg._ui_v(18), content_w, _pw_cfg._ui_v(44), multiline=True, readonly=True) preset_summary_y = preset_comment_y + _pw_cfg._ui_v(70) _wizard_dlg_add_fixed(dialog_model, 'PresetSummaryLabel', u'Что делает этот пресет:', _pw_cfg._M, preset_summary_y, content_w, _pw_cfg._LBL_H) _wizard_dlg_add_edit(dialog_model, 'PresetSummaryField', _pw_cfg._M, preset_summary_y + _pw_cfg._ui_v(18), content_w, _pw_cfg._ui_v(70), multiline=True, readonly=True) preset_btn_y = preset_summary_y + _pw_cfg._ui_v(96) _wizard_dlg_add_button(dialog_model, 'SavePresetBtn', u'Сохранить', _pw_cfg._M, preset_btn_y, preset_bw, preset_btn_h) _wizard_dlg_add_button(dialog_model, 'DeletePresetBtn', u'Удалить', _pw_cfg._M + preset_bw + g, preset_btn_y, preset_bw, preset_btn_h) _wizard_dlg_add_button(dialog_model, 'ExportPresetBtn', u'Экспорт', _pw_cfg._M, preset_btn_y + preset_btn_h + g, preset_bw, preset_btn_h) _wizard_dlg_add_button(dialog_model, 'ImportPresetBtn', u'Импорт', _pw_cfg._M + preset_bw + g, preset_btn_y + preset_btn_h + g, preset_bw, preset_btn_h) _wizard_dlg_add_button(dialog_model, 'ApplyPresetBtn', u'Применить к листу', _pw_cfg._M, preset_btn_y + 2 * (preset_btn_h + g), preset_bw, preset_btn_h) _wizard_dlg_add_button(dialog_model, 'ClearAllPresetsBtn', u'Очистить все', _pw_cfg._M + preset_bw + g, preset_btn_y + 2 * (preset_btn_h + g), preset_bw, preset_btn_h) global_y = preset_y global_half_w = _pw_cfg._GLOBAL_HALF_W global_num_x = _pw_cfg._GLOBAL_NUM_X global_fmt_lbl_y = global_y + _pw_cfg._ui_v(58) global_fmt_combo_y = global_fmt_lbl_y + _pw_cfg._LBL_H global_font_y = global_fmt_combo_y + _pw_cfg._CTRL_H + _pw_cfg._ui_v(12) global_prefix_y = global_font_y + _pw_cfg._ui_v(48) global_msg_chk_y = global_prefix_y + _pw_cfg._ui_v(48) global_flags_lbl_y = global_msg_chk_y + _pw_cfg._ui_v(30) global_flags_lb_y = global_flags_lbl_y + _pw_cfg._LBL_H + _pw_cfg._GAP_S global_footer_y = _pw_cfg._BTN_Y global_flag_hint_y = global_footer_y - _pw_cfg._GAP_L - _pw_cfg._GLOBAL_FLAG_HINT_H global_flags_lb_h = global_flag_hint_y - _pw_cfg._GAP_S - global_flags_lb_y if global_flags_lb_h < _pw_cfg._ui_v(52): global_flags_lb_h = _pw_cfg._ui_v(52) global_flag_hint_y = global_flags_lb_y + global_flags_lb_h + _pw_cfg._GAP_S _wizard_dlg_add_fixed(dialog_model, 'GlobalTitleLabel', u'Глобальные установки (JSON рядом с пресетами):', _pw_cfg._M, global_y, content_w, _pw_cfg._LBL_H) _wizard_dlg_add_fixed(dialog_model, 'GlobalHintLabel', u'Форматы дат/чисел — при MERGE_XML_CONVERT_TO_NUMBERS=Да. Шрифт — для оформления. Префикс листа параметров — имя вкладки (по умолч. Параметры_Объединения) и листов Параметры_Объединения_1…', _pw_cfg._M, global_y + _pw_cfg._ui_v(20), content_w, _pw_cfg._ui_v(36), multiline=True) _wizard_dlg_add_fixed(dialog_model, 'GlobalDateFormatLabel', u'Формат дат по умолчанию:', _pw_cfg._M, global_fmt_lbl_y, global_half_w, _pw_cfg._LBL_H) _wizard_dlg_add_fixed(dialog_model, 'GlobalNumberFormatLabel', u'Формат чисел (пресет или свой):', global_num_x, global_fmt_lbl_y, global_half_w, _pw_cfg._LBL_H) _wizard_dlg_add_combo(dialog_model, 'GlobalDateFormatCombo', _pw_cfg._M, global_fmt_combo_y, global_half_w, _pw_cfg._CTRL_H) _wizard_dlg_add_combo(dialog_model, 'GlobalNumberFormatCombo', global_num_x, global_fmt_combo_y, global_half_w, _pw_cfg._CTRL_H) _wizard_dlg_add_fixed(dialog_model, 'GlobalFontNameLabel', u'Шрифт по умолчанию:', _pw_cfg._M, global_font_y, content_w, _pw_cfg._LBL_H) _wizard_dlg_add_edit(dialog_model, 'GlobalFontNameEdit', _pw_cfg._M, global_font_y + _pw_cfg._ui_v(18), int(content_w * 0.72), _pw_cfg._CTRL_H) _wizard_dlg_add_fixed(dialog_model, 'GlobalFontSizeLabel', u'Размер, pt:', _pw_cfg._M + int(content_w * 0.72) + 8, global_font_y, int(content_w * 0.28) - 8, _pw_cfg._LBL_H) _wizard_dlg_add_edit(dialog_model, 'GlobalFontSizeEdit', _pw_cfg._M + int(content_w * 0.72) + 8, global_font_y + _pw_cfg._ui_v(18), int(content_w * 0.28) - 8, _pw_cfg._CTRL_H) _wizard_dlg_add_fixed(dialog_model, 'GlobalParamPrefixLabel', u'Префикс листа параметров:', _pw_cfg._M, global_prefix_y, content_w, _pw_cfg._LBL_H) _wizard_dlg_add_edit(dialog_model, 'GlobalParamPrefixEdit', _pw_cfg._M, global_prefix_y + _pw_cfg._ui_v(18), content_w, _pw_cfg._CTRL_H) _wizard_dlg_add_checkbox(dialog_model, 'GlobalSuppressParamMsgCheck', u'Не показывать сообщения после действий с параметрами', _pw_cfg._M, global_msg_chk_y, content_w, _pw_cfg._ui_v(20)) _wizard_dlg_add_fixed(dialog_model, 'GlobalFlagsLabel', u'Флаги collect_workbooks (Да/Нет; ниже приоритет, чем на листе параметров):', _pw_cfg._M, global_flags_lbl_y, content_w, _pw_cfg._LBL_H) _wizard_dlg_add_listbox(dialog_model, 'GlobalFlagsListBox', _pw_cfg._M, global_flags_lb_y, content_w, global_flags_lb_h, multiselect=False) _wizard_dlg_add_fixed(dialog_model, 'GlobalFlagHintLabel', u'', _pw_cfg._M, global_flag_hint_y, content_w, _pw_cfg._GLOBAL_FLAG_HINT_H, multiline=True) _wizard_dlg_add_button(dialog_model, 'SaveGlobalSettingsBtn', u'Сохранить', _pw_cfg._M, global_footer_y, _pw_cfg._ACTION_BTN_W, _pw_cfg._BTN_H_MAIN) _wizard_dlg_add_button(dialog_model, 'SaveParamBtn', u'Сохранить', _pw_cfg._M, _pw_cfg._BTN_Y, _pw_cfg._ACTION_BTN_W, _pw_cfg._BTN_H_MAIN) _wizard_dlg_add_checkbox(dialog_model, 'RefreshSheetCheck', u'Обновить оформление и списки всего листа', _pw_cfg._M, _pw_cfg._REFRESH_SHEET_CHK_Y, _pw_cfg._REFRESH_SHEET_CHK_W, _pw_cfg._REFRESH_SHEET_CHK_H) _wizard_dlg_add_button(dialog_model, 'DeleteParamBtn', u'Удалить', _pw_cfg._DELETE_BTN_X, _pw_cfg._BTN_Y, _pw_cfg._ACTION_BTN_W, _pw_cfg._BTN_H_MAIN) ok_btn = dialog_model.createInstance('com.sun.star.awt.UnoControlButtonModel') ok_btn.PositionX = _pw_cfg._OK_BTN_X ok_btn.PositionY = _pw_cfg._BTN_Y ok_btn.Width = _pw_cfg._ACTION_BTN_W ok_btn.Height = _pw_cfg._BTN_H_MAIN ok_btn.Label = u'OK' ok_btn.Name = 'OkButton' try: ok_btn.DefaultButton = True except Exception: pass cancel_btn = dialog_model.createInstance('com.sun.star.awt.UnoControlButtonModel') cancel_btn.PositionX = _pw_cfg._CANCEL_BTN_X cancel_btn.PositionY = _pw_cfg._BTN_Y cancel_btn.Width = _pw_cfg._ACTION_BTN_W cancel_btn.Height = _pw_cfg._BTN_H_MAIN cancel_btn.Label = u'Отмена' cancel_btn.Name = 'CancelButton' dialog_model.insertByName('LblParam', lbl_param) dialog_model.insertByName('ParamCombo', param_combo) dialog_model.insertByName('LblValue', lbl_value) dialog_model.insertByName('ValueCombo', value_combo) dialog_model.insertByName('ValueEdit', value_edit) dialog_model.insertByName('LblTransfer', lbl_transfer) dialog_model.insertByName('TransferCombo', transfer_combo) dialog_model.insertByName('BrowseButton', browse_btn) dialog_model.insertByName('FilesClearLastBtn', files_clear_last_btn) dialog_model.insertByName('FilesClearAllBtn', files_clear_all_btn) dialog_model.insertByName('LblExtra', lbl_extra) dialog_model.insertByName('LblPlugin', lbl_plugin) dialog_model.insertByName('PluginEdit', plugin_edit) dialog_model.insertByName('PluginEditorBtn', plugin_editor_btn) dialog_model.insertByName('ExtraEdit', extra_edit) dialog_model.insertByName('PivotParamsBtn', pivot_params_btn) dialog_model.insertByName('ColCParamsBtn', col_c_params_btn) dialog_model.insertByName('SourceExtraParamsBtn', source_extra_btn) dialog_model.insertByName('LblDesc', lbl_desc) dialog_model.insertByName('DescField', desc_field) dialog_model.insertByName('OkButton', ok_btn) dialog_model.insertByName('CancelButton', cancel_btn) _wizard_apply_soft_gray_theme(dialog_model, title_text=u'Параметр collect_workbooks') dialog = sm.createInstanceWithContext('com.sun.star.awt.UnoControlDialog', ctx) dialog.setModel(dialog_model) presets = _wp_load_presets_list() param_tab_controls = ('LblParam', 'ParamCombo', 'ParamDisabledCheck', 'LblValue', 'ValueEdit', 'ValueCombo', 'LblTransfer', 'TransferCombo', 'BrowseButton', 'FilesClearLastBtn', 'FilesClearAllBtn', 'SourceExtraLbl', 'SourceExtraList', 'SourceExtraParamsBtn', 'LblExtra', 'LblPlugin', 'PluginEdit', 'PluginEditorBtn', 'ExtraEdit', 'PivotParamsBtn', 'ColCParamsBtn', 'LblDesc', 'DescField', 'RefreshSheetCheck', 'SaveParamBtn', 'DeleteParamBtn') preset_tab_controls = ('PresetListLabel', 'PresetCombo', 'PresetNameLabel', 'PresetNameEdit', 'PresetCommentLabel', 'PresetCommentField', 'PresetSummaryLabel', 'PresetSummaryField', 'SavePresetBtn', 'DeletePresetBtn', 'ExportPresetBtn', 'ImportPresetBtn', 'ApplyPresetBtn', 'ClearAllPresetsBtn') global_tab_controls = ('GlobalTitleLabel', 'GlobalHintLabel', 'GlobalDateFormatLabel', 'GlobalDateFormatCombo', 'GlobalNumberFormatLabel', 'GlobalNumberFormatCombo', 'GlobalFontNameLabel', 'GlobalFontNameEdit', 'GlobalFontSizeLabel', 'GlobalFontSizeEdit', 'GlobalParamPrefixLabel', 'GlobalParamPrefixEdit', 'GlobalSuppressParamMsgCheck', 'GlobalFlagsLabel', 'GlobalFlagsListBox', 'GlobalFlagHintLabel', 'SaveGlobalSettingsBtn') mode_tab_controls = ('LblNewParamMode', 'NewParamModeBtn') gs_lib = _wp_import_global_settings_lib() class WizardHandler(unohelper.Base, XActionListener, XItemListener, XTextListener): def __init__(self, dlg, initial_ctx=None): unohelper.Base.__init__(self) self.dialog = dlg self.result = None self.catalog = catalog self.doc = doc self.sheet = sheet self.initial = initial_ctx self.active_tab = 'param' self.preset_combo_busy = False self.new_param_mode = False self.edit_mode = True self.sheet_entries = [] self.new_param_names = [] self._edit_row_pp_snapshots = {} self.sheet_names = sheet_names or [] self.param_combo = _dlg_control(dlg, 'ParamCombo') self.sheet_combo = _dlg_control(dlg, 'SheetParamCombo') self.new_param_btn = _dlg_control(dlg, 'NewParamModeBtn') self.new_param_mode_lbl = _dlg_control(dlg, 'LblNewParamMode') self.desc_field = _dlg_control(dlg, 'DescField') self.value_combo = _dlg_control(dlg, 'ValueCombo') self.value_edit = _dlg_control(dlg, 'ValueEdit') self.lbl_extra = _dlg_control(dlg, 'LblExtra') self.lbl_plugin = _dlg_control(dlg, 'LblPlugin') self.plugin_edit = _dlg_control(dlg, 'PluginEdit') self.plugin_editor_btn = _dlg_control(dlg, 'PluginEditorBtn') self.extra_edit = _dlg_control(dlg, 'ExtraEdit') self.pivot_params_btn = _dlg_control(dlg, 'PivotParamsBtn') self.col_c_params_btn = _dlg_control(dlg, 'ColCParamsBtn') self.source_extra_lbl = _dlg_control(dlg, 'SourceExtraLbl') self.source_extra_list = _dlg_control(dlg, 'SourceExtraList') self.source_extra_params_btn = _dlg_control(dlg, 'SourceExtraParamsBtn') self._source_extra_cols = [] self._col_c_json_text = u'' self.browse_btn = _dlg_control(dlg, 'BrowseButton') self.files_clear_last_btn = _dlg_control(dlg, 'FilesClearLastBtn') self.files_clear_all_btn = _dlg_control(dlg, 'FilesClearAllBtn') self.ok_btn = _dlg_control(dlg, 'OkButton') self.save_param_btn = _dlg_control(dlg, 'SaveParamBtn') self.delete_param_btn = _dlg_control(dlg, 'DeleteParamBtn') self.lbl_value = _dlg_control(dlg, 'LblValue') self.lbl_transfer = _dlg_control(dlg, 'LblTransfer') self.transfer_combo = _dlg_control(dlg, 'TransferCombo') self.param_disabled_chk = _dlg_control(dlg, 'ParamDisabledCheck') self.refresh_sheet_chk = _dlg_control(dlg, 'RefreshSheetCheck') self.global_flag_rows = [] def disposing(self, event): pass def _refresh_full_sheet_from_checkbox(self): if self.refresh_sheet_chk is None: return False try: return int(self.refresh_sheet_chk.Model.State) == 1 except Exception: return False def _param_disabled_from_checkbox(self): if not self.edit_mode or self.param_disabled_chk is None: return False try: return int(self.param_disabled_chk.Model.State) == 1 except Exception: return False def _sync_param_disabled_checkbox(self): show = self.edit_mode and self.active_tab == 'param' _set_control_visible(self.param_disabled_chk, show, _pw_cfg._PARAM_DISABLED_CHK_Y, _pw_cfg._PARAM_DISABLED_CHK_H, _pw_cfg._FIELD_W) if not show or self.param_disabled_chk is None: return entry = self._selected_sheet_entry() disabled = bool(entry and entry.get('disabled')) try: self.param_disabled_chk.Model.State = 1 if disabled else 0 except Exception: pass def _selected_sheet_entry(self): if not self.edit_mode or len(self.sheet_entries) == 0: return None try: pos = self.param_combo.getSelectedItemPos() if pos is not None and 0 <= int(pos) < len(self.sheet_entries): return self.sheet_entries[int(pos)] except Exception: pass try: text = self.param_combo.getText().strip() i = 0 while i < len(self.sheet_entries): e = self.sheet_entries[i] if e['label'] == text or _sheet_name_matches_param(text, e['name']): return e i = i + 1 except Exception: pass return self.sheet_entries[0] def _reload_param_combo(self, select_row=None): if self.param_combo is None: return if self.edit_mode: self.sheet_entries = _scan_sheet_param_entries(self.sheet, self.catalog) labels = [e['label'] for e in self.sheet_entries] select_label = None select_index = 0 if select_row is not None: i = 0 while i < len(self.sheet_entries): if self.sheet_entries[i]['row'] == int(select_row): select_index = i select_label = self.sheet_entries[i]['label'] break i = i + 1 elif self.initial is not None and self.initial.get('row') is not None: init_row = int(self.initial['row']) i = 0 while i < len(self.sheet_entries): if self.sheet_entries[i]['row'] == init_row: select_index = i select_label = self.sheet_entries[i]['label'] break i = i + 1 _set_combo_items(self.param_combo, labels, select_label) if select_label is None and len(labels) > 0: try: self.param_combo.selectItemPos(select_index, True) except Exception: pass else: self.sheet_entries = [] self.new_param_names = _catalog_names_for_new_param(self.sheet, self.catalog) try: fixed = [] for nm in self.new_param_names or []: if unicode(nm or u'').strip().casefold() == u'удаление_верхних_строк'.casefold(): fixed.append(u'Удаление_верхних_строк') else: fixed.append(nm) self.new_param_names = fixed except Exception: pass pick = self.new_param_names[0] if len(self.new_param_names) > 0 else u'' if self.initial is not None and self.initial.get('spec') is not None: init_nm = self.initial['spec'].get('name', u'') try: i = 0 while i < len(self.new_param_names): if _param_match(self.new_param_names[i], init_nm): pick = self.new_param_names[i] break i = i + 1 except Exception: pass _set_combo_items(self.param_combo, self.new_param_names, pick) self._sync_param_disabled_checkbox() def _set_edit_mode(self, new_param_mode): new_param_mode = bool(new_param_mode) if self.new_param_mode == new_param_mode and self.edit_mode == (not new_param_mode): return self.new_param_mode = new_param_mode self.edit_mode = not new_param_mode _sync_new_param_mode_ui(self.new_param_btn, self.new_param_mode_lbl, self.new_param_mode) self._reload_param_combo() spec = self._current_spec() if spec is not None: self._apply_spec_to_form(spec) self._sync_param_action_buttons() def _sync_param_action_buttons(self): on_param = self.active_tab == 'param' _set_control_enabled(self.save_param_btn, on_param) can_delete = on_param and self.edit_mode and (self._selected_sheet_entry() is not None) _set_control_enabled(self.delete_param_btn, can_delete) def _collect_save_payload(self): """Данные формы для записи на лист; None при ошибке или отмене.""" if self.edit_mode: entry = self._selected_sheet_entry() if entry is None: _show_message(u'На листе нет параметров для редактирования.\nПереключите режим на «новый параметр» или выберите другой лист.', dialog=self.dialog, doc=self.doc) return None spec = entry['spec'] else: spec = self._current_spec() if spec is None: _show_message(u'Выберите параметр из списка «Параметр» (колонка A листа).', dialog=self.dialog, doc=self.doc) return None mode = spec.get('mode', 'text') primary = u'' extra = u'' if spec.get('col_c_subdialog'): if _spec_col_c_keeps_b(spec): if self.value_edit is not None: try: primary = self.value_edit.getText().strip() except Exception: primary = u'' else: primary = u'' elif mode == 'choice': if self.value_combo is not None: try: primary = _combo_resolve_fn_key(self.value_combo) except Exception: primary = u'' if primary == u'': primary = _combo_control_text(self.value_combo) else: primary = u'' else: primary = _pw_cfg._PARAM_B_SEE_COLUMN_C if self.extra_edit is not None: try: extra = self.extra_edit.getText().strip() except Exception: extra = unicode(getattr(self, '_col_c_json_text', u'')).strip() else: extra = unicode(getattr(self, '_col_c_json_text', u'')).strip() elif mode == 'vlookup': init = {} if self.edit_mode: entry = self._selected_sheet_entry() if entry is not None and int(entry['row']) >= 1: init = _vlookup_read_row_values(self.sheet, int(entry['row'])) elif self.initial is not None and _param_match(self.initial.get('spec', {}).get('name', u''), spec['name']): row = self.initial.get('row') if row is not None and int(row) >= 1: init = _vlookup_read_row_values(self.sheet, int(row)) vlookup_vals = show_vlookup_param_dialog(None, init, doc=self.doc) if vlookup_vals is None: return None primary = vlookup_vals elif mode == 'merge_sheets': init = u'' if self.edit_mode: entry = self._selected_sheet_entry() if entry is not None and int(entry['row']) >= 1: init = _cell_string(self.sheet.getCellByPosition(2, int(entry['row']))) merge_vals = show_merge_sheets_param_dialog(self.dialog, init, doc=self.doc) if merge_vals is None: return None primary = merge_vals elif mode == 'split_sheets': init = u'' if self.edit_mode: entry = self._selected_sheet_entry() if entry is not None and int(entry['row']) >= 1: init = _cell_string(self.sheet.getCellByPosition(2, int(entry['row']))) split_vals = show_split_sheets_param_dialog(self.dialog, init, doc=self.doc) if split_vals is None: return None primary = split_vals elif mode == 'source_extra': primary = u'' elif mode in ('choice', 'postprocess_range', 'postprocess_row'): if self.value_combo is not None: try: primary = _combo_resolve_fn_key(self.value_combo) except Exception: primary = u'' if primary == u'': primary = _combo_control_text(self.value_combo) elif self.value_edit is not None: primary = self.value_edit.getText().strip() if mode in ('postprocess_range', 'postprocess_row'): if self.extra_edit is not None: extra = self.extra_edit.getText().strip() if _pp_extra_needs_json_seed(primary, extra): force_av = _spec_is_postprocess_xml(spec) extra = _pp_minimal_json_for_fn(primary, force_as_values=force_av) if self.extra_edit is not None: try: self.extra_edit.setText(extra) except Exception: pass plugin_ref = u'' if mode in ('postprocess_range', 'postprocess_row') and _pp_is_plugin_primary(primary): if self.plugin_edit is not None: plugin_ref = self.plugin_edit.getText().strip() ok_val, err = _validate_primary_value(spec, primary) if not ok_val: _show_message(err, dialog=self.dialog, doc=self.doc) return None ok_pl, err_pl = _validate_plugin_ref(spec, primary, plugin_ref) if not ok_pl: _show_message(err_pl, dialog=self.dialog, doc=self.doc) return None if primary == u'' and mode not in ('text', 'number', 'files', 'cell_to_column', 'vlookup', 'merge_sheets', 'split_sheets', 'source_extra') and (not spec.get('col_c_subdialog')): _show_message(u'Укажите значение параметра.', dialog=self.dialog, doc=self.doc) return None ctx_row = None ctx_col = None if self.edit_mode: entry = self._selected_sheet_entry() if entry is not None: ctx_row = entry['row'] if self.initial is not None and self.initial.get('row') == ctx_row: ctx_col = self.initial.get('col') if mode == 'source_extra': # JSON пишется субвизардом в выбранный столбец; OK/Сохранить — только строка A. ctx_col = None primary = u'__source_extra_keep__' transfer_value = None if self._spec_has_paired_transfer(spec) and _mode_value_uses_transfer(primary) and (self.transfer_combo is not None): transfer_value = self.transfer_combo.getText().strip() if transfer_value == u'': transfer_value = _pw_cfg._TRANSFER_WIZARD_DEFAULT return (spec, primary, extra, ctx_row, ctx_col, transfer_value, plugin_ref) def _apply_save_to_sheet(self, close_dialog=False): try: payload = self._collect_save_payload() if payload is None: return False spec, primary, extra, ctx_row, ctx_col, transfer_value, plugin_ref = payload apply_extra = extra if transfer_value is not None and _spec_has_inline_transfer(spec): apply_extra = transfer_value row = apply_param_value(self.sheet, spec, primary, apply_extra, target_row=ctx_row, target_col=ctx_col, catalog=self.catalog, doc=self.doc, plugin_ref=plugin_ref) disabled = self._param_disabled_from_checkbox() if self.edit_mode else False _write_param_row_sheet_name(self.sheet, row, spec['name'], disabled, catalog=self.catalog) refresh_full = self._refresh_full_sheet_from_checkbox() if close_dialog: self.result = ('ok', spec, primary, extra, int(row), ctx_col, transfer_value, refresh_full, bool(self.edit_mode and ctx_row is not None)) self.dialog.endExecute() return True _param_wizard_post_save(self.doc, self.sheet, self.catalog, spec, row, refresh_full_sheet=refresh_full) saved_row = int(row) self.initial = None self._reload_param_combo(select_row=saved_row if self.edit_mode else None) cur = self._current_spec() if cur is not None: self._apply_spec_to_form(cur) self._sync_param_action_buttons() _wp_notify_param_action(u'Параметр «%s» сохранён (строка %d).' % (spec['name'], saved_row + 1), doc=self.doc, dialog=self.dialog, title=u'Параметры') return True except Exception as exc: _wp_notify(u'Ошибка сохранения параметра:\n%s' % exc, doc=self.doc, dialog=self.dialog, title=u'Параметры') return False def _delete_current_param_row(self): if not self.edit_mode: return entry = self._selected_sheet_entry() if entry is None: return row = int(entry['row']) pname = entry['name'] if not _wp_confirm_delete_param_row(pname, row, doc=self.doc, dialog=self.dialog): return _clear_param_sheet_row(self.sheet, row, self.catalog, self.doc) self.initial = None self._reload_param_combo() cur = self._current_spec() if cur is not None: self._apply_spec_to_form(cur) self._sync_param_action_buttons() _wp_notify_param_action(u'Параметр «%s» удалён (строка %d).' % (pname, row + 1), doc=self.doc, dialog=self.dialog, title=u'Параметры') def _on_sheet_changed(self): if self.sheet_combo is None: return try: name = self.sheet_combo.getText().strip() except Exception: return if name == u'': return sh = _get_sheet_by_name(self.doc, name) if sh is None: return self.sheet = sh self.initial = _wizard_initial_context(self.doc, self.sheet, self.catalog) self._reload_param_combo() spec = self._current_spec() if spec is not None: self._apply_spec_to_form(spec) self._sync_param_action_buttons() def _spec_has_paired_transfer(self, spec): return _spec_has_inline_transfer(spec) def _mode_row_for_transfer(self): if self.initial is not None: init_spec = self.initial.get('spec') or {} if _spec_has_inline_transfer(init_spec): row = self.initial.get('row') if row is not None and int(row) >= 0: return int(row) cw = _try_import_collect() p_mode = getattr(cw, 'P_MERGE_MODE', u'Режим') if cw is not None else u'Режим' found = _find_param_row(self.sheet, p_mode) if found >= 0: return found return None def _sync_paired_transfer_visibility(self, spec, mode_primary=u''): show_pair = self._spec_has_paired_transfer(spec) show_ctrl = show_pair and _mode_value_uses_transfer(mode_primary) _set_control_visible(self.lbl_transfer, show_ctrl, _pw_cfg._TRANSFER_PAIR_LBL_Y, _pw_cfg._LBL_H, _pw_cfg._FIELD_W) _set_control_visible(self.transfer_combo, show_ctrl, _pw_cfg._TRANSFER_PAIR_CTRL_Y, _pw_cfg._CTRL_H, _pw_cfg._FIELD_W) if show_ctrl and self.transfer_combo is not None: choices = spec.get('transfer_choices') or _transfer_wizard_choices(self.catalog) current = _read_transfer_wizard_value(self.sheet, self._mode_row_for_transfer()) _set_combo_items(self.transfer_combo, choices, current) def textChanged(self, event): src_name = u'' try: src_name = event.Source.Model.Name except Exception: pass if src_name == 'PresetNameEdit': if self.active_tab == 'presets': _sync_preset_info_panel(_preset_name_from_form()) return if src_name == 'ValueCombo': spec = self._current_spec() if spec is None: return if spec.get('mode') not in ('postprocess_range', 'postprocess_row'): return try: resolved = _combo_resolve_fn_key(self.value_combo) except Exception: resolved = u'' if resolved == u'': return self._sync_paired_transfer_visibility(spec, resolved) self._sync_plugin_form_layout(spec, resolved) self._seed_extra_json_for_fn(resolved, force=True) self._sync_pivot_params_btn_visibility(spec) self._update_description(spec, resolved) return if src_name != 'ParamCombo' or self.param_combo is None: return spec = self._current_spec() if spec is not None: self._apply_spec_to_form(spec) def _current_spec(self): if self.param_combo is None: return None if self.edit_mode: entry = self._selected_sheet_entry() return entry['spec'] if entry is not None else None try: text = self.param_combo.getText().strip() spec = _find_catalog_spec(self.catalog, text) if spec is not None: return spec pos = self.param_combo.getSelectedItemPos() if pos is not None and 0 <= int(pos) < len(self.new_param_names): return _find_catalog_spec(self.catalog, self.new_param_names[int(pos)]) except Exception: pass return None def _update_description(self, spec, selected_b=u''): try: if self.desc_field is not None: text = _build_wizard_description(spec, self.sheet, selected_b) if self._spec_has_paired_transfer(spec): try: mode_val = selected_b if mode_val == u'' and self.value_combo is not None: mode_val = self.value_combo.getText().strip() if self.transfer_combo is not None and _mode_value_uses_transfer(mode_val): tv = self.transfer_combo.getText().strip() th = _pw_cfg.VALUE_HINTS.get(tv, u'') if tv != u'': text = text + u'\n\nСпособ переноса: ' + tv if th != u'': text = text + u'\n' + th except Exception: pass self.desc_field.setText(text) except Exception: pass def _values_for_spec(self, spec): if self.edit_mode: entry = self._selected_sheet_entry() if entry is not None and spec is not None and _param_match(entry['name'], spec['name']): col = -1 if self.initial is not None and self.initial.get('row') == entry['row']: col = self.initial.get('col', -1) primary, extra, plugin_ref = _read_values_at_row(self.sheet, spec, entry['row'], col) return (primary, extra, plugin_ref) return (u'', u'', u'') if self.initial is not None and spec is not None and _param_match(self.initial.get('spec', {}).get('name', u''), spec['name']): return (self.initial.get('primary', u''), self.initial.get('extra', u''), self.initial.get('plugin_ref', u'')) if spec.get('multi_row'): return (u'', u'', u'') row = _find_param_row(self.sheet, spec['name']) if row >= 0 and spec.get('mode') in ('postprocess_range', 'postprocess_row'): primary, plugin_ref, extra = _read_pp_row_values(self.sheet, row) return (primary, extra, plugin_ref) return (_read_primary_value(self.sheet, spec), _read_extra_value(self.sheet, spec), u'') def _sync_wizard_footer_layout(self, desc_y, desc_h): """Чекбокс «Обновить лист» над кнопками; OK/Отмена не перекрываются. desc_y уже с soft-gray offset (_pw_content_y).""" refresh_y = int(desc_y) + int(desc_h) + _pw_cfg._ui_v(12) btn_y = refresh_y + _pw_cfg._REFRESH_SHEET_CHK_H + _pw_cfg._GAP_S chk = self.refresh_sheet_chk if chk is None: try: chk = self.dialog.getControl('RefreshSheetCheck') except Exception: chk = None if chk is not None: try: chk.Model.PositionY = refresh_y chk.Model.Width = _pw_cfg._REFRESH_SHEET_CHK_W except Exception: pass for ctl_name in ('SaveParamBtn', 'DeleteParamBtn', 'OkButton', 'CancelButton'): try: self.dialog.getControl(ctl_name).Model.PositionY = btn_y except Exception: pass try: self.dialog.Model.Height = btn_y + _pw_cfg._BTN_H_MAIN + _pw_cfg._M except Exception: pass def _sync_plugin_form_layout(self, spec, primary_text=u''): mode = spec.get('mode') if spec is not None else u'' show_pp = mode in ('postprocess_range', 'postprocess_row') plugin_on = show_pp and _pp_is_plugin_primary(primary_text) _set_control_visible(self.lbl_plugin, plugin_on, _pw_cfg._EXTRA_LBL_Y) _set_control_visible(self.plugin_edit, plugin_on, _pw_cfg._EXTRA_EDIT_Y, _pw_cfg._EXTRA_EDIT_H, _pw_cfg._FIELD_W - 110) _set_control_visible(self.plugin_editor_btn, plugin_on, _pw_cfg._EXTRA_EDIT_Y, _pw_cfg._CTRL_H, 100) extra_lbl_y = _pw_content_y(_pw_cfg._PLUGIN_D_LBL_Y if plugin_on else _pw_cfg._EXTRA_LBL_Y) extra_edit_y = _pw_content_y(_pw_cfg._PLUGIN_D_EDIT_Y if plugin_on else _pw_cfg._EXTRA_EDIT_Y) if self.lbl_extra is not None: try: self.lbl_extra.Model.PositionY = extra_lbl_y self.lbl_extra.Model.Label = u'Доп. аргументы (D):' if plugin_on else u'Доп. аргументы (C):' except Exception: pass if self.extra_edit is not None: try: self.extra_edit.Model.PositionY = extra_edit_y except Exception: pass if self.pivot_params_btn is not None: try: self.pivot_params_btn.Model.PositionY = extra_edit_y except Exception: pass desc_y = _pw_content_y(_pw_cfg._DESC_Y + (_pw_cfg._PLUGIN_DESC_OFFSET if plugin_on else 0)) desc_h = _pw_cfg._DESC_H_PLUGIN if plugin_on else _pw_cfg._DESC_H try: lbl = self.dialog.getControl('LblDesc') lbl.Model.PositionY = desc_y - _pw_cfg._LBL_H except Exception: pass try: desc_ctl = self.desc_field if desc_ctl is None: desc_ctl = self.dialog.getControl('DescField') if desc_ctl is not None: desc_ctl.Model.PositionY = desc_y desc_ctl.Model.Height = desc_h except Exception: pass self._sync_wizard_footer_layout(desc_y, desc_h) def _on_plugin_editor_clicked(self): spec = self._current_spec() if spec is None: return init_ref = u'' init_extra = u'' if self.plugin_edit is not None: try: init_ref = self.plugin_edit.getText().strip() except Exception: pass if self.extra_edit is not None: try: init_extra = self.extra_edit.getText().strip() except Exception: pass result = _show_plugin_function_dialog(self.dialog, spec, init_ref, init_extra, doc=self.doc) if result is None: return c_ref, d_extra = result if self.plugin_edit is not None: try: self.plugin_edit.setText(c_ref) except Exception: pass if self.extra_edit is not None: try: self.extra_edit.setText(d_extra) except Exception: pass def _capture_edit_row_pp_snapshot(self, primary, extra, plugin_ref): """Запомнить B/C строки при открытии редактирования (откат при возврате fn).""" self._edit_row_pp_snapshots = {} if not self.edit_mode: return key = _normalize_pp_fn_key(primary) if key == u'': return self._edit_row_pp_snapshots[key] = {u'extra': unicode(extra or u'').strip(), u'plugin_ref': unicode(plugin_ref or u'').strip()} def _edit_row_pp_snapshot_for_fn(self, fn_key): if not self.edit_mode: return None snaps = getattr(self, u'_edit_row_pp_snapshots', None) if not snaps: return None key = _normalize_pp_fn_key(fn_key) if key == u'' or key not in snaps: return None hit = snaps[key] if not isinstance(hit, dict): return None return (hit.get(u'extra', u''), hit.get(u'plugin_ref', u'')) def _inner_params_fn_key(self): if self.value_combo is not None: try: return _combo_resolve_fn_key(self.value_combo) except Exception: pass return u'' def _seed_extra_json_for_fn(self, fn_key, force=False): """Подставить JSON в ExtraEdit: снимок строки → с листа → минимальный шаблон.""" key = unicode(fn_key or u'').strip() if key == u'' or not _pp_fn_has_json_params(key): return if self.extra_edit is None: return spec = self._current_spec() resolved = key try: resolved = _combo_resolve_fn_key(self.value_combo) or key except Exception: resolved = key snap = self._edit_row_pp_snapshot_for_fn(resolved) if snap is not None: snap_extra, snap_plugin = snap try: self.extra_edit.setText(snap_extra) except Exception: pass if self.plugin_edit is not None: try: self.plugin_edit.setText(snap_plugin) except Exception: pass return exclude_row = self._wizard_row_for_spec(spec) existing = _find_sheet_pp_extra_for_fn(self.sheet, spec, resolved, exclude_row=exclude_row) if existing != u'': try: self.extra_edit.setText(existing) except Exception: pass return cur = u'' try: cur = self.extra_edit.getText() except Exception: pass if not force and (not _pp_extra_needs_json_seed(resolved, cur)): return force_av = _spec_is_postprocess_xml(spec) seeded = _pp_minimal_json_for_fn(resolved, force_as_values=force_av) try: self.extra_edit.setText(seeded) except Exception: pass def _sync_inner_params_btn_visibility(self, spec=None): """Показать «Параметры…» для INNER_DIALOG_REGISTRY и col_c_subdialog.""" show_btn = False if spec is None: spec = self._current_spec() fn_key = self._inner_params_fn_key() if spec is not None and _spec_has_col_c_subdialog(spec): show_btn = True elif spec is not None and spec.get('mode') in ('postprocess_range', 'postprocess_row') and (not _pp_is_plugin_primary(fn_key)): try: show_btn = _pp_fn_has_json_params(fn_key) except Exception: pass extra_y = _pw_cfg._EXTRA_EDIT_Y if _pp_is_plugin_primary(fn_key): extra_y = _pw_cfg._PLUGIN_D_EDIT_Y _set_control_visible(self.pivot_params_btn, show_btn, extra_y, _pw_cfg._CTRL_H, 100) def _sync_pivot_params_btn_visibility(self, spec=None): self._sync_inner_params_btn_visibility(spec) def _wizard_row_for_spec(self, spec=None): if spec is None: spec = self._current_spec() if self.edit_mode: entry = self._selected_sheet_entry() if entry is not None: return int(entry['row']) if self.initial is not None: row = self.initial.get('row') if row is not None and int(row) >= 1: return int(row) if spec is not None: found = _find_param_row(self.sheet, spec['name']) if found >= 0: return found return None def _sync_col_c_params_ui(self, spec=None): """ col_c_subdialog: B/C как у постобработки (ValueEdit + ExtraEdit + «Параметры…» у C). ColCParamsBtn на строке B не используем. """ if spec is None: spec = self._current_spec() show = _spec_has_col_c_subdialog(spec) keep_b = _spec_col_c_keeps_b(spec) see_only = _spec_col_c_see_only(spec) _set_control_visible(self.col_c_params_btn, False, _pw_cfg._VALUE_CTRL_Y, _pw_cfg._CTRL_H, 100) if self.value_edit is not None: try: self.value_edit.Model.Width = _pw_cfg._FIELD_W self.value_edit.Model.ReadOnly = bool(show and see_only) except Exception: pass def _on_col_c_params_clicked(self): spec = self._current_spec() if not _spec_has_col_c_subdialog(spec): return initial = u'' if self.extra_edit is not None: try: initial = self.extra_edit.getText().strip() except Exception: pass if initial == u'': initial = unicode(getattr(self, '_col_c_json_text', u'') or u'').strip() row = self._wizard_row_for_spec(spec) if initial == u'' and row is not None and (int(row) >= 1): initial = _cell_string(self.sheet.getCellByPosition(2, int(row))) kind = _spec_col_c_subdialog_kind(spec) result = None if kind == u'delete_top_rows': result = show_delete_top_rows_param_dialog(self.dialog, initial, doc=self.doc) elif kind == u'skip_source_rows': result = show_skip_source_rows_param_dialog(self.dialog, initial, doc=self.doc) elif kind == u'cell_to_column': result = show_cell_to_column_param_dialog(self.dialog, initial, doc=self.doc) elif kind == u'add_to_variables_map': result = show_add_to_variables_map_param_dialog(self.dialog, initial, doc=self.doc) elif kind == u'sheets_undelete_filter': result = show_sheets_undelete_filter_dialog(self.dialog, initial, doc=self.doc) elif kind == u'sheets_force_delete_filter': result = show_sheets_force_delete_filter_dialog(self.dialog, initial, doc=self.doc) elif kind == u'sheets_include_filter': result = show_sheets_include_filter_dialog(self.dialog, initial, doc=self.doc) elif kind == u'sheet_list_fn': fn_key = _spec_col_c_fn_key(spec) if fn_key == u'': return result = _show_sheet_list_panel_dialog(self.dialog, fn_key, initial, doc=self.doc) if result is None: return text = unicode(result).strip() self._col_c_json_text = text if self.extra_edit is not None: try: self.extra_edit.setText(text) except Exception: pass def _pivot_wizard_param_row(self): if self.edit_mode: entry = self._selected_sheet_entry() if entry is not None: return entry['row'] if self.initial is not None: row = self.initial.get('row') spec = self.initial.get('spec') or {} if row is not None and int(row) >= 1 and (spec.get('mode') in ('postprocess_range', 'postprocess_row')): return int(row) cur = self._current_spec() if cur is not None: found = _find_param_row(self.sheet, cur['name']) if found >= 0: return found return None def _pivot_wizard_primary(self): if self.value_combo is not None: try: v = _combo_control_text(self.value_combo) if v != u'': return v except Exception: pass return u'сводная_таблица' def _on_pivot_params_clicked(self): """Вложенный диалог параметров; OK пишет JSON/текст в колонку C (ExtraEdit).""" try: spec = self._current_spec() if _spec_has_col_c_subdialog(spec): self._on_col_c_params_clicked() return initial_text = u'' if self.extra_edit is not None: try: initial_text = self.extra_edit.getText().strip() except Exception: pass fn_key = self._inner_params_fn_key() if fn_key == u'': _show_message(u'Выберите функцию в поле «Значение (B)».', doc=self.doc, dialog=self.dialog) return force_av = _spec_is_postprocess_xml(self._current_spec()) if _pp_extra_needs_json_seed(fn_key, initial_text): initial_text = _pp_minimal_json_for_fn(fn_key, force_as_values=force_av) if self.extra_edit is not None: try: self.extra_edit.setText(initial_text) except Exception: pass result = None if _is_pivot_table_pp_name(fn_key): pivot_json = show_pivot_table_param_dialog( self.dialog, initial_text, doc=self.doc, param_sheet=self.sheet, param_row=self._pivot_wizard_param_row(), param_primary=self._pivot_wizard_primary(), force_as_values=force_av, ) if pivot_json is None: result = None else: from libre_macros_param_codec import param_decode, param_encode blocks = param_decode(u'сводная_таблица', pivot_json) result = param_encode(u'сводная_таблица', blocks) else: result = _show_inner_codec_param_dialog(self.dialog, fn_key, initial_text, doc=self.doc, force_as_values=force_av) if result is not None and self.extra_edit is not None: try: self.extra_edit.setText(result) except Exception: pass except Exception as exc: _show_message(u'Ошибка диалога параметров: %s' % exc, doc=self.doc, dialog=self.dialog) def _reload_source_extra_list(self, spec=None): """Заполнить список столбцов по «Файлы-Источники».""" self._source_extra_cols = _source_extra_column_labels(self.sheet, min_cols=1) labels = [] i = 0 while i < len(self._source_extra_cols): labels.append(self._source_extra_cols[i][1]) i = i + 1 sel = 0 focus_col = -1 if self.initial is not None: try: focus_col = int(self.initial.get('col', -1)) except (TypeError, ValueError): focus_col = -1 if focus_col >= 1: j = 0 while j < len(self._source_extra_cols): if int(self._source_extra_cols[j][0]) == focus_col: sel = j break j = j + 1 try: if self.source_extra_list is not None: model = self.source_extra_list.Model model.removeAllItems() k = 0 while k < len(labels): model.insertItemText(k, labels[k]) k = k + 1 if labels: try: self.source_extra_list.selectItemPos(sel, True) except Exception: pass except Exception: pass def _selected_source_extra_col(self): cols = self._source_extra_cols or [] if not cols: return 1 idx = 0 try: if self.source_extra_list is not None: idx = int(self.source_extra_list.getSelectedItemPos()) except Exception: idx = 0 if idx < 0 or idx >= len(cols): idx = 0 return int(cols[idx][0]) def _on_source_extra_params_clicked(self): spec = self._current_spec() if spec is None or spec.get('mode') != 'source_extra': return target_col = self._selected_source_extra_col() row = self._wizard_row_for_spec(spec) if row is None or int(row) < 1: row = _find_param_row(self.sheet, spec['name']) if row is None or int(row) < 1: row = _first_empty_row(self.sheet) try: self.sheet.getCellByPosition(0, int(row)).String = spec['name'] except Exception: pass _ensure_param_row_formatting(self.sheet, int(row), self.catalog, spec, True, doc=self.doc) row = int(row) initial = _cell_string(self.sheet.getCellByPosition(target_col, row)) source_path = _source_extra_path_for_col(self.sheet, target_col) result = show_source_extra_param_dialog( self.dialog, initial, doc=self.doc, source_path=source_path, target_col=target_col, ) if result is None: return _files_prepare_value_column(self.doc, self.sheet, target_col) try: self.sheet.getCellByPosition(target_col, row).String = unicode(result).strip() except Exception: pass _wp_notify_param_action( u'Доп. параметры записаны в столбец %s (строка %d).' % (_col_letter(target_col), row + 1), doc=self.doc, dialog=self.dialog, title=u'Параметры', ) def _files_value_text(self): if self.value_edit is None: return u'' try: return self.value_edit.getText().strip() except Exception: return u'' def _set_files_value_text(self, text): if self.value_edit is None: return try: self.value_edit.setText(text) except Exception: pass def _apply_spec_to_form(self, spec): if spec is None: return mode = spec.get('mode', 'text') source_extra_mode = mode == 'source_extra' col_c_mode = _spec_has_col_c_subdialog(spec) col_c_see_only = _spec_col_c_see_only(spec) keep_b = _spec_col_c_keeps_b(spec) show_combo = mode in ('choice', 'postprocess_range', 'postprocess_row') show_edit = mode in ('text', 'number', 'files', 'cell_to_column') show_files_tools = mode == 'files' show_extra = mode in ('postprocess_range', 'postprocess_row') or col_c_mode show_vlookup_hint = mode in ('vlookup', 'merge_sheets', 'split_sheets') files_mode = mode == 'files' value_ctrl_y = _pw_cfg._VALUE_CTRL_Y if source_extra_mode: show_combo = False show_edit = False show_files_tools = False show_extra = False show_vlookup_hint = False if col_c_mode: if mode == 'choice': show_combo = True show_edit = False else: show_combo = False show_edit = True show_files_tools = False if self.lbl_value is not None: try: if source_extra_mode: _set_control_visible(self.lbl_value, False, _pw_cfg._VALUE_LBL_Y, _pw_cfg._LBL_H, _pw_cfg._FIELD_W) else: _set_control_visible(self.lbl_value, True, _pw_cfg._VALUE_LBL_Y, _pw_cfg._LBL_H, _pw_cfg._FIELD_W) self.lbl_value.Model.PositionY = _pw_content_y(_pw_cfg._VALUE_LBL_Y) vm = self.lbl_value.Model if source_extra_mode: vm.Label = u'Столбцы доп. параметров источников:' elif col_c_see_only: vm.Label = u'Значение (B):' elif keep_b: pname = unicode(spec.get('name') or u'') if _param_match(pname, u'Листы_удалить'): vm.Label = u'Шаблоны листов удалить (B), через запятую (*):' elif _param_match(pname, u'Листы_не_удалять'): vm.Label = u'Шаблоны листов не удалять (B), через запятую (*):' else: vm.Label = u'Шаблоны листов (B), через запятую (*):' elif files_mode: vm.Label = u'Значение (пути через ; или ,):' elif show_vlookup_hint: if mode == 'merge_sheets': vm.Label = u'объединить_листы: OK — шаблон JSON в C' elif mode == 'split_sheets': vm.Label = u'разделить_листы: OK — JSON в C' else: vm.Label = u'ВПР: нажмите OK — откроется форма параметров' else: vm.Label = u'Значение (B):' vm.Width = _pw_cfg._FIELD_W try: vm.MultiLine = False except Exception: pass vm.Height = _pw_cfg._LBL_H except Exception: pass _set_control_visible(self.value_combo, False, value_ctrl_y, _pw_cfg._VALUE_CTRL_H, _pw_cfg._FIELD_W) _set_control_visible(self.value_edit, False, value_ctrl_y, _pw_cfg._VALUE_CTRL_H, _pw_cfg._FIELD_W) _set_control_visible(self.browse_btn, False, _pw_cfg._VALUE_BTNS_Y, _pw_cfg._FILES_BTN_H, _pw_cfg._BROWSE_W) _set_control_visible(self.files_clear_last_btn, False, _pw_cfg._VALUE_BTNS_Y, _pw_cfg._FILES_BTN_H, _pw_cfg._FILES_BTN_W) _set_control_visible(self.files_clear_all_btn, False, _pw_cfg._VALUE_BTNS_Y, _pw_cfg._FILES_BTN_H, _pw_cfg._FILES_BTN_W) _set_control_visible(self.source_extra_lbl, False, _pw_cfg._VALUE_LBL_Y, _pw_cfg._LBL_H, _pw_cfg._FIELD_W) _set_control_visible(self.source_extra_list, False, _pw_cfg._SOURCE_EXTRA_LIST_Y, _pw_cfg._SOURCE_EXTRA_LIST_H, _pw_cfg._FIELD_W) _set_control_visible(self.source_extra_params_btn, False, _pw_cfg._SOURCE_EXTRA_BTN_Y, _pw_cfg._CTRL_H, _pw_cfg._SOURCE_EXTRA_BTN_W) if show_combo: try: if self.value_edit is not None: self.value_edit.setText(u'') except Exception: pass else: try: if self.value_combo is not None: self.value_combo.setText(u'') except Exception: pass _set_control_visible(self.value_combo, show_combo, value_ctrl_y, _pw_cfg._VALUE_CTRL_H, _pw_cfg._FIELD_W) _set_control_visible(self.value_edit, show_edit and (not show_vlookup_hint), value_ctrl_y, _pw_cfg._VALUE_CTRL_H, _pw_cfg._FIELD_W) self._sync_col_c_params_ui(spec) if not col_c_mode: _sync_value_row_z_order(self.value_combo, self.value_edit, self.col_c_params_btn, show_combo, show_edit and (not show_vlookup_hint), False) if show_files_tools: clear_last_x = _pw_cfg._M + _pw_cfg._BROWSE_W + _pw_cfg._FILES_BTN_GAP clear_all_x = clear_last_x + _pw_cfg._FILES_BTN_W + _pw_cfg._FILES_BTN_GAP _set_control_visible(self.browse_btn, True, _pw_cfg._VALUE_BTNS_Y, _pw_cfg._FILES_BTN_H, _pw_cfg._BROWSE_W) _set_control_visible(self.files_clear_last_btn, True, _pw_cfg._VALUE_BTNS_Y, _pw_cfg._FILES_BTN_H, _pw_cfg._FILES_BTN_W) _set_control_visible(self.files_clear_all_btn, True, _pw_cfg._VALUE_BTNS_Y, _pw_cfg._FILES_BTN_H, _pw_cfg._FILES_BTN_W) try: files_y = _pw_content_y(_pw_cfg._VALUE_BTNS_Y) if self.browse_btn is not None: self.browse_btn.Model.PositionX = _pw_cfg._M self.browse_btn.Model.PositionY = files_y if self.files_clear_last_btn is not None: self.files_clear_last_btn.Model.PositionX = clear_last_x self.files_clear_last_btn.Model.PositionY = files_y if self.files_clear_all_btn is not None: self.files_clear_all_btn.Model.PositionX = clear_all_x self.files_clear_all_btn.Model.PositionY = files_y except Exception: pass if source_extra_mode: _set_control_visible(self.source_extra_lbl, True, _pw_cfg._VALUE_LBL_Y, _pw_cfg._LBL_H, _pw_cfg._FIELD_W) _set_control_visible(self.source_extra_list, True, _pw_cfg._SOURCE_EXTRA_LIST_Y, _pw_cfg._SOURCE_EXTRA_LIST_H, _pw_cfg._FIELD_W) _set_control_visible(self.source_extra_params_btn, True, _pw_cfg._SOURCE_EXTRA_BTN_Y, _pw_cfg._CTRL_H, _pw_cfg._SOURCE_EXTRA_BTN_W) self._reload_source_extra_list(spec) if self.lbl_extra is not None and col_c_mode: try: if keep_b: self.lbl_extra.Model.Label = u'Фильтр JSON (C), опционально:' elif mode == 'choice': self.lbl_extra.Model.Label = u'Листы (C), опционально:' else: self.lbl_extra.Model.Label = u'Параметры (C):' except Exception: pass _set_control_visible(self.lbl_extra, show_extra, _pw_cfg._EXTRA_LBL_Y) _set_control_visible(self.extra_edit, show_extra, _pw_cfg._EXTRA_EDIT_Y, _pw_cfg._EXTRA_EDIT_H, _pw_cfg._FIELD_W - 110) current, extra_val, plugin_ref = self._values_for_spec(spec) if show_extra and mode in ('postprocess_range', 'postprocess_row'): self._capture_edit_row_pp_snapshot(current, extra_val, plugin_ref) value_text = u'' if show_combo: choices = spec.get('choices') or [] _set_combo_items(self.value_combo, choices, current) try: value_text = _combo_control_text(self.value_combo) or current except Exception: value_text = current else: try: if self.value_edit is not None: if col_c_see_only and (current == u'' or _param_match(current, _pw_cfg._PARAM_B_SEE_COLUMN_C)): self.value_edit.setText(_pw_cfg._PARAM_B_SEE_COLUMN_C) else: self.value_edit.setText(current) except Exception: pass value_text = current self._update_description(spec, value_text) self._sync_plugin_form_layout(spec, value_text) try: if show_extra: if mode in ('postprocess_range', 'postprocess_row'): if self.plugin_edit is not None: self.plugin_edit.setText(plugin_ref) if self.extra_edit is not None: self.extra_edit.setText(extra_val) if not self.edit_mode: self._seed_extra_json_for_fn(value_text) elif col_c_mode and self.extra_edit is not None: self.extra_edit.setText(extra_val) self._col_c_json_text = unicode(extra_val or u'').strip() except Exception: pass focus_col = -1 if self.initial is not None and spec is not None: if _param_match(self.initial.get('spec', {}).get('name', u''), spec['name']): focus_col = int(self.initial.get('col', -1)) if self.edit_mode: entry = self._selected_sheet_entry() if entry is None or entry['row'] != self.initial.get('row'): focus_col = -1 plugin_on = _pp_is_plugin_primary(value_text or current) focus_extra = show_extra and (plugin_on and focus_col >= 3 or (not plugin_on and focus_col >= 2)) focus_plugin = show_extra and plugin_on and (focus_col == 2) if focus_plugin: _focus_control(self.plugin_edit) elif focus_extra: _focus_control(self.extra_edit) elif show_combo: _focus_control(self.value_combo) elif show_edit: _focus_control(self.value_edit) self._sync_paired_transfer_visibility(spec, value_text) self._sync_pivot_params_btn_visibility(spec) self._sync_param_disabled_checkbox() def itemStateChanged(self, event): try: if event.State != 1: return except Exception: pass src_name = u'' try: src_name = event.Source.Model.Name except Exception: pass if src_name == 'ParamCombo': self._apply_spec_to_form(self._current_spec()) self._sync_param_action_buttons() self._sync_param_disabled_checkbox() elif src_name == 'SheetParamCombo': self._on_sheet_changed() elif src_name == 'ValueCombo': spec = self._current_spec() if spec is None: return try: selected = _combo_resolve_fn_key(self.value_combo) except Exception: selected = u'' self._sync_paired_transfer_visibility(spec, selected) self._sync_plugin_form_layout(spec, selected) self._seed_extra_json_for_fn(selected, force=True) self._sync_pivot_params_btn_visibility(spec) self._update_description(spec, selected) self._sync_pivot_params_btn_visibility(spec) elif src_name == 'TransferCombo': spec = self._current_spec() if spec is None: return try: mode_val = self.value_combo.getText().strip() except Exception: mode_val = u'' self._update_description(spec, mode_val) elif src_name == 'PresetCombo': if self.active_tab != 'presets' or self.preset_combo_busy: return name = _combo_selected_preset_name() _sync_preset_name_edit(name) _sync_preset_info_panel(name) elif src_name == 'GlobalFlagsListBox': if self.active_tab != 'global': return idx = _global_flags_selected_index(self.dialog) _global_sync_flag_hint(self.dialog, self.global_flag_rows, idx) def actionPerformed(self, event): btn = event.Source.Model.Name if btn == 'TabBtnParam': _show_wizard_tab('param') return if btn == 'TabBtnPresets': _show_wizard_tab('presets') return if btn == 'TabBtnGlobal': _show_wizard_tab('global') return if btn == 'NewParamModeBtn': self._set_edit_mode(not self.new_param_mode) return if btn == 'SaveGlobalSettingsBtn': try: err = _save_global_settings_from_form() if err: _wp_notify(err, doc=self.doc, dialog=self.dialog, title=u'Глобальные установки') return path = gs_lib['global_settings_path']() if gs_lib else u'' _wp_notify(u'Глобальные настройки сохранены:\n%s' % path, doc=self.doc, dialog=self.dialog, title=u'Глобальные установки') except Exception as exc: _wp_notify(u'Ошибка сохранения: %s' % exc, doc=self.doc, dialog=self.dialog, title=u'Глобальные установки') return if btn == 'ApplyPresetBtn': try: preset_name = _combo_selected_preset_name() err = _apply_preset_by_name(preset_name) if err: _wp_notify(err, doc=self.doc, dialog=self.dialog) else: self.initial = None self._reload_param_combo() _show_wizard_tab('param') _wp_notify(_wp_msg_applied(preset_name), doc=self.doc, dialog=self.dialog) except Exception as exc: _wp_notify(u'Ошибка применения пресета: %s' % exc, doc=self.doc, dialog=self.dialog) return if btn == 'SavePresetBtn': try: save_name = _combo_selected_preset_name() name_edit = _dlg_control(self.dialog, 'PresetNameEdit') if name_edit is not None: try: t = name_edit.getText() if t is not None and unicode(t).strip() != u'': save_name = unicode(t).strip() except Exception: pass if unicode(save_name).strip() == u'': _wp_notify(u'Укажите имя пресета (поле «Имя для сохранения»).', doc=self.doc, dialog=self.dialog) return try: fresh = _wp_load_presets_list() presets[:] = fresh except Exception: pass if _wp_find_preset_by_name(presets, save_name) is not None: if not _wp_confirm_overwrite_preset(save_name, doc=self.doc, dialog=self.dialog): return path, err = _wp_save_preset_from_doc(self.doc, presets, save_name, self.sheet) if err: _wp_notify(err, doc=self.doc, dialog=self.dialog) return _wp_notify(_wp_msg_saved(save_name, path), doc=self.doc, dialog=self.dialog, title=u'Сохранение пресета') saved = _wp_find_preset_by_name(presets, save_name) pick = saved['name'] if saved is not None else save_name _refresh_preset_combo(select_name=pick) except Exception as exc: _wp_notify(u'Ошибка сохранения пресета: %s' % exc, doc=self.doc, dialog=self.dialog) return if btn == 'DeletePresetBtn': try: del_name = _preset_name_from_form() if unicode(del_name).strip() == u'': _wp_notify(u'Выберите пресет для удаления.', doc=self.doc, dialog=self.dialog) return if not _wp_confirm_delete_preset(del_name, doc=self.doc, dialog=self.dialog): return path, err = _wp_delete_preset_and_save(presets, del_name) if err: _wp_notify(err, doc=self.doc, dialog=self.dialog) return _wp_notify(_wp_msg_deleted(del_name, path), doc=self.doc, dialog=self.dialog) if len(_wp_preset_names(presets)) > 0: _refresh_preset_combo(select_index=0) else: _refresh_preset_combo(clear=True) except Exception as exc: _wp_notify(u'Ошибка удаления пресета: %s' % exc, doc=self.doc, dialog=self.dialog) return if btn == 'ClearAllPresetsBtn': try: if not _wp_confirm_clear_all_presets(doc=self.doc, dialog=self.dialog): return err = _wp_delete_presets_file() if err: _wp_notify(u'Не удалось удалить файл:\n%s' % err, doc=self.doc, dialog=self.dialog) return presets[:] = [] _refresh_preset_combo(clear=True) _wp_notify(u'Файл пресетов удалён.\nПри следующем открытии вкладки «Пресеты» будут созданы примеры Я01…Я18 (9 сценариев × xml + UNO).', doc=self.doc, dialog=self.dialog, title=u'Пресеты') except Exception as exc: _wp_notify(u'Ошибка очистки пресетов: %s' % exc, doc=self.doc, dialog=self.dialog) return if btn == 'ExportPresetBtn': try: preset = _wp_find_preset_by_name(presets, _combo_selected_preset_name()) if preset is None: _wp_notify(u'Выберите пресет для экспорта.', doc=self.doc, dialog=self.dialog) return path = _wp_pick_json_file(ctx, save_as=True, suggested_name=preset['name'] + '.json', parent=_wp_dialog_picker_parent(self.dialog)) if path is None: return err = _wp_export_preset_to_file(preset, path) if err: _wp_notify(u'Ошибка экспорта:\n%s' % err, doc=self.doc, dialog=self.dialog) else: _wp_notify(_wp_msg_exported(preset['name'], path), doc=self.doc, dialog=self.dialog) except Exception as exc: _wp_notify(u'Ошибка экспорта: %s' % exc, doc=self.doc, dialog=self.dialog) return if btn == 'ImportPresetBtn': try: path = _wp_pick_json_file(ctx, save_as=False, parent=_wp_dialog_picker_parent(self.dialog)) if path is None: return preset, err = _wp_import_preset_from_file(path) if err: _wp_notify(u'Ошибка импорта:\n%s' % err, doc=self.doc, dialog=self.dialog) return new_list, uerr = _wp_upsert_preset(presets, preset['name'], preset['rows']) if uerr: _wp_notify(uerr, doc=self.doc, dialog=self.dialog) return presets[:] = new_list err = _wp_save_presets_list(presets) if err: _wp_notify(u'Не удалось сохранить список:\n%s' % err, doc=self.doc, dialog=self.dialog) return _wp_notify(_wp_msg_imported(preset['name'], path), doc=self.doc, dialog=self.dialog) _refresh_preset_combo(select_name=preset['name']) except Exception as exc: _wp_notify(u'Ошибка импорта: %s' % exc, doc=self.doc, dialog=self.dialog) return if btn == 'BrowseButton': cur = self._files_value_text() paths, delim = _split_files_path_text(cur) anchor = paths[-1] if len(paths) > 0 else u'' path = _show_file_picker_with_initial(anchor, doc=self.doc) if path: if cur == u'': self._set_files_value_text(path) else: paths.append(path) self._set_files_value_text(_join_files_path_text(paths, delim)) return if btn == 'FilesClearLastBtn': cur = self._files_value_text() paths, delim = _split_files_path_text(cur) if len(paths) > 0: paths = paths[:-1] self._set_files_value_text(_join_files_path_text(paths, delim)) return if btn == 'FilesClearAllBtn': self._set_files_value_text(u'') return if btn == 'ColCParamsBtn': self._on_col_c_params_clicked() return if btn == 'SourceExtraParamsBtn': self._on_source_extra_params_clicked() return if btn == 'PivotParamsBtn': self._on_pivot_params_clicked() return if btn == 'PluginEditorBtn': self._on_plugin_editor_clicked() return if btn == 'SaveParamBtn': self._apply_save_to_sheet(close_dialog=False) return if btn == 'DeleteParamBtn': self._delete_current_param_row() return if btn == 'CancelButton': self.result = ('cancel', None) self.dialog.endExecute() return if btn == 'OkButton': self._apply_save_to_sheet(close_dialog=True) return def _activate_presets_tab(): created = _wp_ensure_presets_file(presets) if len(_wp_preset_names(presets)) > 0: _refresh_preset_combo(select_index=0) else: _refresh_preset_combo(clear=True) if created: _wp_notify(u'Создан файл пресетов: 18 примеров Я01…Я18 (9 сценариев × xml_excel_ods + UNO).\nПути к источникам уже указывают на test/collect_workbooks/sources.', doc=doc, dialog=dialog, title=u'Пресеты') def _load_global_settings_to_form(): if gs_lib is None: return settings = gs_lib['load_global_settings']() date_ctl = _dlg_control(dialog, 'GlobalDateFormatCombo') num_ctl = _dlg_control(dialog, 'GlobalNumberFormatCombo') font_name_ctl = _dlg_control(dialog, 'GlobalFontNameEdit') font_size_ctl = _dlg_control(dialog, 'GlobalFontSizeEdit') prefix_ctl = _dlg_control(dialog, 'GlobalParamPrefixEdit') suppress_ctl = _dlg_control(dialog, 'GlobalSuppressParamMsgCheck') date_choices = tuple(gs_lib['DATE_FORMAT_CHOICES']) calc_fn = gs_lib.get('calc_number_format_choices') if calc_fn is not None: num_choices = tuple(calc_fn()) else: num_choices = tuple(gs_lib['NUMBER_FORMAT_CHOICES']) date_val = unicode(settings.get('default_date_format') or u'').strip() num_val = unicode(settings.get('number_format') or u'').strip() font_name = unicode(settings.get('font_name') or u'').strip() if font_name == u'': font_name = unicode(gs_lib.get('DEFAULT_FONT_NAME') or u'PT Sans') try: font_size = float(settings.get('font_size') or 11) except (TypeError, ValueError): font_size = float(gs_lib.get('DEFAULT_FONT_SIZE') or 11) prefix_val = unicode(settings.get('param_sheet_prefix') or u'').strip() if prefix_val == u'': prefix_val = unicode(gs_lib.get('DEFAULT_PARAM_SHEET_PREFIX') or u'Параметры_Объединения') suppress_msg = bool(settings.get('suppress_param_action_messages')) if date_ctl is not None: _set_combo_items(date_ctl, date_choices, date_val or date_choices[0]) if num_ctl is not None: if num_val in num_choices: _set_combo_items(num_ctl, num_choices, num_val) elif num_val != u'': _set_combo_items(num_ctl, num_choices, num_val) try: num_ctl.setText(num_val) except Exception: pass else: _set_combo_items(num_ctl, num_choices, num_choices[0]) if font_name_ctl is not None: try: font_name_ctl.setText(font_name) except Exception: pass if font_size_ctl is not None: try: if font_size == int(font_size): font_size_ctl.setText(unicode(int(font_size))) else: font_size_ctl.setText(unicode(font_size)) except Exception: pass if prefix_ctl is not None: try: prefix_ctl.setText(prefix_val) except Exception: pass if suppress_ctl is not None: try: suppress_ctl.setState(1 if suppress_msg else 0) except Exception: pass handler.global_flag_rows = _global_flag_rows_from_settings(settings) _global_flags_refresh_listbox(dialog, handler.global_flag_rows, 0) _global_sync_flag_hint(dialog, handler.global_flag_rows, 0) def _save_global_settings_from_form(): if gs_lib is None: return u'Модуль глобальных настроек недоступен.' date_ctl = _dlg_control(dialog, 'GlobalDateFormatCombo') num_ctl = _dlg_control(dialog, 'GlobalNumberFormatCombo') font_name_ctl = _dlg_control(dialog, 'GlobalFontNameEdit') font_size_ctl = _dlg_control(dialog, 'GlobalFontSizeEdit') prefix_ctl = _dlg_control(dialog, 'GlobalParamPrefixEdit') suppress_ctl = _dlg_control(dialog, 'GlobalSuppressParamMsgCheck') date_fmt = u'' num_fmt = u'' font_name = u'' font_size = gs_lib.get('DEFAULT_FONT_SIZE') or 11.0 prefix_val = u'' suppress_msg = False if date_ctl is not None: try: date_fmt = unicode(date_ctl.getText() or u'').strip() except Exception: pass if num_ctl is not None: try: raw_num = unicode(num_ctl.getText() or u'').strip() except Exception: raw_num = u'' num_fmt = gs_lib['resolve_number_format_choice'](raw_num) if num_fmt == u'' and raw_num not in (u'', gs_lib['CUSTOM_NUMBER_FORMAT_LABEL']): num_fmt = raw_num if font_name_ctl is not None: try: font_name = unicode(font_name_ctl.getText() or u'').strip() except Exception: pass if font_size_ctl is not None: try: raw_sz = unicode(font_size_ctl.getText() or u'').strip() if raw_sz != u'': font_size = float(raw_sz.replace(u',', u'.')) except Exception: pass if prefix_ctl is not None: try: prefix_val = unicode(prefix_ctl.getText() or u'').strip() except Exception: pass if suppress_ctl is not None: try: suppress_msg = bool(suppress_ctl.getState()) except Exception: pass if date_fmt == u'': date_fmt = gs_lib['default_global_settings']()['default_date_format'] if font_name == u'': font_name = unicode(gs_lib['default_global_settings']().get('font_name') or u'PT Sans') if prefix_val == u'': prefix_val = unicode(gs_lib.get('DEFAULT_PARAM_SHEET_PREFIX') or u'Параметры_Объединения') merge_flags = {} for item in handler.global_flag_rows or []: key = unicode(item.get('global_key') or u'').strip() if key != u'': merge_flags[key] = bool(item.get('enabled')) payload = {'default_date_format': date_fmt, 'number_format': num_fmt, 'font_name': font_name, 'font_size': font_size, 'param_sheet_prefix': prefix_val, 'suppress_param_action_messages': suppress_msg, 'merge_flags': merge_flags} return gs_lib['save_global_settings'](payload) def _activate_global_tab(): _load_global_settings_to_form() tab_ctrl_geom = {} def _sync_global_tab_footer(is_global): footer_y = _pw_cfg._BTN_Y footer_y_peer = _pw_content_y(footer_y) try: dialog.Model.Height = _pw_cfg._DLG_H + _pw_cfg._SOFT_GRAY_TITLE_H except Exception: pass save_global_ctl = _dlg_control(dialog, 'SaveGlobalSettingsBtn') if save_global_ctl is not None: try: save_global_ctl.Model.PositionY = footer_y_peer save_global_ctl.Model.Height = _pw_cfg._BTN_H_MAIN save_global_ctl.Model.Width = _pw_cfg._ACTION_BTN_W except Exception: pass if handler.cancel_btn is not None: try: handler.cancel_btn.Model.PositionY = footer_y_peer handler.cancel_btn.Model.Height = _pw_cfg._BTN_H_MAIN handler.cancel_btn.Model.Width = _pw_cfg._ACTION_BTN_W handler.cancel_btn.Model.PositionX = _pw_cfg._CANCEL_BTN_X except Exception: pass if handler.ok_btn is not None: try: handler.ok_btn.Model.PositionY = footer_y_peer except Exception: pass _set_control_visible(handler.ok_btn, not is_global, footer_y, _pw_cfg._BTN_H_MAIN, _pw_cfg._ACTION_BTN_W) _set_control_visible(handler.cancel_btn, True, footer_y, _pw_cfg._BTN_H_MAIN, _pw_cfg._ACTION_BTN_W) def _show_wizard_tab(tab_name): handler.active_tab = tab_name is_param = tab_name == 'param' is_presets = tab_name == 'presets' is_global = tab_name == 'global' for nm in param_tab_controls: _wizard_dialog_set_visible(dialog, nm, is_param, geom_cache=tab_ctrl_geom) for nm in preset_tab_controls: _wizard_dialog_set_visible(dialog, nm, is_presets, geom_cache=tab_ctrl_geom) for nm in global_tab_controls: _wizard_dialog_set_visible(dialog, nm, is_global, geom_cache=tab_ctrl_geom) for nm in mode_tab_controls: _wizard_dialog_set_visible(dialog, nm, not is_global, geom_cache=tab_ctrl_geom) _set_control_enabled(handler.ok_btn, is_param) _set_control_enabled(handler.cancel_btn, is_param or is_global) _sync_global_tab_footer(is_global) handler._sync_param_action_buttons() if is_param: handler._apply_spec_to_form(handler._current_spec()) elif is_presets: _activate_presets_tab() elif is_global: _activate_global_tab() def _sync_preset_info_panel(preset_name=None): if preset_name is None: preset_name = _preset_name_from_form() comment, summary = _wp_preset_info_texts(presets, preset_name) comment_ctl = _dlg_control(dialog, 'PresetCommentField') summary_ctl = _dlg_control(dialog, 'PresetSummaryField') if comment_ctl is not None: try: comment_ctl.setText(comment) except Exception: pass if summary_ctl is not None: try: summary_ctl.setText(summary) except Exception: pass def _preset_name_from_form(): combo_name = _combo_selected_preset_name() name_edit = _dlg_control(dialog, 'PresetNameEdit') if name_edit is not None: try: t = name_edit.getText() if t is not None and unicode(t).strip() != u'': return unicode(t).strip() except Exception: pass return combo_name def _sync_preset_name_edit(name=u''): name_edit = _dlg_control(dialog, 'PresetNameEdit') if name_edit is None: return try: name_edit.setText(unicode(name) if name is not None else u'') except Exception: pass def _refresh_preset_combo(select_name=None, select_index=None, clear=False): combo = _dlg_control(dialog, 'PresetCombo') names = _wp_preset_names(presets) handler.preset_combo_busy = True try: selected = _apply_combo_selection(combo, names, select_name=select_name, select_index=select_index, clear=clear) _sync_preset_name_edit(selected) _sync_preset_info_panel(selected) finally: handler.preset_combo_busy = False def _clear_preset_combo_selection(): _refresh_preset_combo(clear=True) def _combo_selected_preset_name(): combo = _dlg_control(dialog, 'PresetCombo') if combo is None: return u'' try: pos = combo.getSelectedItemPos() if pos is not None and int(pos) >= 0: names = _wp_preset_names(presets) if int(pos) < len(names): return names[int(pos)] except Exception: pass try: text = combo.getText() if text is not None and unicode(text).strip() != u'': return unicode(text).strip() except Exception: pass return u'' def _apply_preset_by_name(name): preset = _wp_find_preset_by_name(presets, name) if preset is None: return u'Выберите пресет из списка.' err = _wp_apply_preset_rows(doc, handler.sheet, preset['rows'], catalog) if err: return err handler.initial = None handler._reload_param_combo() return None if sheet_names is None: sheet_names = _list_param_sheet_names_for_wizard(doc) default_sheet_name = unicode(sheet.Name) if sheet is not None else u'' if default_sheet_name not in sheet_names and len(sheet_names) > 0: default_sheet_name = sheet_names[0] handler = WizardHandler(dialog, initial) handler.sheet_names = sheet_names init_spec = None if initial is not None and initial.get('spec') is not None: init_spec = initial['spec'] if init_spec is None: boot_entries = _scan_sheet_param_entries(sheet, catalog) if len(boot_entries) > 0: init_spec = boot_entries[0]['spec'] if init_spec is None and len(catalog) > 0: init_spec = catalog[0] dialog.createPeer(toolkit, _wizard_dialog_parent()) _wizard_try_paint_titlebar(dialog) handler.param_combo = _dlg_control(dialog, 'ParamCombo') handler.sheet_combo = _dlg_control(dialog, 'SheetParamCombo') handler.new_param_btn = _dlg_control(dialog, 'NewParamModeBtn') handler.new_param_mode_lbl = _dlg_control(dialog, 'LblNewParamMode') handler.value_combo = _dlg_control(dialog, 'ValueCombo') handler.value_edit = _dlg_control(dialog, 'ValueEdit') handler.lbl_extra = _dlg_control(dialog, 'LblExtra') handler.lbl_plugin = _dlg_control(dialog, 'LblPlugin') handler.plugin_edit = _dlg_control(dialog, 'PluginEdit') handler.plugin_editor_btn = _dlg_control(dialog, 'PluginEditorBtn') handler.extra_edit = _dlg_control(dialog, 'ExtraEdit') handler.pivot_params_btn = _dlg_control(dialog, 'PivotParamsBtn') handler.col_c_params_btn = _dlg_control(dialog, 'ColCParamsBtn') handler.browse_btn = _dlg_control(dialog, 'BrowseButton') handler.files_clear_last_btn = _dlg_control(dialog, 'FilesClearLastBtn') handler.files_clear_all_btn = _dlg_control(dialog, 'FilesClearAllBtn') handler.ok_btn = _dlg_control(dialog, 'OkButton') handler.cancel_btn = _dlg_control(dialog, 'CancelButton') handler.save_param_btn = _dlg_control(dialog, 'SaveParamBtn') handler.delete_param_btn = _dlg_control(dialog, 'DeleteParamBtn') handler.lbl_value = _dlg_control(dialog, 'LblValue') handler.lbl_transfer = _dlg_control(dialog, 'LblTransfer') handler.transfer_combo = _dlg_control(dialog, 'TransferCombo') handler.desc_field = _dlg_control(dialog, 'DescField') handler.param_disabled_chk = _dlg_control(dialog, 'ParamDisabledCheck') handler.refresh_sheet_chk = _dlg_control(dialog, 'RefreshSheetCheck') if handler.sheet_combo is not None and len(sheet_names) > 0: _set_combo_items(handler.sheet_combo, sheet_names, default_sheet_name) handler.sheet_combo.addItemListener(handler) _sync_new_param_mode_ui(handler.new_param_btn, handler.new_param_mode_lbl, handler.new_param_mode) handler._reload_param_combo() cur_spec = handler._current_spec() if cur_spec is not None: init_spec = cur_spec if handler.param_combo is not None: handler.param_combo.addItemListener(handler) try: handler.param_combo.addTextListener(handler) except Exception: pass if handler.value_combo is not None: handler.value_combo.addItemListener(handler) try: handler.value_combo.addTextListener(handler) except Exception: pass if handler.transfer_combo is not None: handler.transfer_combo.addItemListener(handler) if handler.param_disabled_chk is not None: handler.param_disabled_chk.addItemListener(handler) global_flags_lb = _dlg_control(dialog, 'GlobalFlagsListBox') if global_flags_lb is not None: global_flags_lb.addItemListener(handler) class _GlobalFlagsMouse(unohelper.Base, XMouseListener): def disposing(self, event): pass def mousePressed(self, event): pass def mouseReleased(self, event): self._maybe_toggle_on_click(event) def _maybe_toggle_on_click(self, event): if handler.active_tab != 'global': return try: clicks = int(getattr(event, 'ClickCount', 0) or 0) except Exception: clicks = 0 # Переключать состояние по одиночному щелчку (не на двойном). if clicks != 1: return idx = _global_flags_selected_index(dialog) if idx < 0: try: y = int(getattr(event, 'Y', -1)) except Exception: y = -1 if y >= 0: row = int(y // 16) if 0 <= row < len(handler.global_flag_rows): idx = row if idx < 0: return if not _global_flags_toggle_index(handler.global_flag_rows, idx): return _global_flags_refresh_listbox(dialog, handler.global_flag_rows, idx) _global_sync_flag_hint(dialog, handler.global_flag_rows, idx) def mouseEntered(self, event): pass def mouseExited(self, event): pass try: global_flags_lb.addMouseListener(_GlobalFlagsMouse()) except Exception: pass preset_combo = _dlg_control(dialog, 'PresetCombo') if preset_combo is not None: preset_combo.addItemListener(handler) preset_name_edit = _dlg_control(dialog, 'PresetNameEdit') if preset_name_edit is not None: try: preset_name_edit.addTextListener(handler) except Exception: pass for ctl_name in ('BrowseButton', 'FilesClearLastBtn', 'FilesClearAllBtn', 'PivotParamsBtn', 'ColCParamsBtn', 'SourceExtraParamsBtn', 'PluginEditorBtn', 'OkButton', 'CancelButton', 'SaveParamBtn', 'DeleteParamBtn', 'TabBtnParam', 'TabBtnPresets', 'TabBtnGlobal', 'NewParamModeBtn', 'SavePresetBtn', 'DeletePresetBtn', 'ExportPresetBtn', 'ImportPresetBtn', 'ApplyPresetBtn', 'ClearAllPresetsBtn', 'SaveGlobalSettingsBtn'): ctl = _dlg_control(dialog, ctl_name) if ctl is not None: ctl.addActionListener(handler) elif ctl_name in ('SavePresetBtn', 'ApplyPresetBtn', 'ExportPresetBtn', 'ImportPresetBtn'): print(u'param_wizard: контрол не найден: %s' % ctl_name) _show_wizard_tab('param') _refresh_preset_combo() _sync_preset_info_panel(u'') if initial is not None and initial.get('new_param_mode') and (not handler.new_param_mode): handler._set_edit_mode(True) handler._apply_spec_to_form(init_spec) handler._sync_param_action_buttons() dialog.execute() return handler.result except Exception as e: print(u'Ошибка диалога: %s' % e) _show_message(u'Ошибка диалога: %s' % e) return None def _param_wizard_finalize(doc, sheet, catalog, spec, row, primary=u'', transfer_value=None, refresh_full_sheet=False, is_new=False): """После записи параметра: валидации, авто-высота, выделение, сообщение.""" if refresh_full_sheet: _param_wizard_post_save_full(doc, sheet, catalog, spec, int(row)) else: _param_wizard_post_save_row(doc, sheet, catalog, spec, int(row)) _param_wizard_select_row(doc, sheet, int(row)) action_verb = u'Добавлен' if is_new else u'Обновлён' action_verb_f = u'Добавлена' if is_new else u'Обновлена' msg = u'Параметр «%s» %s (строка %d, колонка B).\n\nДальше: проверьте значение на листе, при необходимости откройте визард снова или запустите макрос collect_workbooks.' % (spec['name'], u'добавлен' if is_new else u'обновлён', row + 1) if _spec_has_inline_transfer(spec) and transfer_value not in (None, u''): msg = u'Параметр «%s» %s (строка %d):\n Режим (B): %s\n Способ переноса (C): %s\n\nДальше: проверьте значения на листе параметров.' % (spec['name'], u'добавлен' if is_new else u'обновлён', row + 1, primary, transfer_value) elif spec.get('mode') in ('postprocess_range', 'postprocess_row'): msg = u'%s шаг постобработки «%s» (строка %d, B/C).\n\nПорядок шагов — сверху вниз на листе параметров.' % (action_verb, spec['name'], row + 1) elif spec.get('mode') == 'vlookup': if isinstance(primary, dict) and primary.get('save_as_json'): msg = u'%s операция ВПР (строка %d, колонка C — JSON).\n\nПорядок: между шагами «Постобработка_Диапазон», до «Постобработка_Строка».' % (action_verb_f, row + 1) else: msg = u'%s операция ВПР (строка %d, колонки B–F).\n\nПорядок: между шагами «Постобработка_Диапазон», до «Постобработка_Строка».' % (action_verb_f, row + 1) elif spec.get('mode') == 'files': paths, _fd = _split_files_path_text(primary) n = len(paths) if n == 0: msg = u'Параметр «%s» очищен (строка %d).\n\nПути к источникам удалены из строки параметра.' % (spec['name'], row + 1) elif n == 1: msg = u'Путь к источнику записан для «%s» (строка %d, колонка B).\n\nДобавьте ещё файлы через визард или соседние столбцы.' % (spec['name'], row + 1) else: msg = u'Записано %d путей для «%s» (строка %d, столбцы B и далее).\n\nПроверьте значения на листе параметров.' % (n, spec['name'], row + 1) _wp_notify_param_action(msg, doc=doc, title=u'Готово') def toggle_merge_param_disabled(*args): """ Переключить отключение параметра на выделенных строках листа «Параметры_Объединения»: префикс __ в колонке A и серый текст — без открытия визарда. Для каждой строки состояние меняется на противоположное текущему. """ try: with _pw_timed(u'toggle_merge_param_disabled (всего)'): _wp_apply_global_param_prefix() with _pw_timed(u'getDocument'): doc = XSCRIPTCONTEXT.getDocument() if doc is None: _show_message(u'Не открыта книга Calc.') return with _pw_timed(u'ensure_param_sheet'): sheet, sheet_err, sheet_created = _ensure_param_sheet(doc) if sheet is None: _show_message(sheet_err or u'Не удалось открыть лист «%s».' % _pw_cfg.MERGE_PARAM_SHEET_NAME) return if sheet_created: catalog = _build_wizard_catalog() _fill_new_param_sheet(doc, sheet, catalog) _show_message(u'Создан лист «%s». Выделите строки параметров и повторите.' % _pw_cfg.MERGE_PARAM_SHEET_NAME) return with _pw_timed(u'check active sheet'): try: ctrl = doc.getCurrentController() if ctrl is None or unicode(ctrl.getActiveSheet().Name) != unicode(sheet.Name): _show_message(u'Активируйте лист «%s» и выделите строки параметров.' % _pw_cfg.MERGE_PARAM_SHEET_NAME) return except Exception: _show_message(u'Не удалось определить активный лист.') return with _pw_timed(u'get_selection_row_range'): start_row, end_row = _get_selection_row_range(doc) if start_row is None or int(start_row) < 1: _show_message(u'Выделите строки параметров (не заголовок таблицы).') return start_row = int(start_row) end_row = int(end_row) if end_row < start_row: start_row, end_row = (end_row, start_row) with _pw_timed(u'build_wizard_catalog'): catalog = _build_wizard_catalog() toggled = [] skipped = [] row = start_row while row <= end_row: pname = _row_param_name(sheet, row) if pname == u'': skipped.append(row + 1) else: spec = _find_catalog_spec(catalog, pname) if spec is None: skipped.append(row + 1) else: canonical = _param_canonical_sheet_name(pname) new_disabled = not _param_sheet_name_is_disabled(pname) _pw_time_log(u'toggle row=%d param=%r -> disabled=%s' % (row + 1, canonical, new_disabled)) _write_param_row_sheet_name(sheet, row, canonical, new_disabled, catalog=catalog) toggled.append((row + 1, canonical, new_disabled)) row += 1 if not toggled: if skipped: _show_message(u'В выделении нет строк с параметрами из каталога (строки: %s).' % u', '.join((unicode(x) for x in skipped[:12]))) else: _show_message(u'В выделении нет строк с параметрами.') return with _pw_timed(u'select rows'): _param_wizard_select_rows(doc, sheet, start_row, end_row) if len(toggled) == 1: row_n, canonical, new_disabled = toggled[0] if new_disabled: msg = u'Параметр «%s» отключён (строка %d, префикс __).' % (canonical, row_n) else: msg = u'Параметр «%s» включён (строка %d).' % (canonical, row_n) else: n_on = sum((1 for _r, _c, dis in toggled if not dis)) n_off = len(toggled) - n_on msg = u'Переключено параметров: %d (включено: %d, отключено: %d).' % (len(toggled), n_on, n_off) if skipped: msg += u'\n\nПропущено строк: %d.' % len(skipped) with _pw_timed(u'show_message'): _show_message(msg, title=u'Параметры') except Exception as e: _show_message(u'Ошибка: %s' % e) print(u'Ошибка toggle_merge_param_disabled: %s' % e) def _wp_apply_global_param_prefix(): """Подставить префикс листа параметров из merge_global_settings.json.""" gs_lib = _wp_import_global_settings_lib() if gs_lib is None: return fn = gs_lib.get('apply_runtime_param_sheet_prefix') if fn is None: return try: fn() except Exception: pass def set_merge_param(*args): """ Визард параметра для листа параметров (префикс по умолч. Параметры_Объединения). Выбор параметра → описание → ввод значения → запись на лист. """ try: try: print(u'[param_wizard] set_merge_param MACRO_VERSION=%s' % MACRO_VERSION) except Exception: pass _wp_apply_global_param_prefix() doc = XSCRIPTCONTEXT.getDocument() if doc is None: _show_message(u'Не открыта книга Calc.') return sheet, sheet_err, sheet_created = _ensure_param_sheet(doc) if sheet is None: _show_message(sheet_err or u'Не удалось создать лист «%s».' % _pw_cfg.MERGE_PARAM_SHEET_NAME) return catalog = _build_wizard_catalog() if len(catalog) == 0: _show_message(u'Каталог параметров пуст.') return if sheet_created: _fill_new_param_sheet(doc, sheet, catalog) else: _apply_param_sheet_validations(sheet, catalog, doc=doc) plan = _wizard_launch_plan(doc, sheet, catalog) if plan.get('action') == 'subdialog': if _run_subdialog_and_save(doc, sheet, catalog, plan.get('subdialog')): return True return False main_initial = plan.get('initial') if plan.get('new_param_mode'): if main_initial is None: main_initial = {'new_param_mode': True} else: main_initial = dict(main_initial) main_initial['new_param_mode'] = True result = create_param_wizard_dialog(doc, sheet, catalog, main_initial, _list_param_sheet_names_for_wizard(doc)) if result is None or result[0] != 'ok': return _ok, spec, primary, extra, row, ctx_col = result[:6] transfer_value = result[6] if len(result) > 6 else None refresh_full = result[7] if len(result) > 7 else False was_edit = result[8] if len(result) > 8 else False _param_wizard_finalize(doc, sheet, catalog, spec, int(row), primary=primary, transfer_value=transfer_value, refresh_full_sheet=refresh_full, is_new=(not was_edit)) except Exception as e: _show_message(u'Ошибка: %s' % e) print(u'Ошибка set_merge_param: %s' % e) def _merge_param_autofit_moved_rows(doc, sheet, new_start, new_end, touch_start=None, touch_end=None, pad=2, pct=10): """ Автофит + pct% для перемещённого блока, соседней «пропущенной» строки и pad строк сверху/снизу. Берём объединение [new_start..new_end] и [touch_start..touch_end], затем расширяем на pad. Перед OptimalRowHeight сбрасываем Height — иначе после insert/remove нижняя строка часто остаётся с высотой верхней. """ if sheet is None or new_start is None or new_end is None: return try: sr = int(new_start) er = int(new_end) p = int(pad) except Exception: return if er < sr: sr, er = (er, sr) if touch_start is not None and touch_end is not None: try: ts = int(touch_start) te = int(touch_end) if te < ts: ts, te = (te, ts) if ts < sr: sr = ts if te > er: er = te except Exception: pass if p < 0: p = 0 last_row = _sheet_last_used_row(sheet, 30) if last_row < 0: return fit_start = sr - p if fit_start < 0: fit_start = 0 fit_end = er + p if fit_end > last_row: fit_end = last_row end_col = _sheet_last_used_col(sheet, 6) if end_col < 5: end_col = 5 _param_autofit_rows_plus_pct(doc, sheet, fit_start, fit_end, end_col=end_col, pct=pct) def merge_param_go_up(*args): """ Сдвинуть выделенные строки параметров на 1 строку вверх (с перестановкой, без потерь). Если активна одна ячейка — двигается её строка. После сдвига: автофит перемещённых строк ±2 соседних + +10% к высоте. """ try: doc = XSCRIPTCONTEXT.getDocument() if doc is None: _show_message(u'Не открыта книга Calc.') return sheet, sheet_err = _active_param_sheet_for_move(doc) if sheet is None: _show_message(sheet_err or u'Активируйте лист параметров.') return start_row, end_row = _get_selection_row_range(doc) ok, err, new_start, new_end, touch_start, touch_end = _merge_param_move_rows(doc, sheet, start_row, end_row, direction=-1, min_row=1) if not ok: _show_message(err or u'Нельзя сдвинуть вверх.') return _merge_param_autofit_moved_rows(doc, sheet, new_start, new_end, touch_start=touch_start, touch_end=touch_end, pad=2, pct=10) _param_wizard_select_rows(doc, sheet, new_start, new_end) except Exception as e: _show_message(u'Ошибка: %s' % e) print(u'Ошибка merge_param_go_up: %s' % e) def merge_param_go_down(*args): """ Сдвинуть выделенные строки параметров на 1 строку вниз (с перестановкой, без потерь). Если активна одна ячейка — двигается её строка. После сдвига: автофит перемещённых строк ±2 соседних + +10% к высоте. """ try: doc = XSCRIPTCONTEXT.getDocument() if doc is None: _show_message(u'Не открыта книга Calc.') return sheet, sheet_err = _active_param_sheet_for_move(doc) if sheet is None: _show_message(sheet_err or u'Активируйте лист параметров.') return start_row, end_row = _get_selection_row_range(doc) ok, err, new_start, new_end, touch_start, touch_end = _merge_param_move_rows(doc, sheet, start_row, end_row, direction=+1, min_row=1) if not ok: _show_message(err or u'Нельзя сдвинуть вниз.') return _merge_param_autofit_moved_rows(doc, sheet, new_start, new_end, touch_start=touch_start, touch_end=touch_end, pad=2, pct=10) _param_wizard_select_rows(doc, sheet, new_start, new_end) except Exception as e: _show_message(u'Ошибка: %s' % e) print(u'Ошибка merge_param_go_down: %s' % e) CONTEXT_CELL_MENU_KEY = 'param_wizard' __CONTEXT_CELL_MENU = ('submenu#PythonMacros', 'run_function#set_merge_param;module#param_wizard.py;display_name#Мастер параметров;order#20', 'separator#top', 'run_function#toggle_merge_param_disabled;module#param_wizard.py;display_name#Вкл/выкл параметр;order#30', 'run_function#merge_param_go_up;module#param_wizard.py;display_name#Параметр вверх;order#40', 'run_function#merge_param_go_down;module#param_wizard.py;display_name#Параметр вниз;order#50') CONTEXT_CELL_MENU = ('submenu#PythonMacros', 'separator#before', 'run_function#set_merge_param;module#param_wizard.py;display_name#Мастер параметров;order#20', 'run_function#toggle_merge_param_disabled;module#param_wizard.py;display_name#Вкл/выкл параметр;order#30', 'run_function#merge_param_go_up;module#param_wizard.py;display_name#Параметр вверх;order#25', 'run_function#merge_param_go_down;module#param_wizard.py;display_name#Параметр вниз;order#35', 'separator#after') g_exportedScripts = (set_merge_param, toggle_merge_param_disabled, merge_param_go_up, merge_param_go_down)