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 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()
ret = Future()
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:
options = {(idx + 1): str(matching_resources[idx].label) for idx in range(len(matching_resources))}
options[0] = ""
selected_res = prompt(
options=options, text="Chose a resource to continue or 0 to continue with a wiki search", default=1
)
if selected_res is not None and selected_res != 0:
return matching_resources[selected_res - 1]
return None
options[0] = "Continue with wiki search…"
def resource_selected_cb(res_future: Future[int | None]):
selected_res = res_future.result()
if selected_res is not None and selected_res != 0:
ret.set_result(matching_resources[selected_res - 1])
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)
recipes = session.scalars(stmt).all()
ret = Future()
if len(recipes) == 0:
print("No recipes found for resource")
return None
ret.set_result(None)
elif len(recipes) > 1:
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:
return None
return recipes[user_choice - 1]
def user_choice_cb(choice_future: Future[int | None]):
user_choice = choice_future.result()
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:
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 .models import Base, ResourceFlow, Recipe
from .sfp import SatisfactoryPlus
from ..helper import prompt
from ..helper import click_prompt
@click.command()
@ -20,7 +20,7 @@ def main(result: bool, debug: bool, refetch: bool, search: str):
Base.metadata.create_all(bind=engine)
if result and search:
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
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 .provider import RecipeProvider
from ..helper import prompt
from ..helper import click_prompt
class SatisfactoryPlus(RecipeProvider, AbstractContextManager):
@ -66,7 +66,7 @@ class SatisfactoryPlus(RecipeProvider, AbstractContextManager):
options[idx + 1] = name
if name.casefold() == search.casefold():
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:
return None
link_html_elem = choices[user_choice - 1]

View file

@ -1,22 +1,23 @@
#!/usr/bin/env python3
import re
from concurrent.futures import Future
from datetime import timedelta
import click
from NodeGraphQt import BaseNode, NodeBaseWidget
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 PySide2.QtCore import Qt
from PySide2.QtWidgets import QSlider, QLineEdit, QCheckBox, QGraphicsItem
from PySide2 import QtGui
from PySide2.QtCore import Qt, QObject
from PySide2.QtWidgets import QSlider, QLineEdit, QCheckBox, QGraphicsItem, QInputDialog
from Qt import QtWidgets
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from factorygame.data.common import chose_resource, resource_needs_update, chose_recipe
from .models import Recipe, Resource, ResourceFlow
from ..helper import prompt
WORLD_OUTPUT_PORT_NAME = "World Output"
WORLD_OUTPUT_COLOR = (0, 139, 41)
@ -28,9 +29,6 @@ OUTPUT_COLOR = (204, 44, 36)
OTHER_COLOR = (0, 83, 135)
graph: NodeGraph
def in_amount_name(label: str) -> str:
return f"In {label} amount"
@ -134,11 +132,11 @@ class GlobalInput(BaseNode):
self.delete_output(CREATE_MACHINE_PORT_NAME)
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())))
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)
widget = add_resource_text(
node=self,
@ -149,7 +147,7 @@ class GlobalInput(BaseNode):
self.output_resources[resource_label] = parse_resource_amount(initial_resource_amount)
widget.value_changed.connect(lambda: self.update_resource_output(resource_label))
self.reorder_outputs()
self.update_x_pos()
self.update_x_pos(graph=graph)
return new_output_port
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)
def on_port_connected(input_port: Port, output_port: Port):
global debug
if debug:
print(f"Port {output_port} connected to {input_port}")
output_node = output_port.node()
if isinstance(output_node, GlobalInput):
resource_label = input_port.name()
if output_port.name() == CREATE_MACHINE_PORT_NAME:
output_port.clear_connections(push_undo=False, emit_signal=False)
with Session(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.")
class GraphController(QObject):
def __init__(self, debug: bool, parent=None):
super().__init__(parent=parent)
self.debug = debug
bg_color = QtGui.QColor(*ViewerEnum.BACKGROUND_COLOR.value).darker(120).getRgb()
text_color = tuple(map(str, map(lambda i, j: i - j, (255, 255, 255), bg_color)))
self.fgbg_color_stylesheet = (
"* {"
" background-color: rgba(" + ",".join(map(str, bg_color)) + ") ;"
" color: rgb(" + ",".join(text_color) + ");"
"}"
)
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
# FIXME: use some Qt UI prompt method
recipe = chose_recipe(session=session, resource=resource, prompt=prompt)
if recipe is None:
resource, exists_in_db = ret
if not exists_in_db:
print("Resource not yet fetched, run fetch first")
recipe_selected_future.set_result(None)
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)
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)
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,
with Session(self.engine) as session:
chose_recipe(session=session, resource=resource, prompt=self.dialog_prompt).add_done_callback(
lambda fut: recipe_selected_future.set_result(fut.result())
)
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 select_recipe_async():
with Session(self.engine) as session:
resource_future = chose_resource(session=session, resource_label=search, prompt=self.dialog_prompt)
resource_future.add_done_callback(resource_selected_cb)
def recipe_selections_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=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.option("--debug", is_flag=True)
@click.argument("search")
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([])
global graph
graph = NodeGraph()
graph.widget.resize(1280, 720)
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)
graph_controller = GraphController(debug=debug)
graph_controller.show()
graph_controller.add_machine_from_search(search=search)
app.exec_()

View file

@ -1,13 +1,18 @@
from concurrent.futures import Future
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():
if label:
click.echo(f"{idx}: {label}")
ret = click.prompt(**kwargs)
ret = int(ret)
if ret not in options:
user_choice = click.prompt(text=text, default=default)
user_choice = int(user_choice)
ret = Future()
if user_choice not in options:
click.echo("Invalid choice.")
return None
ret.set_result(None)
else:
ret.set_result(user_choice)
return ret