Use futures in prompts, add QInputDialog for vis

This commit is contained in:
Ben 2024-02-02 14:49:39 +01:00
parent 51ef6fe385
commit ada8f8c6bb
Signed by: ben
GPG Key ID: 0F54A7ED232D3319
5 changed files with 195 additions and 123 deletions

View File

@ -1,3 +1,4 @@
from concurrent.futures import Future
from datetime import timedelta, datetime from datetime import timedelta, datetime
from typing import Callable, Optional from typing import Callable, Optional
@ -17,32 +18,45 @@ def resource_needs_update(resource: Resource | None, recipe_info_timeout: Option
) )
def chose_resource(session: AlchemySession, resource_label: str, prompt: Callable) -> Resource | None: def chose_resource(session: AlchemySession, resource_label: str, prompt: Callable) -> Future[Resource | None]:
matching_resources = session.scalars(Resource.by_label(resource_label)).all() matching_resources = session.scalars(Resource.by_label(resource_label)).all()
ret = Future()
if len(matching_resources) == 0: if len(matching_resources) == 0:
print("Could not find existing resources matching the search string.. starting wiki search") print("Could not find any resource matching the search string…")
ret.set_result(None)
else: else:
options = {(idx + 1): str(matching_resources[idx].label) for idx in range(len(matching_resources))} options = {(idx + 1): str(matching_resources[idx].label) for idx in range(len(matching_resources))}
options[0] = "" options[0] = "Continue with wiki search…"
selected_res = prompt(
options=options, text="Chose a resource to continue or 0 to continue with a wiki search", default=1 def resource_selected_cb(res_future: Future[int | None]):
) selected_res = res_future.result()
if selected_res is not None and selected_res != 0: if selected_res is not None and selected_res != 0:
return matching_resources[selected_res - 1] ret.set_result(matching_resources[selected_res - 1])
return None else:
ret.set_result(None)
prompt(options=options, text="Chose a resource", default=1).add_done_callback(resource_selected_cb)
return ret
def chose_recipe(session: AlchemySession, resource: Resource, prompt: Callable) -> Recipe | None: def chose_recipe(session: AlchemySession, resource: Resource, prompt: Callable) -> Future[Recipe | None]:
stmt = select(Recipe).join(Recipe.results).filter(ResourceFlow.resource_id == resource.id) stmt = select(Recipe).join(Recipe.results).filter(ResourceFlow.resource_id == resource.id)
recipes = session.scalars(stmt).all() recipes = session.scalars(stmt).all()
ret = Future()
if len(recipes) == 0: if len(recipes) == 0:
print("No recipes found for resource") print("No recipes found for resource")
return None ret.set_result(None)
elif len(recipes) > 1: elif len(recipes) > 1:
options = {(idx + 1): recipes[idx].describe() for idx in range(len(recipes))} options = {(idx + 1): recipes[idx].describe() for idx in range(len(recipes))}
user_choice = prompt(options=options, text="Select recipe", default=1)
if user_choice is None: def user_choice_cb(choice_future: Future[int | None]):
return None user_choice = choice_future.result()
return recipes[user_choice - 1] if user_choice is None:
ret.set_result(None)
else:
ret.set_result(recipes[user_choice - 1])
prompt(options=options, text="Select recipe", default=1).add_done_callback(user_choice_cb)
else: else:
return recipes[0] ret.set_result(recipes[0])
return ret

View File

@ -7,7 +7,7 @@ from sqlalchemy.orm import Session
from factorygame.data.common import resource_needs_update, chose_resource from factorygame.data.common import resource_needs_update, chose_resource
from .models import Base, ResourceFlow, Recipe from .models import Base, ResourceFlow, Recipe
from .sfp import SatisfactoryPlus from .sfp import SatisfactoryPlus
from ..helper import prompt from ..helper import click_prompt
@click.command() @click.command()
@ -20,7 +20,7 @@ def main(result: bool, debug: bool, refetch: bool, search: str):
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
if result and search: if result and search:
with Session(engine) as session: with Session(engine) as session:
resource = chose_resource(session=session, resource_label=search, prompt=prompt) resource = chose_resource(session=session, resource_label=search, prompt=click_prompt).result()
exists_in_db = resource is not None exists_in_db = resource is not None
with SatisfactoryPlus(debug=debug) as data_provider: with SatisfactoryPlus(debug=debug) as data_provider:

View File

@ -10,7 +10,7 @@ from sqlalchemy.orm import Session as AlchemySession
from .models import Resource, ResourceFlow, Factory, Recipe from .models import Resource, ResourceFlow, Factory, Recipe
from .provider import RecipeProvider from .provider import RecipeProvider
from ..helper import prompt from ..helper import click_prompt
class SatisfactoryPlus(RecipeProvider, AbstractContextManager): class SatisfactoryPlus(RecipeProvider, AbstractContextManager):
@ -66,7 +66,7 @@ class SatisfactoryPlus(RecipeProvider, AbstractContextManager):
options[idx + 1] = name options[idx + 1] = name
if name.casefold() == search.casefold(): if name.casefold() == search.casefold():
default_choice = idx + 1 default_choice = idx + 1
user_choice = prompt(options=options, text="Chose a recipe to continue…", default=default_choice) user_choice = click_prompt(options=options, text="Chose a recipe to continue…", default=default_choice)
if user_choice is None: if user_choice is None:
return None return None
link_html_elem = choices[user_choice - 1] link_html_elem = choices[user_choice - 1]

View File

@ -1,22 +1,23 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import re import re
from concurrent.futures import Future
from datetime import timedelta from datetime import timedelta
import click import click
from NodeGraphQt import BaseNode, NodeBaseWidget from NodeGraphQt import BaseNode, NodeBaseWidget
from NodeGraphQt import NodeGraph, Port from NodeGraphQt import NodeGraph, Port
from NodeGraphQt.constants import PortTypeEnum, NodePropWidgetEnum from NodeGraphQt.constants import PortTypeEnum, NodePropWidgetEnum, ViewerEnum
from NodeGraphQt.widgets.node_widgets import _NodeGroupBox, NodeLineEdit, NodeCheckBox from NodeGraphQt.widgets.node_widgets import _NodeGroupBox, NodeLineEdit, NodeCheckBox
from PySide2.QtCore import Qt from PySide2 import QtGui
from PySide2.QtWidgets import QSlider, QLineEdit, QCheckBox, QGraphicsItem from PySide2.QtCore import Qt, QObject
from PySide2.QtWidgets import QSlider, QLineEdit, QCheckBox, QGraphicsItem, QInputDialog
from Qt import QtWidgets from Qt import QtWidgets
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from factorygame.data.common import chose_resource, resource_needs_update, chose_recipe from factorygame.data.common import chose_resource, resource_needs_update, chose_recipe
from .models import Recipe, Resource, ResourceFlow from .models import Recipe, Resource, ResourceFlow
from ..helper import prompt
WORLD_OUTPUT_PORT_NAME = "World Output" WORLD_OUTPUT_PORT_NAME = "World Output"
WORLD_OUTPUT_COLOR = (0, 139, 41) WORLD_OUTPUT_COLOR = (0, 139, 41)
@ -28,9 +29,6 @@ OUTPUT_COLOR = (204, 44, 36)
OTHER_COLOR = (0, 83, 135) OTHER_COLOR = (0, 83, 135)
graph: NodeGraph
def in_amount_name(label: str) -> str: def in_amount_name(label: str) -> str:
return f"In {label} amount" return f"In {label} amount"
@ -134,11 +132,11 @@ class GlobalInput(BaseNode):
self.delete_output(CREATE_MACHINE_PORT_NAME) self.delete_output(CREATE_MACHINE_PORT_NAME)
self.add_special_ports() self.add_special_ports()
def update_x_pos(self): def update_x_pos(self, graph: NodeGraph):
min_x_pos = min(map(lambda node: node.x_pos(), filter(lambda node: node != self, graph.all_nodes()))) min_x_pos = min(map(lambda node: node.x_pos(), filter(lambda node: node != self, graph.all_nodes())))
self.set_x_pos(min_x_pos - self.view.width - 150) self.set_x_pos(min_x_pos - self.view.width - 150)
def create_global_output(self, resource_label: str, initial_resource_amount: str) -> Port: def create_global_output(self, graph: NodeGraph, resource_label: str, initial_resource_amount: str) -> Port:
new_output_port = self.add_output(name=resource_label, multi_output=False, color=OUTPUT_COLOR) new_output_port = self.add_output(name=resource_label, multi_output=False, color=OUTPUT_COLOR)
widget = add_resource_text( widget = add_resource_text(
node=self, node=self,
@ -149,7 +147,7 @@ class GlobalInput(BaseNode):
self.output_resources[resource_label] = parse_resource_amount(initial_resource_amount) self.output_resources[resource_label] = parse_resource_amount(initial_resource_amount)
widget.value_changed.connect(lambda: self.update_resource_output(resource_label)) widget.value_changed.connect(lambda: self.update_resource_output(resource_label))
self.reorder_outputs() self.reorder_outputs()
self.update_x_pos() self.update_x_pos(graph=graph)
return new_output_port return new_output_port
def update_resource_output(self, resource_label: str): def update_resource_output(self, resource_label: str):
@ -323,106 +321,161 @@ class Machine(BaseNode):
return self.output_resources[resource_label] * (int(self.get_property(Machine.ACTUAL_PERFORMANCE_PROP)) / 100.0) return self.output_resources[resource_label] * (int(self.get_property(Machine.ACTUAL_PERFORMANCE_PROP)) / 100.0)
def on_port_connected(input_port: Port, output_port: Port): class GraphController(QObject):
global debug def __init__(self, debug: bool, parent=None):
if debug: super().__init__(parent=parent)
print(f"Port {output_port} connected to {input_port}") self.debug = debug
output_node = output_port.node()
if isinstance(output_node, GlobalInput): bg_color = QtGui.QColor(*ViewerEnum.BACKGROUND_COLOR.value).darker(120).getRgb()
resource_label = input_port.name() text_color = tuple(map(str, map(lambda i, j: i - j, (255, 255, 255), bg_color)))
if output_port.name() == CREATE_MACHINE_PORT_NAME: self.fgbg_color_stylesheet = (
output_port.clear_connections(push_undo=False, emit_signal=False) "* {"
with Session(engine) as session: " background-color: rgba(" + ",".join(map(str, bg_color)) + ") ;"
resource = session.scalars(Resource.by_label(resource_label)).one_or_none() " color: rgb(" + ",".join(text_color) + ");"
if resource_needs_update(resource, recipe_info_timeout=timedelta(days=365)): "}"
print("Please fetch resource", resource_label, "first.") )
self.engine = create_engine("sqlite:///file.db", echo=debug)
self.graph = NodeGraph(parent=self)
self.graph.widget.resize(1280, 720)
self.graph.register_node(Machine)
self.graph.register_node(GlobalInput)
self.global_input: GlobalInput = self.graph.create_node("factorygame.GlobalInput", push_undo=False)
self.graph.port_connected.connect(self.on_port_connected)
def add_machine_from_search(self, search: str):
recipe_selected_future = Future()
def resource_selected_cb(resource_future: Future[Resource | None]):
resource = resource_future.result()
if resource is None:
# FIXME: use concurrent resource searching/fetching
# ret = data_provider.search_for_resource(session=session, search=search)
ret = None
if ret is None:
print("Resource not found")
recipe_selected_future.set_result(None)
return return
# FIXME: use some Qt UI prompt method resource, exists_in_db = ret
recipe = chose_recipe(session=session, resource=resource, prompt=prompt) if not exists_in_db:
if recipe is None: print("Resource not yet fetched, run fetch first")
recipe_selected_future.set_result(None)
return return
if resource_needs_update(resource, recipe_info_timeout=timedelta(days=365)):
print("Please fetch resource", resource.label, "first.")
recipe_selected_future.set_result(None)
return
recipe_machine: Machine = graph.create_node("factorygame.Machine", push_undo=True) with Session(self.engine) as session:
recipe_machine.assign_recipe(recipe) chose_recipe(session=session, resource=resource, prompt=self.dialog_prompt).add_done_callback(
recipe_machine.update() lambda fut: recipe_selected_future.set_result(fut.result())
recipe_machine.set_x_pos(input_port.node().x_pos() - recipe_machine.view.width - 100)
recipe_machine.get_output(resource_label).connect_to(input_port)
output_node.update_x_pos()
elif output_port.name() == WORLD_OUTPUT_PORT_NAME:
output_port.clear_connections(push_undo=False, emit_signal=False)
for idx, port in enumerate(output_node.output_ports()):
assert port.name() != resource_label, f"Duplicate output port for {resource_label}"
# if port.name() == resource_label:
# port.connect_to(input_port, push_undo=True, emit_signal=False)
# return
if isinstance(input_port.node(), Machine):
machine_node: Machine = input_port.node()
initial_resource_amount = str(machine_node.get_property(in_amount_name(resource_label)))
new_output_port = output_node.create_global_output(
resource_label=resource_label,
initial_resource_amount=initial_resource_amount,
) )
new_output_port.connect_to(input_port, push_undo=True, emit_signal=False)
elif isinstance(output_node, Machine): def select_recipe_async():
input_node = input_port.node() with Session(self.engine) as session:
resource_label = output_port.name() resource_future = chose_resource(session=session, resource_label=search, prompt=self.dialog_prompt)
if isinstance(input_node, Machine): resource_future.add_done_callback(resource_selected_cb)
resource_amount = output_node.get_resource_output(resource_label)
input_node.update_input( def recipe_selections_cb(recipe_future: Future[Recipe | None]):
resource_label, recipe = recipe_future.result()
resource_amount, if recipe is not None:
) recipe_machine: Machine = self.graph.create_node("factorygame.Machine", push_undo=False)
recipe_machine.assign_recipe(recipe)
self.graph.auto_layout_nodes([self.global_input, recipe_machine])
self.global_input.set_y_pos(recipe_machine.y_pos())
self.global_input.update_x_pos(graph=self.graph)
self.graph.center_on([self.global_input, recipe_machine])
recipe_selected_future.add_done_callback(recipe_selections_cb)
select_recipe_async()
def show(self):
self.graph.widget.show()
def on_port_connected(self, input_port: Port, output_port: Port):
if self.debug:
print(f"Port {output_port} connected to {input_port}")
output_node = output_port.node()
if output_node == self.global_input:
resource_label = input_port.name()
if output_port.name() == CREATE_MACHINE_PORT_NAME:
output_port.clear_connections(push_undo=False, emit_signal=False)
def recipe_selected_cb(recipe_future: Future[Recipe | None]):
recipe = recipe_future.result()
if recipe is not None:
recipe_machine: Machine = self.graph.create_node("factorygame.Machine", push_undo=True)
recipe_machine.assign_recipe(recipe)
recipe_machine.update()
recipe_machine.set_x_pos(input_port.node().x_pos() - recipe_machine.view.width - 100)
recipe_machine.get_output(resource_label).connect_to(input_port)
self.global_input.update_x_pos(graph=self.graph)
with Session(self.engine) as session:
resource = session.scalars(Resource.by_label(resource_label)).one_or_none()
if resource_needs_update(resource, recipe_info_timeout=timedelta(days=365)):
print("Please fetch resource", resource_label, "first.")
return
chose_recipe(session=session, resource=resource, prompt=self.dialog_prompt).add_done_callback(
recipe_selected_cb
)
elif output_port.name() == WORLD_OUTPUT_PORT_NAME:
output_port.clear_connections(push_undo=False, emit_signal=False)
for idx, port in enumerate(self.global_input.output_ports()):
assert port.name() != resource_label, f"Duplicate output port for {resource_label}"
# if port.name() == resource_label:
# port.connect_to(input_port, push_undo=True, emit_signal=False)
# return
if isinstance(input_port.node(), Machine):
machine_node: Machine = input_port.node()
initial_resource_amount = str(machine_node.get_property(in_amount_name(resource_label)))
new_output_port = self.global_input.create_global_output(
graph=self.graph,
resource_label=resource_label,
initial_resource_amount=initial_resource_amount,
)
new_output_port.connect_to(input_port, push_undo=True, emit_signal=False)
elif isinstance(output_node, Machine):
input_node = input_port.node()
resource_label = output_port.name()
if isinstance(input_node, Machine):
resource_amount = output_node.get_resource_output(resource_label)
input_node.update_input(
resource_label,
resource_amount,
)
def dialog_prompt(self, options: dict[int, str], text: str, default: int) -> Future[int | None]:
if self.debug:
print("Displaying QInputDialog with options:", ", ".join(options.values()))
dialog = QInputDialog(parent=self.graph.widget)
dialog.setStyleSheet(self.fgbg_color_stylesheet)
dialog.setModal(True)
dialog.setLabelText(text)
if default in options:
dialog.setTextValue(options[default])
reversed_options: dict[str, int] = {value: key for key, value in options.items()}
dialog.setComboBoxItems(reversed_options.keys())
ret = Future()
dialog.rejected.connect(lambda: ret.set_result(None))
dialog.accepted.connect(lambda: ret.set_result(reversed_options[dialog.textValue()]))
dialog.show()
return ret
@click.command @click.command
@click.option("--debug", is_flag=True) @click.option("--debug", is_flag=True)
@click.argument("search") @click.argument("search")
def main(debug: bool, search: str): def main(debug: bool, search: str):
global engine
globals()["debug"] = debug
engine = create_engine("sqlite:///file.db", echo=debug)
with Session(engine) as session:
# FIXME: use some Qt UI prompt method
resource = chose_resource(session=session, resource_label=search, prompt=prompt)
if resource is None:
# FIXME: use concurrent resource searching/fetching
# ret = data_provider.search_for_resource(session=session, search=search)
ret = None
if ret is None:
print("Resource not found")
return
resource, exists_in_db = ret
if not exists_in_db:
print("Resource not yet fetched, run fetch first")
return
if resource_needs_update(resource, recipe_info_timeout=timedelta(days=365)):
print("Please fetch resource", resource.label, "first.")
return
# FIXME: use some Qt UI prompt method
recipe = chose_recipe(session=session, resource=resource, prompt=prompt)
if recipe is None:
return
app = QtWidgets.QApplication([]) app = QtWidgets.QApplication([])
global graph graph_controller = GraphController(debug=debug)
graph = NodeGraph() graph_controller.show()
graph.widget.resize(1280, 720) graph_controller.add_machine_from_search(search=search)
graph.register_node(Machine)
graph.register_node(GlobalInput)
graph_widget = graph.widget
graph_widget.show()
global_input = graph.create_node("factorygame.GlobalInput", push_undo=False)
recipe_machine = graph.create_node("factorygame.Machine", push_undo=False)
recipe_machine.assign_recipe(recipe)
graph.auto_layout_nodes([global_input, recipe_machine])
global_input.set_y_pos(recipe_machine.y_pos())
global_input.update_x_pos()
graph.center_on([global_input, recipe_machine])
graph.port_connected.connect(on_port_connected)
app.exec_() app.exec_()

View File

@ -1,13 +1,18 @@
from concurrent.futures import Future
import click import click
def prompt(options: dict[int, str], **kwargs) -> int | None: def click_prompt(options: dict[int, str], text: str, default: int) -> Future[int | None]:
for idx, label in options.items(): for idx, label in options.items():
if label: if label:
click.echo(f"{idx}: {label}") click.echo(f"{idx}: {label}")
ret = click.prompt(**kwargs) user_choice = click.prompt(text=text, default=default)
ret = int(ret) user_choice = int(user_choice)
if ret not in options: ret = Future()
if user_choice not in options:
click.echo("Invalid choice.") click.echo("Invalid choice.")
return None ret.set_result(None)
else:
ret.set_result(user_choice)
return ret return ret