From bb4bd07f5f44a730375a40bf0b0f7232623bab67 Mon Sep 17 00:00:00 2001
From: Maryshca <kuzmichova.maria@ya.ru>
Date: Thu, 26 Sep 2024 19:33:32 +0300
Subject: [PATCH] v 2.0 init

---
 README.md     |   4 +-
 app.py        | 415 +++-----------------------------------------------
 callbacks.py  | 128 ++++++++++++++++
 constants.py  |  45 ++++++
 dynamic_ui.py | 110 +++++++++++++
 generator.py  |  36 -----
 handlers.py   |  89 +++++++++++
 remote_run.py |  93 +++++++++++
 static_ui.py  | 132 ++++++++++++++++
 styles.py     |  72 +++------
 utils.py      | 180 ++++++++++++++++++----
 11 files changed, 790 insertions(+), 514 deletions(-)
 create mode 100644 callbacks.py
 create mode 100644 constants.py
 create mode 100644 dynamic_ui.py
 create mode 100644 handlers.py
 create mode 100644 remote_run.py
 create mode 100644 static_ui.py

diff --git a/README.md b/README.md
index 54e2598..08342a5 100644
--- a/README.md
+++ b/README.md
@@ -2,12 +2,12 @@
 
 ## Как запустить?
 
-`pip install dearpygui numpy`
+`pip install dearpygui numpy paramiko scp python-dotenv`
 
 `git clone http://tesla.parallel.ru/kuzmichovamary/gui.git`
 
 `cd gui`
 
-`python prerun.py` -- один раз (тут собирается `config-parser` ;)
+`python prerun.py` — один раз (тут собирается `config-parser` ;)
 
 `python app.py`
\ No newline at end of file
diff --git a/app.py b/app.py
index a76068a..5c4fc7c 100644
--- a/app.py
+++ b/app.py
@@ -4,403 +4,28 @@ import sys
 
 import dearpygui.dearpygui as dpg
 import numpy as np
+from dotenv import load_dotenv
 
-from styles import GuiStyle, INDENT, SPACE, FONT_SIZE, ICON_PATH
-from config import load_config, dump_config
-from generator import LCZ
-from utils import *
+from static_ui import construct_ui
+from constants import ICON_PATH
 
-ctypes.windll.shcore.SetProcessDpiAwareness(1)
+load_dotenv()
 
-TABS_UI = 'tabs_ui'
-COLLAPSING_HEADERS_UI = 'collapsing_headers_ui'
-WIDGET_WIDTH = 300
-XOFFSET = 300
-LCZS = ['compact_high_rise', 'compact_low_rise', 'compact_mid_rise', 'heavy_industry', 'large_low_rise', 'lightweight_low_rise', 'open_high_rise', 'open_low_rise', 'open_mid_rise', 'sparsley_build']
-
-UIS = [TABS_UI, COLLAPSING_HEADERS_UI]
-
-class ConfigEditor:
-    def __init__(self):
-        self.config = dict()
-        self.config_file_path = str()
-
-        self.height_map = None
-        self.height_map_path = str()
-
-        self.current_action = None
-        self.prev_coords = None
-        self.drawing = False
-
-        dpg.create_context()
-        dpg.create_viewport(
-            title='config file editor',
-            small_icon=ICON_PATH,
-            large_icon=ICON_PATH
-        )
-        dpg.setup_dearpygui()
-        dpg.show_viewport()
-
-        self.styles = GuiStyle()
-
-        self.construct_main_window_ui()
-        if self.config_file_path:
-            self.construct_config_ui()
-
-    def update_texture(self):
-        if not self.height_map_path:
-            return
-        max_height = np.max(self.height_map)
-        normalized_map = self.height_map / max_height
-
-        texture_data = normalized_map.flatten()
-        texture_data = np.tile(texture_data, (4, 1)).T.flatten()
-
-        texture_data.resize(800 * 800 * 4)
-
-        width, height = self.height_map.shape
-
-        dpg.set_value('heatmap_texture', texture_data)
-
-    def map_edit_window_on_close(self):
-        self.height_map = None
-        self.height_map_path = str()
-
-        self.current_action = None
-        self.prev_coords = None
-        self.drawing = False
-
-    def delete_config_ui_and_clear_aliases(self):
-        if dpg.does_item_exist(self.config_file_path):
-            if dpg.get_item_type(self.config_file_path) == 'mvAppItemType::mvChildWindow':
-                uuid = dpg.get_item_parent(dpg.get_item_parent(self.config_file_path))
-                dpg.delete_item(uuid)
-            else:
-                dpg.delete_item(self.config_file_path)
-
-        if dpg.does_alias_exist(self.config_file_path):
-            dpg.remove_alias(self.config_file_path)
-        for full_path in self.config:
-            for path in path_iter(full_path, prefix=self.config_file_path):
-                if dpg.does_alias_exist(path):
-                    dpg.remove_alias(path)
-
-    def update_config_values(self):
-        for path in self.config:
-            if dpg.does_item_exist(f'{self.config_file_path}.{path}'):
-                self.config[path].value = dpg.get_value(f'{self.config_file_path}.{path}')
-
-    def manage_file(self, sender, sender_data, user_data):
-        file_path = sender_data['file_path_name']
-
-        if not file_path:
-            return
-
-        if user_data == 'open_config':
-            self.delete_config_ui_and_clear_aliases()
-            self.config_file_path = file_path
-            self.config = load_config(file_path)
-            self.construct_config_ui(dpg.get_item_user_data('ui'))
-        elif user_data == 'open_map':
-            self.height_map_path = file_path
-            self.height_map = load_height_map(file_path)
-            dpg.show_item('map_window')
-            self.update_texture()
-        elif user_data == 'save_as' or user_data == 'save':
-            self.update_config_values()
-            dump_config(self.config, file_path)
-            dpg.show_item('file_save_popup')
-        elif user_data == 'save_map_as' or user_data == 'save_map':
-            dump_map(self.height_map, file_path)
-            dpg.show_item('file_save_popup')
-
-    def generate_map(self, sender, sender_data, user_data):
-        self.height_map_path = 'lcz.txt'
-        self.height_map = LCZ(config_path=f'configs/{sender}.json').to_height_map()
-        dpg.show_item('map_window')
-        self.update_texture()
-
-    def set_file_path(self, sender, sender_data, user_data):
-        file_path = sender_data['file_path_name']
-        dpg.set_value(user_data, file_path)
-
-    def open_file_dialog(self, sender, sender_data, user_data):
-        dpg.show_item('file_dialog')
-        dpg.set_item_user_data('file_dialog', user_data)
-
-    def open_folder_dialog(self, sender, sender_data, user_data):
-        dpg.show_item('folder_dialog')
-        dpg.set_item_user_data('folder_dialog', user_data)
-
-    def set_action(self, action):
-        self.current_action = action
-
-    def check_coords(self, x, y, w, h):
-        mx, my = dpg.get_item_pos('map')
-        mw, mh = dpg.get_item_width('map'), dpg.get_item_height('map')
-
-        return mx <= x <= x + w <= mx + mw and my <= y <= y + h <= my + mh
-
-    def mouse_down_callback(self):
-        if not self.height_map_path:
-            return
-        if not self.drawing:
-            self.prev_coords = dpg.get_mouse_pos()
-            self.drawing = True
-        else:
-            x, y = dpg.get_mouse_pos()
-            px, py = self.prev_coords
-            y += dpg.get_text_size('edit map')[1] + 2 * SPACE + dpg.get_y_scroll('map_window')
-            py += dpg.get_text_size('edit map')[1] + 2 * SPACE + dpg.get_y_scroll('map_window')
-            x += dpg.get_x_scroll('map_window')
-            px += dpg.get_x_scroll('map_window')
-            w, h = abs(x - px), abs(y - py)
-            x, y = min(x, px), min(y, py)
-
-            if w > 0 and h > 0 and self.check_coords(x, y, w, h):
-                dpg.set_item_pos('drawing_frame', [min(x, px), min(y, py)])
-                dpg.set_item_width('drawing_frame', w)
-                dpg.set_item_height('drawing_frame', h)
-                dpg.show_item('drawing_frame')
-
-    def mouse_release_callback(self):
-        coords = dpg.get_mouse_pos()
-
-        if self.current_action == 'erase':
-            self.draw_rectangle(coords, 0)
-        elif self.current_action == 'draw_rect':
-            height = dpg.get_value('height_input')
-            self.draw_rectangle(coords, np.max(self.height_map) * height)
-
-        self.update_texture()
-
-        self.drawing = False
-        dpg.hide_item('drawing_frame')
-
-    def draw_rectangle(self, coords, height):
-        y1, y2, x1, x2 = get_rect(self.prev_coords, coords)
-        self.height_map[y1:y2, x1:x2] = height
-
-    def run_on_lab_server(self):
-        pass
-
-    def construct_config_ui(self, ui):
-        if not self.config_file_path:
-            return
-
-        self.update_config_values()
-        self.delete_config_ui_and_clear_aliases()
-        if ui == TABS_UI:
-            self.construct_config_ui_tabs()
-        else:
-            self.construct_config_ui_collapsing_headers()
-
-    def construct_main_window_ui(self):
-        dpg.bind_font(self.styles.montserrat_font)
-        self.styles.apply_theme('orange')
-
-        with dpg.window(label='config file editor', tag='main'):
-            with dpg.menu_bar():
-                with dpg.menu(label='config'):
-                    dpg.add_menu_item(label='open', callback=self.open_file_dialog, tag='open_btn', user_data='open_config')
-                    dpg.add_menu_item(label='save', callback=lambda: self.manage_file('', {'file_path_name': self.config_file_path}, 'save'))
-                    dpg.add_menu_item(label='save as', callback=self.open_file_dialog, tag='save_as_btn', user_data='save_as')
-
-                with dpg.menu(label='map'):
-                    dpg.add_menu_item(label='open map', callback=self.open_file_dialog, tag='open_map_btn', user_data='open_map')
-                    with dpg.menu(label='generate map'):
-                        for lcz_type in LCZS:
-                            dpg.add_menu_item(label=lcz_type, check=False, callback=combo_menu(self.generate_map), tag=lcz_type)
-
-                with dpg.menu(label='run'):
-                    dpg.add_menu_item(label='run on lab server', callback=self.run_on_lab_server)
-
-                with dpg.menu(label='view'):
-                    with dpg.menu(label='ui', tag='ui', user_data=COLLAPSING_HEADERS_UI):
-                        for ui in UIS:
-                            dpg.add_menu_item(label=ui, check=True, default_value=False, callback=combo_menu(lambda s, a, u: self.construct_config_ui(s)), tag=ui)
-                        dpg.set_value(UIS[1], True)
-
-                    with dpg.menu(label='theme'):
-                        for theme in self.styles.themes:
-                            dpg.add_menu_item(label=theme, check=True, default_value=False, callback=combo_menu(lambda s, a, u: self.styles.apply_theme(s)), tag=theme)
-                        dpg.set_value('orange', True)
-                    
-                    dpg.add_menu_item(label='show theme editor', callback=dpg.show_style_editor)
-
-                with dpg.menu(label='settings'):
-                    dpg.add_menu_item(label='show mouse coords', check=True, callback=lambda _, v: dpg.show_item('mouse') if v else dpg.hide_item('mouse'), default_value=False)
-
-                dpg.add_text('', tag='mouse', show=False)
-
-        with dpg.file_dialog(directory_selector=False, show=False, callback=self.manage_file, tag='file_dialog', width=600, height=400):
-            dpg.add_file_extension('.txt', color=(0, 255, 0, 255))
-            dpg.add_file_extension('.*', color=(255, 255, 255, 255))
-
-        with dpg.file_dialog(directory_selector=True, show=False, callback=self.set_file_path, tag='folder_dialog', width=500, height=400):
-            dpg.add_file_extension('.*', color=(255, 255, 255, 255))
-
-        with dpg.window(label='', autosize=True, no_resize=True, modal=True, tag='file_save_popup', show=False, pos=(40, 40)):
-            dpg.add_text('file saved successfully:)')
-
-        with dpg.item_handler_registry(tag='main_handler'):
-            dpg.add_item_resize_handler(callback=lambda: self.change_height(self.config_file_path))
-
-        dpg.bind_item_handler_registry('main', 'main_handler')
-        dpg.set_viewport_resize_callback(lambda: self.change_height(self.config_file_path))
-
-        with dpg.texture_registry():
-            dpg.add_dynamic_texture(800, 800, np.zeros((800, 800, 4), dtype=np.uint8), tag='heatmap_texture')
-
-        with dpg.theme() as item_theme:
-            with dpg.theme_component(dpg.mvAll):
-                dpg.add_theme_color(dpg.mvThemeCol_ChildBg, (0, 0, 0, 0), category=dpg.mvThemeCat_Core)
-                dpg.add_theme_style(dpg.mvStyleVar_ChildRounding, 0, category=dpg.mvThemeCat_Core)
-
-        dpg.add_colormap_registry(label="colormap registry", tag="colormap_registry")
-        dpg.add_colormap(list(map(apply_tint, [[0, 0, 0], [100, 100, 100], [255, 255, 255]])), False, tag="colormap", parent="colormap_registry")
-
-        with dpg.window(label='edit map', tag='map_window', horizontal_scrollbar=True, show=False, width=600, height=400, pos=[20, 60], on_close=self.map_edit_window_on_close):
-            with dpg.menu_bar():
-                with dpg.menu(label='file'):
-                    dpg.add_menu_item(label='save', callback=lambda: self.manage_file('', {'file_path_name': self.height_map_path}, 'save_map'))
-                    dpg.add_menu_item(label='save as', callback=self.open_file_dialog, tag='save_map_as_btn', user_data='save_map_as')
-
-            with dpg.group(horizontal=True):
-                with dpg.group(width=200, tag='tools'):
-                    dpg.add_button(label='erase', callback=lambda: self.set_action('erase'))
-                    dpg.add_button(label='draw rectangle', callback=lambda: self.set_action('draw_rect'))
-                    dpg.add_colormap_slider(tag='height_input')
-                    dpg.bind_colormap(dpg.last_item(), "colormap")
-                dpg.add_image('heatmap_texture', tint_color=(0, 119, 200, 255), tag='map')
-
-            dpg.add_child_window(tag='drawing_frame', show=False, width=0, height=0)
-
-        dpg.bind_item_theme('drawing_frame', item_theme)
-
-        def clb(s, a, u):
-            dpg.set_value('mouse', str(a))
-
-        with dpg.handler_registry():
-            dpg.add_mouse_move_handler(callback=clb)
-            dpg.add_mouse_down_handler(callback=self.mouse_down_callback)
-            dpg.add_mouse_release_handler(callback=self.mouse_release_callback)
-
-    def construct_config_ui_collapsing_headers(self):
-        dpg.add_collapsing_header(label=self.config_file_path, parent='main', tag=self.config_file_path, indent=0, default_open=True)
-
-        for full_path in self.config:
-            namespaces = full_path.split('.')
-            parent = self.config_file_path
-            for level in range(len(namespaces) - 1):
-                path = self.config_file_path + '.' + '.'.join(namespaces[:level+1])
-
-                if not dpg.does_item_exist(path):
-                    dpg.add_collapsing_header(label=namespaces[level], parent=parent, tag=path, indent=INDENT)
-
-                parent = path
-
-            self.create_widget(full_path)
-
-    def construct_config_ui_tabs(self):
-        tab_bar_exists, tab_bar_uuid = does_tab_bar_child_exist('main')
-        if not tab_bar_exists:
-            tab_bar_uuid = dpg.add_tab_bar(parent='main')
-        tab_uuid = dpg.add_tab(label=self.config_file_path, parent=tab_bar_uuid)
-        dpg.add_child_window(parent=tab_uuid, tag=self.config_file_path, border=True, autosize_y=False, autosize_x=True)
-        dpg.set_item_user_data(self.config_file_path, 2 * SPACE)
-
-        for full_path in self.config:
-            namespaces = full_path.split('.')
-            parent = self.config_file_path
-            for level in range(len(namespaces) - 1):
-                path = self.config_file_path + '.' + '.'.join(namespaces[:level+1])
-
-                tab_bar_exists, tab_bar_uuid = does_tab_bar_child_exist(parent)
-
-                if not tab_bar_exists:
-                    tab_bar_uuid = dpg.add_tab_bar(parent=parent, callback=lambda s, a: self.change_height(a))
-                    dpg.set_item_user_data(parent, dpg.get_item_user_data(parent) + 4 * SPACE + FONT_SIZE)
-
-                if not dpg.does_item_exist(path):
-                    tab_uuid = dpg.add_tab(label=namespaces[level], parent=tab_bar_uuid)
-                    dpg.add_child_window(parent=tab_uuid, tag=path, border=True, autosize_y=False, autosize_x=True)
-                    dpg.set_item_user_data(path, 2 * SPACE)
-
-                parent = path
-
-            self.create_widget(full_path)
-
-    def change_height(self, item):
-        if not dpg.does_item_exist(item):
-            return
-        if dpg.get_item_type(item) == 'mvAppItemType::mvChildWindow':
-            uuid = dpg.get_item_parent(item)
-            new_height = dpg.get_viewport_client_height() - get_absolute_y(uuid) - SPACE * get_item_level(item)
-
-            dpg.set_item_height(item, max(new_height, dpg.get_item_user_data(item)))
-
-        for child in dpg.get_item_children(item, 1):
-            self.change_height(child)
-
-    def create_widget(self, full_path):
-        tag = self.config_file_path + '.' + full_path
-        parent =  '.'.join([self.config_file_path] + full_path.split('.')[:-1])
-
-        value = self.config[full_path].value
-        value_type = self.config[full_path].value_type
-        key = self.config[full_path].key
-        comment = self.config[full_path].comment
-
-        if dpg.get_value('use_tabs_ui') and parent != self.config_file_path and full_path != key:
-            dpg.set_item_user_data(parent, dpg.get_item_user_data(parent) + 2 * SPACE + FONT_SIZE + SPACE)
-
-        indent = INDENT
-        if dpg.get_value('use_tabs_ui'):
-            indent = 0
-
-        with dpg.group(horizontal=True, parent=parent, indent=indent, xoffset=XOFFSET):
-            if not comment:
-                dpg.add_text(key)
-                if value_type == 'BOOLEAN':
-                    dpg.add_checkbox(default_value=value, tag=tag)
-                elif value_type == 'DOUBLE':
-                    dpg.add_input_double(default_value=value, tag=tag, format='%.10f', width=WIDGET_WIDTH)
-                elif value_type == 'INT':
-                    dpg.add_input_int(default_value=value, tag=tag, width=WIDGET_WIDTH)
-                elif 'dir' in key.lower() or 'file' in key.lower():
-                    with dpg.group(horizontal=True):
-                        dpg.add_input_text(default_value=value, tag=tag, width=WIDGET_WIDTH)
-                        dpg.add_button(label='...', callback=self.open_folder_dialog, user_data=tag)
-                else:
-                    dpg.add_input_text(default_value=value, tag=tag, width=WIDGET_WIDTH)
-            else:
-                dpg.add_text(key)
-                with dpg.group(horizontal=True, xoffset=WIDGET_WIDTH + 100):
-                    if value_type == 'BOOLEAN':
-                        dpg.add_checkbox(default_value=value, tag=tag)
-                    elif value_type == 'DOUBLE':
-                        dpg.add_input_double(default_value=value, tag=tag, format='%.10f', width=WIDGET_WIDTH)
-                    elif value_type == 'INT':
-                        dpg.add_input_int(default_value=value, tag=tag, width=WIDGET_WIDTH)
-                    elif 'dir' in key.lower() or 'file' in key.lower():
-                        with dpg.group(horizontal=True):
-                            dpg.add_input_text(default_value=value, tag=tag, width=WIDGET_WIDTH - 3 * SPACE - dpg.get_text_size('...')[0])
-                            dpg.add_button(label='...', callback=self.open_folder_dialog, user_data=tag)
-                    else:
-                        dpg.add_input_text(default_value=value, tag=tag, width=WIDGET_WIDTH)
-                    dpg.add_text(comment, color=(255, 255, 255, 150))
-
-    def run(self):
-        dpg.set_primary_window('main', True)
-        dpg.start_dearpygui()
-        dpg.destroy_context()
+if sys.platform.startswith('win'):
+    ctypes.windll.shcore.SetProcessDpiAwareness(1)
 
 if __name__ == '__main__':
-    app = ConfigEditor()
-
-    if len(sys.argv) > 1:
-        app.config_file_path = sys.argv[1]
-        app.config = load_config(sys.argv[1])
-
-    app.run()
\ No newline at end of file
+    dpg.create_context()
+    dpg.create_viewport(
+        title='config file editor',
+        small_icon=ICON_PATH,
+        large_icon=ICON_PATH
+    )
+    dpg.setup_dearpygui()
+    dpg.show_viewport()
+
+    construct_ui()
+    
+    dpg.set_primary_window('main', True)
+    dpg.start_dearpygui()
+    dpg.destroy_context()
\ No newline at end of file
diff --git a/callbacks.py b/callbacks.py
new file mode 100644
index 0000000..34417fc
--- /dev/null
+++ b/callbacks.py
@@ -0,0 +1,128 @@
+import dearpygui.dearpygui as dpg
+
+from utils import *
+from remote_run import start_remote_execution
+from styles import get_accent_color, apply_theme
+from generator import LCZ
+from config import load_config, dump_config
+from dynamic_ui import construct_config_ui
+
+def map_window_on_close_clb(s, a, u):
+    dpg.set_item_user_data('map_window', {'height_map': None, 'height_map_file_path': None, 'drawing': False, 'prev_coords': None, 'current_action': None})
+
+def construct_config_ui_clb(s, a, u):
+    if not get_config_file_path():
+        return
+    delete_item_children_and_clear_aliases()
+    update_config_values()
+    construct_config_ui(get_config_file_path(), get_config())
+
+def map_open_clb(s, a, u):
+    height_map_file_path = a['file_path_name']
+    height_map = load_height_map(height_map_file_path)
+
+    set_map_window_value('height_map', height_map)
+    set_map_window_value('height_map_file_path', height_map_file_path)
+
+    update_texture(height_map)
+    dpg.show_item('map_window')
+
+def generate_map_clb(s, a, u):
+    height_map = LCZ(config_path=f'configs/{s}.json').to_height_map()
+
+    set_map_window_value('height_map', height_map)
+    set_map_window_value('height_map_file_path', 'lcz.txt')
+
+    update_texture(height_map)
+    dpg.show_item('map_window')
+
+def map_save_clb(s, a, u):
+    dump_map(get_height_map(), get_height_map_file_path())
+    dpg.show_item('message_popup')
+
+def map_save_as_clb(s, a, u):
+    dump_map(get_height_map(), a['file_path_name'])
+    dpg.show_item('message_popup')
+
+def open_map_save_as_dialog_clb(s, a, u):
+    dpg.show_item('map_save_as')
+
+def open_map_open_dialog_clb(s, a, u):
+    dpg.show_item('map_open')
+
+def open_config_open_dialog_clb(s, a, u):
+    dpg.show_item('config_open')
+
+def search_clb(s, data, u):
+    if not get_config_file_path():
+        return
+
+    if not data:
+        return
+
+    for path in get_config():
+        if data.lower() in path.split('.')[-1]:
+            dpg.set_y_scroll('main', dpg.get_item_pos('.'.join([get_config_file_path(), path.split('.')[0]]))[1])
+            for tag in path_iter(path, prefix=get_config_file_path()):
+                if dpg.get_value(COLLAPSING_HEADERS_UI) == True:
+                    dpg.set_value(tag, True)
+                else:
+                    dpg.set_value(dpg.get_item_parent(dpg.get_item_parent(tag)), dpg.get_item_parent(tag))
+            change_color_temporarily('.'.join([get_config_file_path(), path, 'text']), color=get_accent_color())
+
+def remote_run_clb(s, a, u):
+    if not get_config_file_path():
+        show_status_text('no config.txt specified. cannot run.')
+        return
+
+    update_config_values()
+    dump_config(get_config(), 'tmp.txt')
+
+    dpg.show_item('login_modal')
+
+def apply_theme_clb(s, a, u):
+    apply_theme(s)
+
+def set_file_path_clb(s, a, u):
+    file_path = a['file_path_name']
+    dpg.set_value(u, file_path)
+
+def show_config_search_window_clb(s, a, u):
+    dpg.show_item('search_popup')
+
+def start_remote_execution_clb(s, a, u):
+    start_remote_execution(dpg.get_value('server_username'), dpg.get_value('server_password'), dpg.get_value('gitlab_username'), dpg.get_value('gitlab_password'))
+
+def update_config_values():
+    config = get_config()
+    for path in config:
+        if dpg.does_item_exist(f'{get_config_file_path()}.{path}'):
+            config[path].value = dpg.get_value(f'{get_config_file_path()}.{path}')
+    set_main_window_value('config', config)
+
+def config_save_as_clb(s, a, u):
+    update_config_values()
+    dump_config(get_config(), a['file_path_name'])
+    dpg.show_item('message_popup')
+
+def config_open_clb(s, a, u):
+    delete_item_children_and_clear_aliases()
+    config_file_path = a['file_path_name']
+    config = load_config(config_file_path)
+    set_main_window_value('config', config)
+    set_main_window_value('config_file_path', config_file_path)
+    construct_config_ui(a['file_path_name'], config)
+
+def config_save_clb(s, a, u):
+    update_config_values()
+    dump_config(get_config(), get_config_file_path())
+    dpg.show_item('message_popup')
+
+def set_action_clb(s, a, u):
+    set_map_window_value('current_action', s)
+
+def set_action_none_clb(s, a, u):
+    set_map_window_value('current_action', None)
+
+def open_config_save_as_dialog_clb(s, a, u):
+    dpg.show_item('config_open')
\ No newline at end of file
diff --git a/constants.py b/constants.py
new file mode 100644
index 0000000..dbeadab
--- /dev/null
+++ b/constants.py
@@ -0,0 +1,45 @@
+import os
+
+TABS_UI = 'tabs_ui'
+COLLAPSING_HEADERS_UI = 'collapsing_headers_ui'
+WIDGET_WIDTH = 300
+XOFFSET = 300
+LCZS = ['compact_high_rise', 'compact_low_rise', 'compact_mid_rise', 'heavy_industry', 'large_low_rise', 'lightweight_low_rise', 'open_high_rise', 'open_low_rise', 'open_mid_rise', 'sparsley_build']
+
+UIS = [TABS_UI, COLLAPSING_HEADERS_UI]
+
+FONT_PATH = os.path.join('fonts', 'Montserrat-Medium.ttf')
+ICON_PATH = os.path.join('icons', 'icon.ico')
+FONT_SIZE = 24
+ACCENT_COLOR = (0, 119, 200, 100)
+ROUNDING = 10
+SPACE = 10
+INDENT = 40
+BORDER = 3
+
+TEXT = (0, 1)
+ACCENT = (5, 18, 19, 20, 15, 16, 35, 37, 34, 23, 22, 25, 26, 27, 28, 29, 10, 11, 12, 30, 31, 32, 49, 50)
+PRIMARY = (2, 3, 39)
+SECONDARY = (36, 33, 21, 24, 4, 7, 8, 9, 13, 14)
+APPLY_ALPHA = (1, 22, 25, 31, 34, 16)
+COLOR_TYPE_TO_DPG_ITEM = {'TEXT': (0, 1), 'PRIMARY': (2, 3, 39), 'SECONDARY': (36, 33, 21, 24, 4, 7, 8, 9, 13, 14), 'ACCENT': (5, 18, 19, 20, 15, 16, 35, 37, 34, 23, 22, 25, 26, 27, 28, 29, 10, 11, 12, 30, 31, 32, 49, 50)}
+COLOR_TO_COLOR_TYPE = {0: 'TEXT', 1: 'TEXT', 2: 'PRIMARY', 3: 'PRIMARY', 39: 'PRIMARY', 36: 'SECONDARY', 21: 'SECONDARY', 24: 'SECONDARY', 4: 'SECONDARY', 7: 'SECONDARY', 8: 'SECONDARY', 9: 'SECONDARY', 13: 'SECONDARY', 33: 'SECONDARY', 14: 'SECONDARY', 35: 'ACCENT', 20: 'ACCENT', 37: 'ACCENT', 22: 'ACCENT', 23: 'ACCENT', 5: 'ACCENT', 25: 'ACCENT', 26: 'ACCENT', 27: 'ACCENT', 28: 'ACCENT', 29: 'ACCENT', 10: 'ACCENT', 30: 'ACCENT', 11: 'ACCENT', 31: 'ACCENT', 12: 'ACCENT', 32: 'ACCENT', 49: 'ACCENT', 34: 'ACCENT', 15: 'ACCENT', 50: 'ACCENT', 16: 'ACCENT', 18: 'ACCENT', 19: 'ACCENT', 1: 'TEXT'}
+
+BLUE = {
+    'TEXT': (255, 255, 255, 255),
+    'PRIMARY': (37, 37, 38, 255),
+    'SECONDARY': (51, 51, 55, 255),
+    'ACCENT': (0, 119, 200, 153)
+}
+
+ORANGE = {
+    'TEXT': (255, 255, 255, 255),
+    'PRIMARY': (37, 37, 38, 255),
+    'SECONDARY': (51, 51, 55, 255),
+    'ACCENT': (251, 133, 0, 153)
+}
+
+THEMES = {
+    'blue': BLUE,
+    'orange': ORANGE
+}
\ No newline at end of file
diff --git a/dynamic_ui.py b/dynamic_ui.py
new file mode 100644
index 0000000..3b6ab2e
--- /dev/null
+++ b/dynamic_ui.py
@@ -0,0 +1,110 @@
+import dearpygui.dearpygui as dpg
+
+from utils import *
+from constants import *
+
+def open_folder_dialog_clb(s, a, u):
+    dpg.show_item('set_folder_dialog')
+    dpg.set_item_user_data('set_folder_dialog', u)
+
+def change_tab_bar_height_clb(s, a, u):
+    change_height(a)
+
+def construct_config_ui(config_file_path, config):
+    if get_current_ui_type() == TABS_UI:
+        construct_config_ui_tabs(config_file_path, config)
+    else:
+        construct_config_ui_collapsing_headers(config_file_path, config)
+
+def construct_config_ui_collapsing_headers(config_file_path, config):
+    dpg.add_collapsing_header(label=config_file_path, parent='main', tag=config_file_path, indent=0, default_open=True)
+
+    for full_path in config:
+        namespaces = full_path.split('.')
+        parent = config_file_path
+        for level in range(len(namespaces) - 1):
+            path = config_file_path + '.' + '.'.join(namespaces[:level+1])
+
+            if not dpg.does_item_exist(path):
+                dpg.add_collapsing_header(label=namespaces[level], parent=parent, tag=path, indent=INDENT)
+
+            parent = path
+
+        create_widget(config_file_path, config, full_path)
+
+def construct_config_ui_tabs(config_file_path, config):
+    tab_bar_exists, tab_bar_uuid = does_tab_bar_child_exist('main')
+    if not tab_bar_exists:
+        tab_bar_uuid = dpg.add_tab_bar(parent='main')
+    tab_uuid = dpg.add_tab(label=config_file_path, parent=tab_bar_uuid)
+    dpg.add_child_window(parent=tab_uuid, tag=config_file_path, border=True, autosize_y=False, autosize_x=True)
+    dpg.set_item_user_data(config_file_path, 2 * SPACE)
+
+    for full_path in config:
+        namespaces = full_path.split('.')
+        parent = config_file_path
+        for level in range(len(namespaces) - 1):
+            path = config_file_path + '.' + '.'.join(namespaces[:level+1])
+
+            tab_bar_exists, tab_bar_uuid = does_tab_bar_child_exist(parent)
+
+            if not tab_bar_exists:
+                tab_bar_uuid = dpg.add_tab_bar(parent=parent, callback=change_tab_bar_height_clb)
+                dpg.set_item_user_data(parent, dpg.get_item_user_data(parent) + 4 * SPACE + FONT_SIZE)
+
+            if not dpg.does_item_exist(path):
+                tab_uuid = dpg.add_tab(label=namespaces[level], parent=tab_bar_uuid)
+                dpg.add_child_window(parent=tab_uuid, tag=path, border=True, autosize_y=False, autosize_x=True)
+                dpg.set_item_user_data(path, 2 * SPACE)
+
+            parent = path
+
+        create_widget(config_file_path, config, full_path)
+
+def create_widget(config_file_path, config, full_path):
+    tag = config_file_path + '.' + full_path
+    parent =  '.'.join([config_file_path] + full_path.split('.')[:-1])
+
+    value = config[full_path].value
+    value_type = config[full_path].value_type
+    key = config[full_path].key
+    comment = config[full_path].comment
+
+    if dpg.get_value('use_tabs_ui') and parent != config_file_path and full_path != key:
+        dpg.set_item_user_data(parent, dpg.get_item_user_data(parent) + 2 * SPACE + FONT_SIZE + SPACE)
+
+    indent = INDENT
+    if dpg.get_value('use_tabs_ui'):
+        indent = 0
+
+    with dpg.group(horizontal=True, parent=parent, indent=indent, xoffset=XOFFSET):
+        if not comment:
+            dpg.add_text(key, tag=f'{tag}.text')
+            if value_type == 'BOOLEAN':
+                dpg.add_checkbox(default_value=value, tag=tag)
+            elif value_type == 'DOUBLE':
+                dpg.add_input_double(default_value=value, tag=tag, format='%.10f', width=WIDGET_WIDTH)
+            elif value_type == 'INT':
+                dpg.add_input_int(default_value=value, tag=tag, width=WIDGET_WIDTH)
+            elif 'dir' in key.lower() or 'file' in key.lower():
+                with dpg.group(horizontal=True):
+                    dpg.add_input_text(default_value=value, tag=tag, width=WIDGET_WIDTH)
+                    dpg.add_button(label='...', callback=open_folder_dialog_clb, user_data=tag)
+            else:
+                dpg.add_input_text(default_value=value, tag=tag, width=WIDGET_WIDTH)
+        else:
+            dpg.add_text(key, tag=f'{tag}.text')
+            with dpg.group(horizontal=True, xoffset=WIDGET_WIDTH + 100):
+                if value_type == 'BOOLEAN':
+                    dpg.add_checkbox(default_value=value, tag=tag)
+                elif value_type == 'DOUBLE':
+                    dpg.add_input_double(default_value=value, tag=tag, format='%.10f', width=WIDGET_WIDTH)
+                elif value_type == 'INT':
+                    dpg.add_input_int(default_value=value, tag=tag, width=WIDGET_WIDTH)
+                elif 'dir' in key.lower() or 'file' in key.lower():
+                    with dpg.group(horizontal=True):
+                        dpg.add_input_text(default_value=value, tag=tag, width=WIDGET_WIDTH - 3 * SPACE - dpg.get_text_size('...')[0])
+                        dpg.add_button(label='...', callback=open_folder_dialog_clb, user_data=tag)
+                else:
+                    dpg.add_input_text(default_value=value, tag=tag, width=WIDGET_WIDTH)
+                dpg.add_text(comment, color=(255, 255, 255, 150))
\ No newline at end of file
diff --git a/generator.py b/generator.py
index 5866d81..df871be 100644
--- a/generator.py
+++ b/generator.py
@@ -256,8 +256,6 @@ class LCZ:
 if __name__ == '__main__':
     lcz_types = ['compact_high_rise', 'compact_mid_rise', 'compact_low_rise', 'open_high_rise', 'open_mid_rise', 'open_low_rise', 'lightweight_low_rise', 'large_low_rise', 'sparsley_build', 'heavy_industry']
 
-    #sys.argv.append('sparsley_build')
-
     fig, axs = plt.subplots(2, 2, figsize=(10, 6))
 
     lcz_type = "open_mid_rise"
@@ -281,37 +279,3 @@ if __name__ == '__main__':
     plt.savefig(f'{out_folder}/height_maps_mid_rise.png')
     plt.show()
 
-
-    if False:#len(sys.argv) <= 1:
-        fig, axs = plt.subplots(2, 5, figsize=(16, 7))
-
-        for ax, lcz_type in zip(axs.flat, lcz_types):
-            print(lcz_type)
-            lcz = LCZ(config_path=f'configs/{lcz_type}.json', output_folder='v2')
-
-            # lcz.to_model_input_building(f'building_{fn}.txt')
-            # lcz.to_model_input_street(f'street_{fn}.txt')
-
-            # lcz.to_image(f'lcz_{lcz_type}_tree.png')
-            lcz.check_params()
-
-            height_map = lcz.to_height_map()
-            im = ax.imshow(height_map, cmap='viridis', origin='lower')
-            ax.set_title(lcz_type)
-            fig.colorbar(im, ax=ax, shrink=0.3)
-
-        plt.tight_layout()
-        plt.savefig('height_maps_main_road.png')
-        plt.show()
-
-    if len(sys.argv) > 1:
-        lcz_type = sys.argv[1]
-
-        if lcz_type not in lcz_types:
-            lcz_type = lcz_types[0]
-
-        lcz = LCZ(config_path=f'configs/{lcz_type}.json', output_folder='v2')
-
-        lcz.to_image(f'lcz_{lcz_type}_tree.png')
-        lcz.check_params()
-
diff --git a/handlers.py b/handlers.py
new file mode 100644
index 0000000..f77165e
--- /dev/null
+++ b/handlers.py
@@ -0,0 +1,89 @@
+import dearpygui.dearpygui as dpg
+
+from utils import *
+from callbacks import *
+
+def mouse_down_callback():
+    if not get_height_map_path():
+        return
+    if not is_drawing():
+        set_map_window_value('prev_coords', dpg.get_mouse_pos())
+        set_map_window_value('drawing', True)
+    else:
+        x, y = dpg.get_mouse_pos()
+        px, py = get_prev_coords()
+        y += dpg.get_text_size('edit map')[1] + 2 * SPACE + dpg.get_y_scroll('map_window')
+        py += dpg.get_text_size('edit map')[1] + 2 * SPACE + dpg.get_y_scroll('map_window')
+        x += dpg.get_x_scroll('map_window')
+        px += dpg.get_x_scroll('map_window')
+        w, h = abs(x - px), abs(y - py)
+        x, y = min(x, px), min(y, py)
+
+        if w > 0 and h > 0 and check_coords(x, y, w, h):
+            dpg.set_item_pos('drawing_frame', [min(x, px), min(y, py)])
+            dpg.set_item_width('drawing_frame', w)
+            dpg.set_item_height('drawing_frame', h)
+            dpg.show_item('drawing_frame')
+
+def mouse_release_callback():
+    if not get_height_map_path():
+        return
+    coords = dpg.get_mouse_pos()
+
+    if get_current_action() == 'erase':
+        draw_rectangle(coords, 0)
+    elif get_current_action() == 'draw_rect':
+        height = dpg.get_value('height_input')
+        draw_rectangle(coords, np.max(get_height_map()) * height)
+
+    update_texture(get_height_map())
+
+    set_map_window_value('drawing', False)
+    dpg.hide_item('drawing_frame')
+
+def draw_rectangle(coords, height):
+    y1, y2, x1, x2 = get_rect(get_prev_coords(), coords)
+    height_map = get_height_map()
+    height_map[y1:y2, x1:x2] = height
+    set_map_window_value('height_map', height_map)
+
+def on_ctrl_o(sender, app_data):
+    if dpg.is_key_down(dpg.mvKey_Control) and dpg.is_key_down(dpg.mvKey_O):
+        dpg.show_item('config_open')
+
+def on_ctrl_f(sender, app_data):
+    if dpg.is_key_down(dpg.mvKey_Control) and dpg.is_key_down(dpg.mvKey_F):
+        dpg.show_item('search_popup')
+
+def on_ctrl_s(sender, app_data):
+    if dpg.is_key_down(dpg.mvKey_Control) and dpg.is_key_down(dpg.mvKey_Shift) and dpg.is_key_down(dpg.mvKey_S):
+        dpg.show_item('config_save_as')
+    elif dpg.is_key_down(dpg.mvKey_Control) and dpg.is_key_down(dpg.mvKey_S):
+        config_save_clb()
+
+def change_height_():
+    dpg.set_item_pos('status_text', (dpg.get_item_width('main') - dpg.get_text_size(dpg.get_value('status_text'))[0] - 2 * SPACE, dpg.get_item_pos('status_text')[1]))
+
+    if get_config_file_path():
+        change_height(get_config_file_path())
+
+def setup_handlers():
+    with dpg.handler_registry():
+        dpg.add_key_press_handler(key=dpg.mvKey_O, callback=on_ctrl_o)
+        dpg.add_key_press_handler(key=dpg.mvKey_F, callback=on_ctrl_f)
+        dpg.add_key_press_handler(key=dpg.mvKey_S, callback=on_ctrl_s)
+
+    with dpg.item_handler_registry(tag='main_handler'):
+        dpg.add_item_resize_handler(callback=change_height_)
+
+    dpg.bind_item_handler_registry('main', 'main_handler')
+    dpg.set_viewport_resize_callback(change_height_)
+
+    with dpg.item_handler_registry(tag='map_handler'):
+        dpg.add_item_resize_handler(callback=set_action_none_clb)
+
+    dpg.bind_item_handler_registry('map_window', 'map_handler')
+
+    with dpg.handler_registry():
+        dpg.add_mouse_down_handler(callback=mouse_down_callback)
+        dpg.add_mouse_release_handler(callback=mouse_release_callback)
\ No newline at end of file
diff --git a/remote_run.py b/remote_run.py
new file mode 100644
index 0000000..721b75e
--- /dev/null
+++ b/remote_run.py
@@ -0,0 +1,93 @@
+import threading
+import os
+import re
+import time
+
+from scp import SCPClient
+import dearpygui.dearpygui as dpg
+import paramiko
+
+from utils import show_status_text
+
+
+HOST = "geophyslab.srcc.msu.ru"
+REPOS = (
+    "git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-common.git",
+    "git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-wstgrid.git",
+    "git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nse-gabls1-urban-les.git"
+)
+
+def run_on_remote_server(username, password, gitlab_username, gitlab_password):
+    try:
+        ssh = paramiko.SSHClient()
+        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+        ssh.connect(HOST, username=username, password=password)
+        
+        scp = SCPClient(ssh.get_transport())
+        
+        scp.put("tmp.txt", "./tmp.txt")
+
+        commands = [
+            "mkdir run",
+            "cd ./run"
+        ]
+
+        # for repo in REPOS:
+        #     commands.append(repo.format(username=gitlab_username, password=gitlab_password))
+        
+        commands += [
+            "cd ./nse-gabls1-urban-les/nse-gabls1-urban-les/",
+            #"chmod +x cpall.sh",
+            #"./cpall.sh ../../code",
+            "cd ../../code",
+            #"make -B MACHINE=local COMPILER=gnu",
+            "cp ../../tmp.txt ./config.txt",
+            "./nsenx"
+        ]
+
+        commands = '; '.join(commands)
+
+        log = ''
+
+        stdin, stdout, stderr = ssh.exec_command(commands)
+
+        while not stdout.channel.exit_status_ready():
+            if stdout.channel.recv_ready():
+                output = stdout.channel.recv(512).decode()
+                update_progress(output)
+                log += '\n\n' + output
+            time.sleep(1)
+
+        output = stdout.read().decode()
+        dpg.set_value("progress_bar", 1.)
+        dpg.hide_item('loading')
+
+        log += '\n\n' + output
+        
+        with open("log.txt", "w") as res_file:
+            res_file.write(output)
+        
+        scp.close()
+        ssh.close()
+        
+        show_status_text("success:) log written to log.txt.")
+    
+    except Exception as e:
+        show_status_text(f"error: {str(e)}")
+
+def update_progress(output):
+    if not re.findall(r'([0-9]+)%', output):
+        progress = 0.0
+        show_status_text(f"building model")
+        dpg.show_item('loading')
+        x, y = dpg.get_item_pos('status_text')
+        dpg.set_item_pos('loading', (x - 40, y))
+    else:
+        progress = int(re.findall(r'([0-9]+)%', output)[-1]) / 100
+        show_status_text('')
+        dpg.hide_item('loading')
+    dpg.set_value("progress_bar", progress)
+
+def start_remote_execution(server_username, server_password, gitlab_username, gitlab_password):
+    dpg.configure_item("login_modal", show=False)
+    threading.Thread(target=run_on_remote_server, args=(server_username, server_password, gitlab_username, gitlab_password)).start()
diff --git a/static_ui.py b/static_ui.py
new file mode 100644
index 0000000..051a130
--- /dev/null
+++ b/static_ui.py
@@ -0,0 +1,132 @@
+import dearpygui.dearpygui as dpg
+
+from utils import *
+from constants import *
+from styles import setup_fonts, setup_themes, apply_theme, get_accent_color
+from handlers import setup_handlers
+from callbacks import *
+
+
+def construct_main_window_ui():
+    with dpg.window(label='config file editor', tag='main', user_data={'config': None, 'config_file_path': None}):
+        with dpg.menu_bar():
+            with dpg.menu(label='config'):
+                dpg.add_menu_item(label='open\t\t', shortcut='ctrl+o', callback=open_config_open_dialog_clb, tag='open_config_open_dialog')
+                dpg.add_menu_item(label='search\t\t', shortcut='ctrl+f', callback=show_config_search_window_clb, tag='show_config_search_window')
+                dpg.add_menu_item(label='save\t\t', shortcut='ctrl+s', callback=config_save_clb, tag='save_config')
+                dpg.add_menu_item(label='save as\t\t', shortcut='ctrl+shift+s', callback=open_config_save_as_dialog_clb, tag='open_config_save_as_dialog')
+
+            with dpg.menu(label='map'):
+                dpg.add_menu_item(label='open map', callback=open_map_open_dialog_clb, tag='open_map_open_dialog')
+                with dpg.menu(label='generate map'):
+                    for lcz_type in LCZS:
+                        dpg.add_menu_item(label=lcz_type, check=False, callback=combo_menu(generate_map_clb), tag=lcz_type)
+
+            with dpg.menu(label='run'):
+                dpg.add_menu_item(label='run on lab server', callback=remote_run_clb, tag='remote_run')
+
+            with dpg.menu(label='view'):
+                with dpg.menu(label='ui', tag='ui'):
+                    for ui in UIS:
+                        dpg.add_menu_item(label=ui, check=True, default_value=False, callback=combo_menu(construct_config_ui_clb), tag=ui)
+                    dpg.set_value(UIS[1], True)
+
+                with dpg.menu(label='theme', tag='theme'):
+                    for theme in THEMES:
+                        dpg.add_menu_item(label=theme, check=True, default_value=False, callback=combo_menu(apply_theme_clb), tag=theme)
+                    dpg.set_value('orange', True)
+    
+            dpg.add_progress_bar(label='progress', default_value=0.0, tag='progress_bar', height=FONT_SIZE, width=200, pos=[dpg.get_viewport_width() // 2 - 100 + SPACE, SPACE])
+            dpg.add_text('', tag='status_text', show=True)
+            dpg.add_loading_indicator(style=1, color=(255, 255, 255, 255), thickness=2, radius=2, tag='loading', show=False)
+
+def construct_crendentials_window():
+    with dpg.window(label='credentials', show=False, tag='login_modal', width=550, height=350):
+        dpg.add_input_text(label='server username', tag='server_username', password=False, default_value=os.getenv('SERVER_LOGIN', ''), width=300)
+        dpg.add_input_text(label='server password', tag='server_password', password=True, default_value=os.getenv('SERVER_PASS', ''), width=300)
+        dpg.add_input_text(label='gitlab username', tag='gitlab_username', password=False, default_value=os.getenv('GITLAB_LOGIN', ''), width=300)
+        dpg.add_input_text(label='gitlab password', tag='gitlab_password', password=True, default_value=os.getenv('GITLAB_PASS', ''), width=300)
+        dpg.add_button(label='submit', callback=start_remote_execution_clb)
+
+def construct_config_open_dialog():
+    with dpg.file_dialog(directory_selector=False, show=False, callback=config_open_clb, tag='config_open', width=600, height=400):
+        dpg.add_file_extension('.txt', color=(0, 255, 0, 255))
+        dpg.add_file_extension('.*', color=(255, 255, 255, 255))
+
+def construct_config_save_as_dialog():
+    with dpg.file_dialog(directory_selector=False, show=False, callback=config_save_as_clb, tag='config_save_as', width=600, height=400):
+        dpg.add_file_extension('.txt', color=(0, 255, 0, 255))
+        dpg.add_file_extension('.*', color=(255, 255, 255, 255))
+
+def construct_map_open_dialog():
+    with dpg.file_dialog(directory_selector=False, show=False, callback=map_open_clb, tag='map_open', width=600, height=400):
+        dpg.add_file_extension('.txt', color=(0, 255, 0, 255))
+        dpg.add_file_extension('.*', color=(255, 255, 255, 255))
+
+def construct_map_save_as_dialog():
+    with dpg.file_dialog(directory_selector=False, show=False, callback=map_save_as_clb, tag='map_save_as', width=600, height=400):
+        dpg.add_file_extension('.txt', color=(0, 255, 0, 255))
+        dpg.add_file_extension('.*', color=(255, 255, 255, 255))
+
+def construct_set_file_path_dialog():
+    with dpg.file_dialog(directory_selector=True, show=False, callback=set_file_path_clb, tag='set_folder_dialog', width=500, height=400):
+        dpg.add_file_extension('.*', color=(255, 255, 255, 255))
+
+def construct_message_window():
+    with dpg.window(label='', no_collapse=True, autosize=True, no_resize=True, modal=True, tag='message_popup', show=False, pos=(dpg.get_viewport_width() / 3, dpg.get_viewport_height() / 3)):
+        dpg.add_text('file saved successfully:)')
+
+def construct_search_window():
+    with dpg.window(label='', no_collapse=True, autosize=True, no_resize=True, tag='search_popup', show=False, pos=(40, 40)):
+        dpg.add_input_text(hint='search for...', on_enter=True, callback=search_clb, tag='search')
+
+def construct_map_edit_window():
+    with dpg.texture_registry():
+        dpg.add_dynamic_texture(800, 800, np.zeros((800, 800, 4), dtype=np.uint8), tag='heatmap_texture')
+
+    with dpg.theme() as item_theme:
+        with dpg.theme_component(dpg.mvAll):
+            dpg.add_theme_color(dpg.mvThemeCol_ChildBg, (0, 0, 0, 0), category=dpg.mvThemeCat_Core)
+            dpg.add_theme_style(dpg.mvStyleVar_ChildRounding, 0, category=dpg.mvThemeCat_Core)
+
+    dpg.add_colormap_registry(label='colormap registry', tag='colormap_registry')
+    dpg.add_colormap(list(map(apply_tint, [[0, 0, 0], [100, 100, 100], [255, 255, 255]])), False, tag='colormap', parent='colormap_registry')
+
+    with dpg.window(label='edit map', tag='map_window', user_data={'height_map': None, 'height_map_file_path': None, 'drawing': False, 'prev_coords': None, 'current_action': None}, horizontal_scrollbar=True, show=False, width=600, height=400, pos=[20, 60], on_close=map_window_on_close_clb):
+        with dpg.menu_bar():
+            with dpg.menu(label='file'):
+                dpg.add_menu_item(label='save', callback=map_save_clb, tag='map_save')
+                dpg.add_menu_item(label='save as', callback=open_map_save_as_dialog_clb, tag='open_map_save_as_dialog')
+
+        with dpg.group(horizontal=True):
+            with dpg.group(width=200, tag='tools'):
+                dpg.add_button(label='erase', callback=set_action_clb, tag='erase')
+                dpg.add_button(label='draw rectangle', callback=set_action_clb, tag='draw_rect')
+                dpg.add_colormap_slider(tag='height_input')
+                dpg.bind_colormap(dpg.last_item(), 'colormap')
+            dpg.add_image('heatmap_texture', tint_color=(0, 119, 200, 255), tag='map')
+
+        dpg.add_child_window(tag='drawing_frame', show=False, width=0, height=0)
+
+    dpg.bind_item_theme('drawing_frame', item_theme)
+
+
+def construct_ui():
+    construct_main_window_ui()
+
+    construct_message_window()
+
+    construct_map_open_dialog()
+    construct_map_save_as_dialog()
+    construct_map_edit_window()
+    construct_crendentials_window()
+
+    construct_config_save_as_dialog()
+    construct_config_open_dialog()
+    construct_search_window()
+
+    construct_set_file_path_dialog()
+    
+    setup_handlers()
+    setup_fonts()
+    setup_themes()
\ No newline at end of file
diff --git a/styles.py b/styles.py
index fcb7861..d8a8bbd 100644
--- a/styles.py
+++ b/styles.py
@@ -1,37 +1,11 @@
+import os
+
 from enum import Enum
 
 import dearpygui.dearpygui as dpg
 
-FONT_PATH = "fonts\\Montserrat-Medium.ttf"
-ICON_PATH = "icons\\icon.ico"
-FONT_SIZE = 24
-ACCENT_COLOR = (0, 119, 200, 100)
-ROUNDING = 10
-SPACE = 10
-INDENT = 40
-BORDER = 3
-
-TEXT = (0, 1)
-ACCENT = (5, 18, 19, 20, 15, 16, 35, 37, 34, 23, 22, 25, 26, 27, 28, 29, 10, 11, 12, 30, 31, 32, 49, 50)
-PRIMARY = (2, 3, 39)
-SECONDARY = (36, 33, 21, 24, 4, 7, 8, 9, 13, 14)
-APPLY_ALPHA = (1, 22, 25, 31, 34, 16)
-COLOR_TYPE_TO_DPG_ITEM = {'TEXT': (0, 1), 'PRIMARY': (2, 3, 39), 'SECONDARY': (36, 33, 21, 24, 4, 7, 8, 9, 13, 14), 'ACCENT': (5, 18, 19, 20, 15, 16, 35, 37, 34, 23, 22, 25, 26, 27, 28, 29, 10, 11, 12, 30, 31, 32, 49, 50)}
-COLOR_TO_COLOR_TYPE = {0: 'TEXT', 1: 'TEXT', 2: 'PRIMARY', 3: 'PRIMARY', 39: 'PRIMARY', 36: 'SECONDARY', 21: 'SECONDARY', 24: 'SECONDARY', 4: 'SECONDARY', 7: 'SECONDARY', 8: 'SECONDARY', 9: 'SECONDARY', 13: 'SECONDARY', 33: 'SECONDARY', 14: 'SECONDARY', 35: 'ACCENT', 20: 'ACCENT', 37: 'ACCENT', 22: 'ACCENT', 23: 'ACCENT', 5: 'ACCENT', 25: 'ACCENT', 26: 'ACCENT', 27: 'ACCENT', 28: 'ACCENT', 29: 'ACCENT', 10: 'ACCENT', 30: 'ACCENT', 11: 'ACCENT', 31: 'ACCENT', 12: 'ACCENT', 32: 'ACCENT', 49: 'ACCENT', 34: 'ACCENT', 15: 'ACCENT', 50: 'ACCENT', 16: 'ACCENT', 18: 'ACCENT', 19: 'ACCENT', 1: 'TEXT'}
-
-BLUE = {
-    'TEXT': (255, 255, 255, 255),
-    'PRIMARY': (37, 37, 38, 255),
-    'SECONDARY': (51, 51, 55, 255),
-    'ACCENT': (0, 119, 200, 153)
-}
-
-ORANGE = {
-    'TEXT': (255, 255, 255, 255),
-    'PRIMARY': (37, 37, 38, 255),
-    'SECONDARY': (51, 51, 55, 255),
-    'ACCENT': (251, 133, 0, 153)
-}
+from utils import *
+from constants import *
 
 def apply_alpha(color, divisor=2):
     r, g, b, a = color
@@ -76,31 +50,19 @@ def add_styles_to_theme(theme):
         dpg.add_theme_style(dpg.mvStyleVar_FrameBorderSize, 0, category=dpg.mvThemeCat_Core)
 
 
-class GuiStyle:
-    def __init__(self):
-        self.font_registry = dpg.add_font_registry()
-        self.montserrat_font = dpg.add_font(FONT_PATH, FONT_SIZE, parent=self.font_registry)
-
-        self.fonts = {
-            'montserrat': self.montserrat_font
-        }
-
-        self.themes = {
-            'blue': BLUE,
-            'orange': ORANGE
-        }
-
-        self.dpg_themes = dict()
+def setup_fonts():
+    font_registry = dpg.add_font_registry()
+    montserrat_font = dpg.add_font(FONT_PATH, FONT_SIZE, parent=font_registry)
+    dpg.bind_font(montserrat_font)
 
-        for theme_name in self.themes:
-            theme = dpg.add_theme()
-            add_styles_to_theme(theme)
-            add_colors_to_theme(self.themes[theme_name], theme)
-            self.dpg_themes[theme_name] = theme
+def apply_theme(theme_name):
+    theme = dpg.add_theme()
+    add_styles_to_theme(theme)
+    add_colors_to_theme(THEMES[theme_name], theme)
+    dpg.bind_theme(theme)
 
-    def apply_font(self, font):
-        dpg.bind_font(self.fonts[font])
+def setup_themes():
+    apply_theme(get_current_theme())
 
-    def apply_theme(self, theme):
-        print('appling', theme)
-        dpg.bind_theme(self.dpg_themes[theme])
\ No newline at end of file
+def get_accent_color():
+    return THEMES[get_current_theme()]['ACCENT']
\ No newline at end of file
diff --git a/utils.py b/utils.py
index 78dc762..bb18a0a 100644
--- a/utils.py
+++ b/utils.py
@@ -1,14 +1,116 @@
 import dearpygui.dearpygui as dpg
+
 import numpy as np
+import threading
 
-from styles import SPACE
+from constants import *
 
-def dump_map(height_map, file_path):
-    with open(file_path, "w", encoding="utf-8") as file:
-        w, h = height_map.shape
-        file.write(f"{w} {h}\n")
-        for row in height_map:
-            file.write(" ".join(list(map(str, row))) + "\n")
+def get_current_ui_type():
+    for child in dpg.get_item_children('ui', 1):
+        if dpg.get_value(child):
+            return dpg.get_item_alias(child)
+
+def get_current_theme():
+    for child in dpg.get_item_children('theme', 1):
+        if dpg.get_value(child):
+            return dpg.get_item_alias(child)
+
+def get_config_file_path():
+    return dpg.get_item_user_data('main')['config_file_path']
+
+def get_config():
+    return dpg.get_item_user_data('main')['config']
+
+def set_main_window_value(key, value):
+    data = dpg.get_item_user_data('main')
+    data[key] = value
+    dpg.set_item_user_data('main', data)
+
+def get_height_map_path():
+    return dpg.get_item_user_data('map_window')['height_map_file_path']
+
+def get_height_map():
+    return dpg.get_item_user_data('map_window')['height_map']
+
+def is_drawing():
+    return dpg.get_item_user_data('map_window')['drawing']
+
+def get_prev_coords():
+    return dpg.get_item_user_data('map_window')['prev_coords']
+
+def get_current_action():
+    return dpg.get_item_user_data('map_window')['current_action']
+
+def set_map_window_value(key, value):
+    data = dpg.get_item_user_data('map_window')
+    data[key] = value
+    dpg.set_item_user_data('map_window', data)
+
+def show_status_text(message):
+    dpg.set_item_pos('status_text', (dpg.get_item_width('main') - dpg.get_text_size(message)[0] - 2 * SPACE, dpg.get_item_pos('status_text')[1]))
+    dpg.set_value('status_text', message)
+
+def path_iter(path, prefix=""):
+    namespaces = path.split('.')
+    for i in range(len(namespaces)):
+        yield prefix + '.' + '.'.join(namespaces[:i+1])
+
+def delete_item_and_clear_alias(item):
+    alias = dpg.get_item_alias(item)
+    if alias:
+        dpg.remove_alias(alias)
+    if dpg.does_item_exist(item):
+        dpg.delete_item(item)
+
+def delete_item_children_and_clear_aliases_(item):
+    if dpg.does_item_exist(item):
+        for child in dpg.get_item_children(item, 1):
+            delete_item_children_and_clear_aliases_(child)
+            delete_item_and_clear_alias(child)
+
+def delete_item_children_and_clear_aliases():
+    if len(dpg.get_item_children('main', 1)) > 1:
+        delete_item_children_and_clear_aliases_(dpg.get_item_children('main', 1)[1])
+        delete_item_and_clear_alias(dpg.get_item_children('main', 1)[1])
+
+####################################################################################
+#
+#    search utils
+#
+####################################################################################
+
+def change_color_temporarily(text_tag, color, duration=1):
+    color = list(color)[:-1] + [255]
+    dpg.configure_item(text_tag, color=color)
+    threading.Timer(duration, lambda: revert_color(text_tag)).start()
+
+def revert_color(text_tag):
+    dpg.configure_item(text_tag, color=(255, 255, 255, 255))
+
+####################################################################################
+#
+#    edit map utils
+#
+####################################################################################
+
+def update_texture(height_map):
+    max_height = np.max(height_map)
+    normalized_map = height_map / max_height
+
+    texture_data = normalized_map.flatten()
+    texture_data = np.tile(texture_data, (4, 1)).T.flatten()
+
+    texture_data.resize(800 * 800 * 4)
+
+    width, height = height_map.shape
+
+    dpg.set_value('heatmap_texture', texture_data)
+
+def check_coords(x, y, w, h):
+    mx, my = dpg.get_item_pos('map')
+    mw, mh = dpg.get_item_width('map'), dpg.get_item_height('map')
+
+    return mx <= x <= x + w <= mx + mw and my <= y <= y + h <= my + mh
 
 def apply_tint(color, tint=(0, 119, 200)):
     tint = np.array(tint) / 255
@@ -38,6 +140,42 @@ def load_height_map(file_path):
         height_map = [list(map(int, line.strip().split())) for line in lines]
     return np.array(height_map)
 
+def dump_map(height_map, file_path):
+    with open(file_path, "w", encoding="utf-8") as file:
+        w, h = height_map.shape
+        file.write(f"{w} {h}\n")
+        for row in height_map:
+            file.write(" ".join(list(map(str, row))) + "\n")
+
+####################################################################################
+#
+#    main menu utils
+#
+####################################################################################
+
+def combo_menu(callback):
+    def func(s, a, u):
+        for child in dpg.get_item_children(dpg.get_item_parent(s), 1):
+            if dpg.get_item_alias(child) != dpg.get_item_alias(s):
+                dpg.set_value(child, False)
+            else:
+                dpg.set_value(child, True)
+        dpg.set_item_user_data(s, dpg.get_item_alias(s))
+        callback(s, a, u)
+    return func
+
+####################################################################################
+#
+#    tabs ui utils
+#
+####################################################################################
+
+def does_tab_bar_child_exist(item):
+    for uuid in dpg.get_item_children(item, 1):
+        if dpg.get_item_type(uuid) == "mvAppItemType::mvTabBar":
+            return True, uuid
+    return False, None
+
 def get_absolute_y(item):
     y = dpg.get_item_pos(item)[1]
     parent = dpg.get_item_parent(item)
@@ -56,24 +194,14 @@ def get_item_level(item):
         parent = dpg.get_item_parent(parent)
     return level
 
-def does_tab_bar_child_exist(item):
-    for uuid in dpg.get_item_children(item, 1):
-        if dpg.get_item_type(uuid) == "mvAppItemType::mvTabBar":
-            return True, uuid
-    return False, None
+def change_height(item):    
+    if not dpg.does_item_exist(item):
+        return
+    if dpg.get_item_type(item) == 'mvAppItemType::mvChildWindow':
+        uuid = dpg.get_item_parent(item)
+        new_height = dpg.get_viewport_client_height() - get_absolute_y(uuid) - SPACE * get_item_level(item)
 
-def path_iter(path, prefix=""):
-    namespaces = path.split('.')
-    for i in range(len(namespaces)):
-        yield prefix + '.' + '.'.join(namespaces[:i+1])
+        dpg.set_item_height(item, max(new_height, dpg.get_item_user_data(item)))
 
-def combo_menu(callback):
-    def func(s, a, u):
-        for child in dpg.get_item_children(dpg.get_item_parent(s), 1):
-            if dpg.get_item_alias(child) != dpg.get_item_alias(s):
-                dpg.set_value(child, False)
-            else:
-                dpg.set_value(child, True)
-        dpg.set_item_user_data(s, dpg.get_item_alias(s))
-        callback(s, a, u)
-    return func
\ No newline at end of file
+    for child in dpg.get_item_children(item, 1):
+        change_height(child)
\ No newline at end of file
-- 
GitLab