interface.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. # TODO Lot of hardcoded shizzle
  2. import enum
  3. import logging
  4. import sys
  5. from itertools import groupby
  6. from typing import Optional, List, Union
  7. from urllib.error import URLError
  8. import arklog
  9. import dearpygui.dearpygui as dpg
  10. import dearpygui.demo as demo
  11. from graph_exploring_tool import query
  12. from graph_exploring_tool.configuration import Configuration
  13. from graph_exploring_tool.query import QueryTemplate
  14. arklog.set_config_logging()
  15. class StatusMessageType(enum.Enum):
  16. DEFAULT = 0
  17. WARNING = 1
  18. ERROR = 2
  19. STATUS_CONSOLE_TAG = "__status_console"
  20. QUERY_FILTER_TAG = "__query_filter"
  21. ENDPOINT_TEXTINPUT_TAG = "__endpoint_input"
  22. POST_METHOD_CHECKBOX_TAG = "__use_post_method"
  23. QUERY_RESULT_WINDOW_TAG = "__query_results_window"
  24. QUERY_RESULT_TAB_TAG = "__query_result_tab"
  25. RESULT_TABLE_TAG = "__result_table"
  26. PRIMARY_WINDOW_TAG = "__primary"
  27. QUERY_EDITOR_VISUAL_TAG = "__query_editor_visual"
  28. QUERY_EDITOR_TEXTUAL_TAG = "__query_editor_textual"
  29. EDITOR_SELECTOR_TAG = "__editor_selector"
  30. RADIO_BUTTON_TYPE = "mvAppItemType::mvRadioButton"
  31. def set_status_text(message: str, message_type: StatusMessageType = StatusMessageType.DEFAULT):
  32. """Set the text displayed on the status bar."""
  33. dpg.set_value(STATUS_CONSOLE_TAG, message)
  34. with dpg.theme() as default_theme:
  35. with dpg.theme_component(dpg.mvAll):
  36. dpg.add_theme_color(dpg.mvThemeCol_Text, (255, 255, 255), category=dpg.mvThemeCat_Core)
  37. with dpg.theme() as warning_theme:
  38. with dpg.theme_component(dpg.mvAll):
  39. dpg.add_theme_color(dpg.mvThemeCol_Text, (50, 200, 50), category=dpg.mvThemeCat_Core)
  40. with dpg.theme() as error_theme:
  41. with dpg.theme_component(dpg.mvAll):
  42. dpg.add_theme_color(dpg.mvThemeCol_Text, (200, 50, 50), category=dpg.mvThemeCat_Core)
  43. if message_type == StatusMessageType.DEFAULT:
  44. dpg.bind_item_theme(STATUS_CONSOLE_TAG, default_theme)
  45. elif message_type == StatusMessageType.WARNING:
  46. dpg.bind_item_theme(STATUS_CONSOLE_TAG, warning_theme)
  47. elif message_type == StatusMessageType.ERROR:
  48. dpg.bind_item_theme(STATUS_CONSOLE_TAG, error_theme)
  49. def _config(sender, keyword, user_data):
  50. widget_type = dpg.get_item_type(sender)
  51. items = user_data
  52. if widget_type == RADIO_BUTTON_TYPE:
  53. value = True
  54. else:
  55. keyword = dpg.get_item_label(sender)
  56. value = dpg.get_value(sender)
  57. if isinstance(user_data, list):
  58. for item in items:
  59. dpg.configure_item(item, **{keyword: value})
  60. else:
  61. dpg.configure_item(items, **{keyword: value})
  62. def __save():
  63. """Save current query to a file."""
  64. logging.debug("Saving.")
  65. def add_main_menu():
  66. with dpg.menu_bar():
  67. with dpg.menu(label="Menu"):
  68. # dpg.add_menu_item(label="New")
  69. # dpg.add_menu_item(label="Open")
  70. # with dpg.menu(label="Open Recent"):
  71. # dpg.add_menu_item(label="patty.h")
  72. # dpg.add_menu_item(label="nick.py")
  73. # dpg.add_menu_item(label="Save")
  74. dpg.add_menu_item(label="Exit", callback=lambda _: sys.exit())
  75. # TODO Add tab for prefixes
  76. def create_query_palette(query_palette: List[QueryTemplate]):
  77. with dpg.child_window(label="Query Palette", width=220, menubar=True):
  78. with dpg.menu_bar():
  79. dpg.add_menu(label="Query Palette", enabled=False)
  80. dpg.add_input_text(label="Filter", callback=lambda s, a: dpg.set_value(QUERY_FILTER_TAG, a))
  81. with dpg.filter_set(tag=QUERY_FILTER_TAG):
  82. # TODO Populate from onto/base
  83. # TODO Bug fix this; actually figure out groups
  84. # TODO Fix filtering
  85. grouped_query_palette = groupby(sorted(query_palette, key=lambda palette_query: palette_query.group), key=lambda palette_query: palette_query.group)
  86. for query_template_group, query_template_queries in grouped_query_palette:
  87. with dpg.tree_node(label=query_template_group, tag=query_template_group, default_open=True, filter_key=""):
  88. full_filter = ""
  89. for query_template in query_template_queries:
  90. query_button = dpg.add_button(label=query_template.name, filter_key=query_template.name, callback=_query_palette_click, user_data=query_template)
  91. with dpg.tooltip(dpg.last_item()):
  92. dpg.add_text(query_template.description)
  93. full_filter =f"{full_filter},{query_template.name}"
  94. # TODO Make theme-ing more generic
  95. with dpg.theme() as item_theme:
  96. with dpg.theme_component(dpg.mvAll):
  97. dpg.add_theme_color(dpg.mvThemeCol_Text, (200, 200, 100), category=dpg.mvThemeCat_Core)
  98. dpg.add_theme_style(dpg.mvStyleVar_FrameRounding, 0, category=dpg.mvThemeCat_Core)
  99. with dpg.theme() as modifies_theme:
  100. with dpg.theme_component(dpg.mvAll):
  101. dpg.add_theme_color(dpg.mvThemeCol_Text, (200, 50, 50), category=dpg.mvThemeCat_Core)
  102. if query_template.visual_support:
  103. dpg.bind_item_theme(query_button, item_theme)
  104. if query_template.modifies:
  105. dpg.bind_item_theme(query_button, modifies_theme)
  106. dpg.configure_item(query_template_group, filter_key=full_filter)
  107. def create_query_options(endpoint: str):
  108. with dpg.group(horizontal=True):
  109. dpg.add_combo(("Visual", "Textual"), default_value="Visual", callback=_mode_select, width=100, tag=EDITOR_SELECTOR_TAG)
  110. dpg.add_input_text(default_value=endpoint, width=300, enabled=False, tag=ENDPOINT_TEXTINPUT_TAG)
  111. dpg.add_checkbox(label="debug", callback=_config)
  112. dpg.add_checkbox(label="annotate", default_value=False, callback=_config) # TODO Actually make this annotate the result data with its type
  113. # TODO Make the elements color coded etc...
  114. # TODO Fix context menu and keyboard operations
  115. def create_query_editor_visual(example_prefix:str, example_query:str, show: bool = False):
  116. with dpg.child_window(autosize_x=True, height=500, menubar=True, show=show, tag=QUERY_EDITOR_VISUAL_TAG):
  117. with dpg.menu_bar():
  118. dpg.add_menu(label="Visual Query Editor", enabled=False)
  119. with dpg.tab_bar():
  120. with dpg.tab(label="Query", tag="__query_tab"):
  121. dpg.add_input_text(default_value=example_query, multiline=True, on_enter=True, height=300, width=-1, tag="__query_editor_visual_input")
  122. with dpg.tab(label="Prefix", tag="__prefix_tab"):
  123. dpg.add_input_text(default_value=example_prefix, multiline=True, on_enter=True, height=300, width=-1, tag="__prefix_editor_visual_input")
  124. with dpg.tab(label="Advanced", tag="__advanced_tab"):
  125. dpg.add_checkbox(label="Use post method", default_value=False, tag=POST_METHOD_CHECKBOX_TAG)
  126. with dpg.group(tag="__visual_editor_fields"):
  127. # TODO Make more clear which placeholder is getting replaced (Maybe do it interactively, replace as user changes)
  128. pass
  129. with dpg.group(horizontal=True):
  130. dpg.add_button(label="Query", callback=_perform_query)
  131. dpg.add_button(label="Save", callback=_select_directory) # TODO
  132. dpg.add_button(label="Load") # TODO
  133. # TODO Maybe remove/change this option, we made all the queries mostly 'visual'
  134. # It's practically a duplicate at this point
  135. def create_query_editor_textual(example_prefix:str, example_query:str, show: bool = False):
  136. with dpg.child_window(autosize_x=True, height=500, menubar=True, show=show, tag=QUERY_EDITOR_TEXTUAL_TAG):
  137. with dpg.menu_bar():
  138. dpg.add_menu(label="Textual Query Editor", enabled=False)
  139. with dpg.tab_bar():
  140. with dpg.tab(label="Query", tag="__query_textual_tab"):
  141. dpg.add_input_text(default_value=example_query, multiline=True, on_enter=True, height=300, width=-1, tag="__query_editor_textual_input")
  142. with dpg.tab(label="Prefix", tag="__prefix_textual_tab"):
  143. dpg.add_input_text(default_value=example_prefix, multiline=True, on_enter=True, height=300, width=-1, tag="__prefix_editor_textual_input")
  144. with dpg.group(horizontal=True):
  145. dpg.add_button(label="Query", callback=_perform_query)
  146. dpg.add_button(label="Save", callback=_select_directory) # TODO
  147. dpg.add_button(label="Load") # TODO
  148. # TODO Not at the bottom because fok getting that layout to fit correctly
  149. def create_status_console():
  150. with dpg.child_window(autosize_x=True, height=35):
  151. with dpg.group(horizontal=True):
  152. dpg.add_text(default_value="Ready.", tag=STATUS_CONSOLE_TAG)
  153. def set_copy(element: Union[int, str], text_data:str):
  154. dpg.set_value(element, shorten(text_data))
  155. i = dpg.get_item_label(element)
  156. dpg.set_value(f"__copy_{i}", shorten(text_data))
  157. dpg.configure_item(f"__copy_drag_payload_{i}", drag_data=shorten(text_data))
  158. dpg.set_value(f"__copy_drag_{i}", shorten(text_data))
  159. def create_query_results():
  160. with dpg.child_window(autosize_x=True, autosize_y=True, menubar=True, tag=QUERY_RESULT_WINDOW_TAG):
  161. with dpg.menu_bar():
  162. dpg.add_menu(label="Results", enabled=False)
  163. # dpg.add_input_text(label="Filter", tag="__result_filter_input", callback=lambda s, a: dpg.set_value("__result_filter", a))
  164. with dpg.tab_bar():
  165. with dpg.tab(label="Query Result", tag=QUERY_RESULT_TAB_TAG):
  166. with dpg.table(header_row=True, policy=dpg.mvTable_SizingFixedFit, row_background=True, reorderable=True,
  167. resizable=True, no_host_extendX=False, hideable=True,
  168. borders_innerV=True, delay_search=True, borders_outerV=True, borders_innerH=True,
  169. borders_outerH=True, tag=RESULT_TABLE_TAG):
  170. pass
  171. with dpg.tab(label="Saved"):
  172. with dpg.group(horizontal=True):
  173. with dpg.group():
  174. for i in range(0, 10):
  175. dpg.add_input_text(label=f"{i}", payload_type="string", width=500, drop_callback=lambda s, a: set_copy(s,a))
  176. with dpg.group():
  177. for i in range(0, 10):
  178. dpg.add_text(tag=f"__copy_{i}", default_value="Empty")
  179. with dpg.drag_payload(parent=dpg.last_item(), drag_data="Empty", payload_type="string", tag=f"__copy_drag_payload_{i}"):
  180. dpg.add_text(tag=f"__copy_drag_{i}", default_value="Empty")
  181. with dpg.tab(label="Debug"):
  182. dpg.add_text("This is the debug tab!")
  183. def _mode_select(sender: int, mode: str, user_data: Optional[dict]):
  184. mode = mode.lower().strip()
  185. visual = mode == "visual"
  186. # TODO When showing visual we need to fix the custom fields
  187. dpg.configure_item(QUERY_EDITOR_VISUAL_TAG, show=visual)
  188. dpg.configure_item(QUERY_EDITOR_TEXTUAL_TAG, show=not visual)
  189. def shorten(identifier: str) -> str:
  190. for key, value in query.reverse_prefix.items():
  191. identifier = identifier.replace(key, f"{value}:")
  192. return identifier
  193. def _query_palette_click(sender: int, mode: str, user_data: Optional[QueryTemplate]):
  194. mode = dpg.get_value(EDITOR_SELECTOR_TAG).lower().strip()
  195. if mode == "visual" and not user_data.visual_support:
  196. logging.warning(f"Visual mode for template '{user_data.name}' not implemented yet!")
  197. set_status_text(f"Visual mode for template '{user_data.name}' not implemented yet!", StatusMessageType.WARNING)
  198. return
  199. set_status_text(f"Using {mode} template '{user_data.name}'.")
  200. dpg.set_value(f"__prefix_editor_{mode}_input", user_data.prefix)
  201. dpg.set_value(f"__query_editor_{mode}_input", user_data.query)
  202. dpg.set_value(POST_METHOD_CHECKBOX_TAG, user_data.modifies)
  203. dpg.delete_item("__visual_editor_fields", children_only=True)
  204. if mode == "visual" and user_data.visual_support:
  205. for replacement in user_data.replacements:
  206. dpg.add_input_text(label=f"{replacement.description}",default_value=replacement.suggestion, payload_type="string", width=500, parent="__visual_editor_fields", drop_callback=lambda s, a: dpg.set_value(s, shorten(a)), tag=f"__{replacement.suggestion}", user_data=replacement)
  207. def _perform_query(sender: int, mode: str, user_data: Optional[dict]):
  208. # TODO Fix for new query structure
  209. mode = dpg.get_value(EDITOR_SELECTOR_TAG).lower().strip()
  210. prefix_text = dpg.get_value(f"__prefix_editor_{mode}_input")
  211. query_text = dpg.get_value(f"__query_editor_{mode}_input")
  212. endpoint = dpg.get_value(ENDPOINT_TEXTINPUT_TAG)
  213. use_post_method = dpg.get_value(POST_METHOD_CHECKBOX_TAG)
  214. try:
  215. if mode=="visual":
  216. for replacement_field in dpg.get_item_children("__visual_editor_fields", 1):
  217. value = dpg.get_value(replacement_field)
  218. placeholder = dpg.get_item_user_data(replacement_field).placeholder
  219. query_text = query_text.replace(f"{{{{ {placeholder} }}}}", value)
  220. query_result = query.perform_query(endpoint, prefix_text + "\n" + query_text, use_post_method)
  221. except URLError as e:
  222. logging.error(f"Connection to '{endpoint}' failed.")
  223. set_status_text(f"Connection to '{endpoint}' failed.", StatusMessageType.ERROR)
  224. return
  225. if use_post_method:
  226. logging.debug(f"{query_result}")
  227. if query_result:
  228. set_status_text(f"{query_result}")
  229. return
  230. result_items = query_result["results"]["bindings"]
  231. if not result_items:
  232. dpg.delete_item(RESULT_TABLE_TAG, children_only=True)
  233. logging.debug(f"No results returned.")
  234. set_status_text("No results.")
  235. return
  236. dpg.delete_item(RESULT_TABLE_TAG, children_only=True)
  237. set_status_text("Query successful.")
  238. columns = result_items[0].keys()
  239. for column in columns:
  240. dpg.add_table_column(label=column, width_fixed=True, parent=RESULT_TABLE_TAG)
  241. # dpg.add_table_column(label="CCC", width_stretch=True, init_width_or_weight=0.0)
  242. # TODO This is as brittle as my ego, needs a fix asap
  243. # TODO Fix filter
  244. # with dpg.filter_set(tag="__result_filter", parent=QUERY_RESULT_TAB_TAG):
  245. for result in result_items:
  246. with dpg.table_row(parent=RESULT_TABLE_TAG):
  247. for key, value in result.items():
  248. shortened = shorten(f"{value.get('value')}")
  249. dpg.add_text(shortened, filter_key=shortened)
  250. with dpg.drag_payload(parent=dpg.last_item(), drag_data=shortened, payload_type="string"):
  251. dpg.add_text(shortened, filter_key=shortened)
  252. def _select_directory(sender: int, mode: str, user_data: Optional[dict]):
  253. with dpg.file_dialog(directory_selector=True, show=True, callback=_process_directory):
  254. dpg.add_file_extension(".*")
  255. def _process_directory(sender: int, app_data: str, user_data: Optional[dict]):
  256. directory = app_data["file_path_name"]
  257. # files = listdir(directory)
  258. # dpg.configure_item("files_listbox", items=files)
  259. # dpg.set_value("file_text", directory)
  260. def _select_file(sender: int, app_data: str, user_data: Optional[dict]):
  261. selected_file = app_data
  262. cwd = dpg.get_value("file_text")
  263. selected_file = cwd + "/" + selected_file
  264. dpg.set_value("file_info_n", "file :" + selected_file)
  265. def interface(configuration: Configuration, palette: List[QueryTemplate], width=1200, height=900):
  266. """Show the full user interface."""
  267. dpg.create_context()
  268. dpg.create_viewport(title="Graph Exploring Tool", width=width, height=height)
  269. with dpg.window(tag=PRIMARY_WINDOW_TAG, label="Graph Exploring Tool", menubar=True):
  270. add_main_menu()
  271. with dpg.group(horizontal=True, label="Main"):
  272. create_query_palette(palette)
  273. with dpg.group(horizontal=False):
  274. create_query_options(configuration.endpoint_sparql)
  275. create_query_editor_visual(configuration.example_prefix, configuration.example_query, show=True)
  276. create_query_editor_textual(configuration.example_prefix, configuration.example_query, show=False)
  277. create_status_console()
  278. create_query_results()
  279. # demo.show_demo()
  280. dpg.setup_dearpygui()
  281. dpg.show_viewport()
  282. dpg.set_primary_window(PRIMARY_WINDOW_TAG, True)
  283. dpg.start_dearpygui()
  284. dpg.destroy_context()