ui_loadsave.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import json
  2. import os
  3. import gradio as gr
  4. from modules import errors
  5. from modules.ui_components import ToolButton, InputAccordion
  6. def radio_choices(comp): # gradio 3.41 changes choices from list of values to list of pairs
  7. return [x[0] if isinstance(x, tuple) else x for x in getattr(comp, 'choices', [])]
  8. class UiLoadsave:
  9. """allows saving and restoring default values for gradio components"""
  10. def __init__(self, filename):
  11. self.filename = filename
  12. self.ui_settings = {}
  13. self.component_mapping = {}
  14. self.error_loading = False
  15. self.finalized_ui = False
  16. self.ui_defaults_view = None
  17. self.ui_defaults_apply = None
  18. self.ui_defaults_review = None
  19. try:
  20. self.ui_settings = self.read_from_file()
  21. except FileNotFoundError:
  22. pass
  23. except Exception as e:
  24. self.error_loading = True
  25. errors.display(e, "loading settings")
  26. def add_component(self, path, x):
  27. """adds component to the registry of tracked components"""
  28. assert not self.finalized_ui
  29. def apply_field(obj, field, condition=None, init_field=None):
  30. key = f"{path}/{field}"
  31. if getattr(obj, 'custom_script_source', None) is not None:
  32. key = f"customscript/{obj.custom_script_source}/{key}"
  33. if getattr(obj, 'do_not_save_to_config', False):
  34. return
  35. saved_value = self.ui_settings.get(key, None)
  36. if isinstance(obj, gr.Accordion) and isinstance(x, InputAccordion) and field == 'value':
  37. field = 'open'
  38. if saved_value is None:
  39. self.ui_settings[key] = getattr(obj, field)
  40. elif condition and not condition(saved_value):
  41. pass
  42. else:
  43. if isinstance(obj, gr.Textbox) and field == 'value': # due to an undesirable behavior of gr.Textbox, if you give it an int value instead of str, everything dies
  44. saved_value = str(saved_value)
  45. elif isinstance(obj, gr.Number) and field == 'value':
  46. try:
  47. saved_value = float(saved_value)
  48. except ValueError:
  49. return
  50. setattr(obj, field, saved_value)
  51. if init_field is not None:
  52. init_field(saved_value)
  53. if field == 'value' and key not in self.component_mapping:
  54. self.component_mapping[key] = obj
  55. if type(x) in [gr.Slider, gr.Radio, gr.Checkbox, gr.Textbox, gr.Number, gr.Dropdown, ToolButton, gr.Button] and x.visible:
  56. apply_field(x, 'visible')
  57. if type(x) == gr.Slider:
  58. apply_field(x, 'value')
  59. apply_field(x, 'minimum')
  60. apply_field(x, 'maximum')
  61. apply_field(x, 'step')
  62. if type(x) == gr.Radio:
  63. apply_field(x, 'value', lambda val: val in radio_choices(x))
  64. if type(x) == gr.Checkbox:
  65. apply_field(x, 'value')
  66. if type(x) == gr.Textbox:
  67. apply_field(x, 'value')
  68. if type(x) == gr.Number:
  69. apply_field(x, 'value')
  70. if type(x) == gr.Dropdown:
  71. def check_dropdown(val):
  72. choices = radio_choices(x)
  73. if getattr(x, 'multiselect', False):
  74. return all(value in choices for value in val)
  75. else:
  76. return val in choices
  77. apply_field(x, 'value', check_dropdown, getattr(x, 'init_field', None))
  78. if type(x) == InputAccordion:
  79. if hasattr(x, 'custom_script_source'):
  80. x.accordion.custom_script_source = x.custom_script_source
  81. if x.accordion.visible:
  82. apply_field(x.accordion, 'visible')
  83. apply_field(x, 'value')
  84. apply_field(x.accordion, 'value')
  85. def check_tab_id(tab_id):
  86. tab_items = list(filter(lambda e: isinstance(e, gr.TabItem), x.children))
  87. if type(tab_id) == str:
  88. tab_ids = [t.id for t in tab_items]
  89. return tab_id in tab_ids
  90. elif type(tab_id) == int:
  91. return 0 <= tab_id < len(tab_items)
  92. else:
  93. return False
  94. if type(x) == gr.Tabs:
  95. apply_field(x, 'selected', check_tab_id)
  96. def add_block(self, x, path=""):
  97. """adds all components inside a gradio block x to the registry of tracked components"""
  98. if hasattr(x, 'children'):
  99. if isinstance(x, gr.Tabs) and x.elem_id is not None:
  100. # Tabs element can't have a label, have to use elem_id instead
  101. self.add_component(f"{path}/Tabs@{x.elem_id}", x)
  102. for c in x.children:
  103. self.add_block(c, path)
  104. elif x.label is not None:
  105. self.add_component(f"{path}/{x.label}", x)
  106. elif isinstance(x, gr.Button) and x.value is not None:
  107. self.add_component(f"{path}/{x.value}", x)
  108. def read_from_file(self):
  109. with open(self.filename, "r", encoding="utf8") as file:
  110. return json.load(file)
  111. def write_to_file(self, current_ui_settings):
  112. with open(self.filename, "w", encoding="utf8") as file:
  113. json.dump(current_ui_settings, file, indent=4, ensure_ascii=False)
  114. def dump_defaults(self):
  115. """saves default values to a file unless the file is present and there was an error loading default values at start"""
  116. if self.error_loading and os.path.exists(self.filename):
  117. return
  118. self.write_to_file(self.ui_settings)
  119. def iter_changes(self, current_ui_settings, values):
  120. """
  121. given a dictionary with defaults from a file and current values from gradio elements, returns
  122. an iterator over tuples of values that are not the same between the file and the current;
  123. tuple contents are: path, old value, new value
  124. """
  125. for (path, component), new_value in zip(self.component_mapping.items(), values):
  126. old_value = current_ui_settings.get(path)
  127. choices = radio_choices(component)
  128. if isinstance(new_value, int) and choices:
  129. if new_value >= len(choices):
  130. continue
  131. new_value = choices[new_value]
  132. if isinstance(new_value, tuple):
  133. new_value = new_value[0]
  134. if new_value == old_value:
  135. continue
  136. if old_value is None and new_value == '' or new_value == []:
  137. continue
  138. yield path, old_value, new_value
  139. def ui_view(self, *values):
  140. text = ["<table><thead><tr><th>Path</th><th>Old value</th><th>New value</th></thead><tbody>"]
  141. for path, old_value, new_value in self.iter_changes(self.read_from_file(), values):
  142. if old_value is None:
  143. old_value = "<span class='ui-defaults-none'>None</span>"
  144. text.append(f"<tr><td>{path}</td><td>{old_value}</td><td>{new_value}</td></tr>")
  145. if len(text) == 1:
  146. text.append("<tr><td colspan=3>No changes</td></tr>")
  147. text.append("</tbody>")
  148. return "".join(text)
  149. def ui_apply(self, *values):
  150. num_changed = 0
  151. current_ui_settings = self.read_from_file()
  152. for path, _, new_value in self.iter_changes(current_ui_settings.copy(), values):
  153. num_changed += 1
  154. current_ui_settings[path] = new_value
  155. if num_changed == 0:
  156. return "No changes."
  157. self.write_to_file(current_ui_settings)
  158. return f"Wrote {num_changed} changes."
  159. def create_ui(self):
  160. """creates ui elements for editing defaults UI, without adding any logic to them"""
  161. gr.HTML(
  162. f"This page allows you to change default values in UI elements on other tabs.<br />"
  163. f"Make your changes, press 'View changes' to review the changed default values,<br />"
  164. f"then press 'Apply' to write them to {self.filename}.<br />"
  165. f"New defaults will apply after you restart the UI.<br />"
  166. )
  167. with gr.Row():
  168. self.ui_defaults_view = gr.Button(value='View changes', elem_id="ui_defaults_view", variant="secondary")
  169. self.ui_defaults_apply = gr.Button(value='Apply', elem_id="ui_defaults_apply", variant="primary")
  170. self.ui_defaults_review = gr.HTML("")
  171. def setup_ui(self):
  172. """adds logic to elements created with create_ui; all add_block class must be made before this"""
  173. assert not self.finalized_ui
  174. self.finalized_ui = True
  175. self.ui_defaults_view.click(fn=self.ui_view, inputs=list(self.component_mapping.values()), outputs=[self.ui_defaults_review])
  176. self.ui_defaults_apply.click(fn=self.ui_apply, inputs=list(self.component_mapping.values()), outputs=[self.ui_defaults_review])