Index: geniuswebcore/geniusweb/protocol/session/DefaultSessionState.py
===================================================================
--- geniuswebcore/geniusweb/protocol/session/DefaultSessionState.py	(revision 87)
+++ geniuswebcore/geniusweb/protocol/session/DefaultSessionState.py	(revision 88)
@@ -12,10 +12,9 @@
 from geniusweb.utils import toTuple, val
 
-
-S = TypeVar('S',bound=SessionSettings)   
-P = TypeVar('P',bound="DefaultSessionState")
-
-
-class DefaultSessionState (SessionState, Generic[P,S]):
+S = TypeVar('S', bound=SessionSettings)   
+P = TypeVar('P', bound="DefaultSessionState")
+
+
+class DefaultSessionState (SessionState, Generic[P, S]):
 	'''
 	The default current state of the session. immutable.
@@ -29,9 +28,8 @@
 	# 	partyprofiles: Optional[Dict[PartyId, PartyWithProfile]]
 	# 	settings: S
-	
 
 	def __init__(self, actions:List[Action], connections:List[PartyId],
 			progress:Optional[Progress], settings:S,
-			partyprofiles:Optional[Dict[PartyId, PartyWithProfile]]=None, 
+			partyprofiles:Optional[Dict[PartyId, PartyWithProfile]]=None,
 			error:Optional[ProtocolException]=None):
 		'''
@@ -63,5 +61,5 @@
 		if len(self._connections) != len(set(self._connections)):
 			raise ValueError("There can not be multiple connections for a party:"
-							+ str(self._connections))
+							+str(self._connections))
 
 		if not settings:
@@ -72,7 +70,7 @@
 
 	@abstractmethod
-	def With(self,actions1:List[Action] ,  conns:List[PartyId],
+	def With(self, actions1:List[Action] , conns:List[PartyId],
 			progress1:Optional[Progress] , settings1:S,
-			partyprofiles1:Dict[PartyId, PartyWithProfile] , e: Optional[ProtocolException]) -> P :
+			partyprofiles1:Dict[PartyId, PartyWithProfile] , e: Optional[ProtocolException]) -> P:
 		'''
 		Construct a new session state, where the DefaultSessionState changes
@@ -93,5 +91,5 @@
 		'''
 
-	def getConnections(self)->List[PartyId] :
+	def getConnections(self) -> List[PartyId]:
 		'''
 		@return existing connections.
@@ -99,5 +97,5 @@
 		return list(self._connections)
 
-	def getPartyProfiles(self)->Dict[PartyId, PartyWithProfile] :
+	def getPartyProfiles(self) -> Dict[PartyId, PartyWithProfile]:
 		'''
 		@return map with {@link PartyWithProfile} for the parties. May be an
@@ -107,5 +105,5 @@
 		return dict(self._partyprofiles)
 
-	def getActions(self)->List[Action] :
+	def getActions(self) -> List[Action]:
 		'''
 		@return unmodifyable list of actions done so far.
@@ -113,15 +111,15 @@
 		return list(self._actions)
 
-	def getProgress(self)->Optional[Progress] :
+	def getProgress(self) -> Optional[Progress]:
 		return self._progress
 
-	def getSettings(self)->S :
+	def getSettings(self) -> S:
 		return self._settings
 
-	def isFinal(self,currentTimeMs:int) ->bool :
+	def isFinal(self, currentTimeMs:int) -> bool:
 		return self._error != None or \
-			(self._progress!=None and self._progress.isPastDeadline(currentTimeMs)) #type:ignore
-
-	def getError(self)-> Optional[ProtocolException]:
+			(self._progress != None and self._progress.isPastDeadline(currentTimeMs))  # type:ignore
+
+	def getError(self) -> Optional[ProtocolException]:
 		'''
 		@return an error that occured that caused the session to
@@ -132,9 +130,9 @@
 		return self._error
 
-	def WithDeadlineReached(self)->P :
-		return self.With(self._actions, self._connections, val(self._progress), self._settings, 
+	def WithDeadlineReached(self) -> P:
+		return self.With(self._actions, self._connections, val(self._progress), self._settings,
 				self._partyprofiles, self._error) 
 
-	def WithoutParty(self, party:PartyId )->P :
+	def WithoutParty(self, party:PartyId) -> P:
 		assert isinstance(party, PartyId)
 		newconn = list(self._connections)
@@ -143,5 +141,5 @@
 				self._settings, self._partyprofiles, self._error)
 
-	def WithException(self, e:ProtocolException)-> P:
+	def WithException(self, e:ProtocolException) -> P:
 		'''
 		@param e the error that occured
@@ -149,8 +147,8 @@
 		'''
 		assert isinstance(e, ProtocolException)
-		return self.With(self._actions, self._connections, val(self._progress), 
+		return self.With(self._actions, self._connections, self._progress,
 						self._settings, self._partyprofiles, e)
 
-	def WithParty(self, connection:PartyId,  partyprofile:PartyWithProfile) -> P :
+	def WithParty(self, connection:PartyId, partyprofile:PartyWithProfile) -> P:
 		assert isinstance(connection, PartyId)
 		assert isinstance(partyprofile, PartyWithProfile)
@@ -158,9 +156,9 @@
 		newconns.append(connection)
 		newprofiles = dict(self.getPartyProfiles())
-		newprofiles[connection]= partyprofile
+		newprofiles[connection] = partyprofile
 		return self.With(self.getActions(), newconns, self._progress, self.getSettings(),
 				newprofiles, None)
 
-	def WithProgress(self,newprogress:Progress )->P :
+	def WithProgress(self, newprogress:Progress) -> P:
 		'''
 		Sets the new progress for this session.
@@ -172,8 +170,8 @@
 		if not newprogress:
 			raise ValueError("newprogress must be not null");
-		return self.With(self._actions, self._connections, newprogress, self._settings, 
-						self._partyprofiles,self._error)
-
-	def WithAction(self,actor:PartyId , action:Action )->P :
+		return self.With(self._actions, self._connections, newprogress, self._settings,
+						self._partyprofiles, self._error)
+
+	def WithAction(self, actor:PartyId , action:Action) -> P:
 		'''
 		@param actor  the actor that did this action. Can be used to check if
@@ -189,6 +187,6 @@
 		newactions = list(self.getActions())
 		newactions.append(action)
-		return self.With(newactions, self._connections, val(self._progress), 
-				self._settings, self._partyprofiles,self._error)
+		return self.With(newactions, self._connections, val(self._progress),
+				self._settings, self._partyprofiles, self._error)
 
 	def checkAction(self, actor:PartyId , action:Action) -> Optional[str]:
@@ -198,5 +196,5 @@
 		@return null if action seems ok, or message explaining why not.
 		'''
-		if not actor :
+		if not actor:
 			return "actor must not be null"
 		if not action:
@@ -206,7 +204,7 @@
 		return None
 
-	def __repr__(self)->str:
+	def __repr__(self) -> str:
 		return type(self).__name__ + "[" + str(self._actions) + ","\
-				+ str(self._connections) + "," + str(self._progress) + "," + \
+				+str(self._connections) + "," + str(self._progress) + "," + \
 				str(self._settings) + "," + str(self._error);
 
@@ -219,5 +217,5 @@
 		return isinstance(other, self.__class__) and \
 			self._partyprofiles == other._partyprofiles and \
-			self._actions==other._actions and \
+			self._actions == other._actions and \
 			self._connections == other._connections and \
 			self._progress == other._progress and \
Index: geniuswebcore/geniusweb/references/PartyRef.py
===================================================================
--- geniuswebcore/geniusweb/references/PartyRef.py	(revision 87)
+++ geniuswebcore/geniusweb/references/PartyRef.py	(revision 88)
@@ -12,20 +12,20 @@
 	'''
 
-
-	def __init__(self, party:URI ):
-		if not isinstance(party, URI):
-			raise ValueError("expected URI, but got "+repr(party))
+	def __init__(self, party:URI):
+		# EXTRA: we check that the URI is not completely empty. 
+		# This to get similar behaviour as Java
+		if not isinstance(party, URI) or (party.getHost() == None and party.getPath() == None):
+			raise ValueError("expected URI, but got " + repr(party))
 		self._party = party
 
 	@JsonValue()
-	def getURI(self)-> URI :
+	def getURI(self) -> URI:
 		return self._party;
 
-	def __repr__(self) ->str:
+	def __repr__(self) -> str:
 		return "PartyRef[" + str(self._party) + "]"
 
-
 	def __eq__(self, other):
-		return isinstance(other, self.__class__) and self._party==other._party
+		return isinstance(other, self.__class__) and self._party == other._party
 
 	def __hash__(self):
Index: geniuswebcore/geniusweb/references/ProfileRef.py
===================================================================
--- geniuswebcore/geniusweb/references/ProfileRef.py	(revision 87)
+++ geniuswebcore/geniusweb/references/ProfileRef.py	(revision 88)
@@ -1,5 +1,5 @@
 from pyson.JsonGetter import JsonGetter
 from pyson.JsonValue import JsonValue
-from uri.uri import URI # type: ignore
+from uri.uri import URI  # type: ignore
 
 from geniusweb.references.Reference import Reference
@@ -18,10 +18,12 @@
 
 	def __init__(self, uri:URI):
-		if uri == None:
+		# EXTRA: we check that the URI is not completely empty. 
+		# This to get similar behaviour as Java
+		if uri == None or (uri.getHost() == None and uri.getPath() == None):
 			raise ValueError("profile=null")
 		self._profile = uri
 	
 	@JsonValue()
-	def getURI(self) -> URI :
+	def getURI(self) -> URI:
 		return self._profile
 
@@ -31,5 +33,5 @@
 	def __eq__(self, other):
 		return isinstance(other, self.__class__) and \
-			self._profile==other._profile
+			self._profile == other._profile
 
 	def __hash__(self):
Index: geniuswebcore/geniusweb/simplerunner/NegoRunner.py
===================================================================
--- geniuswebcore/geniusweb/simplerunner/NegoRunner.py	(revision 87)
+++ geniuswebcore/geniusweb/simplerunner/NegoRunner.py	(revision 88)
@@ -17,9 +17,12 @@
 from geniusweb.protocol.NegoState import NegoState
 from geniusweb.simplerunner.ClassPathConnectionFactory import ClassPathConnectionFactory
+from geniusweb.simplerunner.Runner import Runner
+from geniusweb.simplerunner.gui.GUI import GUI
 
 
 class StdOutReporter (Reporter):
+
 	def log(self, level:int , msg:str, exc:Optional[BaseException]=None):
-		if level >= logging.WARNING :
+		if level >= logging.WARNING:
 			print(logging.getLevelName(level) + ":" + msg, file=sys.stderr)
 		else:
@@ -29,6 +32,4 @@
 class NegoRunner:
 	'''
-	WARNING DO NOT USE. NOT WORKING YET. 
-	
 	A simple tool to run a negotiation stand-alone, without starting the servers.
 	All referred files and classes need to be stored locally (or be in the
@@ -47,95 +48,18 @@
 	'''
 
-	_properlyStopped:bool = False
-	_LOOPTIME = 200 # ms
-	_FINALWAITTIME = 5000 # ms
-
-	def __init__(self, settings:NegoSettings ,
-			connectionfactory:ClassPathConnectionFactory , logger:Reporter ,
-			maxruntime:int):
-		'''
-		@param settings          the {@link NegoSettings}
-		@param connectionfactory the {@link ProtocolToPartyConnFactory}
-		@param logger            the {@link Reporter} to log problems
-		@param maxruntime        limit in millisecs. Ignored if 0
-		'''
-		if settings == None  or connectionfactory == None:
-			raise ValueError("Arguments must be not null");
-		self._settings = settings;
-		self._log = logger;
-		self._protocol = settings.getProtocol(self._log);
-		self._connectionfactory = connectionfactory;
-		self._maxruntime = maxruntime;
-		self._jackson=ObjectMapper()
-
-	def isProperlyStopped(self)->bool:
-		'''
-		@return true if the runner has finished
-		'''
-		return self._properlyStopped
-
-	def run(self):
-		this=self
-		class protocolListener(Listener[ProtocolEvent]):
-			def notifyChange(self, evt: ProtocolEvent):
-				this._handle(evt)
-
-		self._protocol.addListener(protocolListener())
-		self._protocol.start(self._connectionfactory)
-		remainingtime = self._maxruntime;
-		while not self._properlyStopped and  (self._maxruntime == 0 or remainingtime > 0):
-			time.sleep(self._LOOPTIME/1000.)
-			remainingtime -= self._LOOPTIME
-		self._log.log(logging.INFO, "Waiting for connection closure")
-
-		remainingtime = self._FINALWAITTIME;
-		while remainingtime > 0 and\
-			len(self._connectionfactory.getOpenConnections())!=0:
-				time.sleep(self._LOOPTIME/1000.)
-				remainingtime -= self._LOOPTIME
-			
-		openconn  = self._connectionfactory.getOpenConnections()
-		if len(openconn)!=0:
-			self._log.log(logging.WARNING, "Connections " + str(openconn)\
-					+ " did not close properly at end of run")
-		self._log.log(logging.INFO, "end run")
-
-	def _handle(self, evt:ProtocolEvent ):
-		if isinstance(evt , CurrentNegoState) and \
-			evt.getState().isFinal(1000*time.time()):
-				self._stop()
-
-	def _stop(self):
-		self._logFinal(logging.INFO, self._protocol.getState())
-		self._properlyStopped =True
-
-	def _logFinal(self, level:int , state: NegoState):
-		'''
-		Separate so that we can intercept this when mocking, as this will crash
-		on mocks because {@link #jackson} can not handle mocks.
-		
-		@param level the log {@link Level}, eg logging.WARNING
-		@param state the {@link NegoState} to log
-		'''
-		try:
-			self._log.log(level, "protocol ended normally: "
-					+ json.dumps(self._jackson.toJson(self._protocol.getState())))
-		except Exception as e: # catch json issues
-			traceback.print_exc()
-
-	def getProtocol(self) ->NegoProtocol :
-		'''
-		@return protocol that runs/ran the session.
-		'''
-		return self._protocol
-
 	@staticmethod
-	def main( args:List[str]) :
+	def main(args:List[str]):
 		'''
 		The main runner
 		
-		@param args should have 1 argument, the settings.json file to be used.
+		@param args should have 0 or 1 argument. If 0 arguments, the {@link GUI}
+		            is started. If one argoment, it should be a filename
+		            containing a settings.json file. That session is then run.
 		@throws IOException if problem occurs
 		'''
+		if len(args) == 0:
+			GUI.main([])
+			return 
+		
 		if len(args) != 1:
 			NegoRunner.showusage()
@@ -143,8 +67,8 @@
 		
 		serialized = Path(args[0]).read_text("utf-8")
-		settings:NegoSettings  = ObjectMapper().parse(json.loads(serialized),
-						NegoSettings) #type:ignore
+		settings:NegoSettings = ObjectMapper().parse(json.loads(serialized),
+						NegoSettings)  # type:ignore
 
-		runner = NegoRunner(settings,
+		runner = Runner(settings,
 				ClassPathConnectionFactory(), StdOutReporter(), 0)
 		runner.run()
@@ -156,8 +80,7 @@
 		print("The settings.json file should contain the NegoSettings.")
 		print("See the settings.json example file and the GeniusWeb wiki pages. ")
-
 	
 
-if __name__ == '__main__':   
-	NegoRunner.main([sys.argv[1]])
+if __name__ == '__main__': 
+	NegoRunner.main(sys.argv[1:])
 	
Index: geniuswebcore/geniusweb/simplerunner/Runner.py
===================================================================
--- geniuswebcore/geniusweb/simplerunner/Runner.py	(revision 88)
+++ geniuswebcore/geniusweb/simplerunner/Runner.py	(revision 88)
@@ -0,0 +1,122 @@
+import json
+import logging
+from pathlib import Path
+import sys
+import time
+import traceback
+from typing import List, Optional
+
+from pyson.ObjectMapper import ObjectMapper
+from tudelft.utilities.listener.Listener import Listener
+from tudelft_utilities_logging.Reporter import Reporter
+
+from geniusweb.events.ProtocolEvent import ProtocolEvent
+from geniusweb.protocol.CurrentNegoState import CurrentNegoState
+from geniusweb.protocol.NegoProtocol import NegoProtocol
+from geniusweb.protocol.NegoSettings import NegoSettings
+from geniusweb.protocol.NegoState import NegoState
+from geniusweb.simplerunner.ClassPathConnectionFactory import ClassPathConnectionFactory
+
+
+class Runner:
+	'''
+	A simple tool to run a negotiation stand-alone, without starting the servers.
+	All referred files and classes need to be stored locally (or be in the
+	dependency list if you use maven).
+	<p>
+	<em>IMPORTANT</em> SimpleRunner has a number of restrictions, compared to a
+	run using a runserver and partyserver
+	<ul>
+	<li>With stand-alone runner, your parties are run together in a single
+	classloader. The main implication is that there may arise version conflicts
+	between parties.
+	<li>Stand-alone runner does NOT enforce the time deadline. Parties may
+	continue running indefinitely and thus bog down the JVM and stalling
+	tournaments.
+	</ul>
+	'''
+
+	_properlyStopped:bool = False
+	_LOOPTIME = 200  # ms
+	_FINALWAITTIME = 5000  # ms
+
+	def __init__(self, settings:NegoSettings ,
+			connectionfactory:ClassPathConnectionFactory , logger:Reporter ,
+			maxruntime:int):
+		'''
+		@param settings          the {@link NegoSettings}
+		@param connectionfactory the {@link ProtocolToPartyConnFactory}
+		@param logger            the {@link Reporter} to log problems
+		@param maxruntime        limit in millisecs. Ignored if 0
+		'''
+		if settings == None  or connectionfactory == None:
+			raise ValueError("Arguments must be not null");
+		self._settings = settings;
+		self._log = logger;
+		self._protocol = settings.getProtocol(self._log);
+		self._connectionfactory = connectionfactory;
+		self._maxruntime = maxruntime;
+		self._jackson = ObjectMapper()
+
+	def isProperlyStopped(self) -> bool:
+		'''
+		@return true if the runner has finished
+		'''
+		return self._properlyStopped
+
+	def run(self):
+		this = self
+
+		class protocolListener(Listener[ProtocolEvent]):
+
+			def notifyChange(self, evt: ProtocolEvent):
+				this._handle(evt)
+
+		self._protocol.addListener(protocolListener())
+		self._protocol.start(self._connectionfactory)
+		remainingtime = self._maxruntime;
+		while not self._properlyStopped and  (self._maxruntime == 0 or remainingtime > 0):
+			time.sleep(self._LOOPTIME / 1000.)
+			remainingtime -= self._LOOPTIME
+		self._log.log(logging.INFO, "Waiting for connection closure")
+
+		remainingtime = self._FINALWAITTIME;
+		while remainingtime > 0 and\
+			len(self._connectionfactory.getOpenConnections()) != 0:
+				time.sleep(self._LOOPTIME / 1000.)
+				remainingtime -= self._LOOPTIME
+			
+		openconn = self._connectionfactory.getOpenConnections()
+		if len(openconn) != 0:
+			self._log.log(logging.WARNING, "Connections " + str(openconn)\
+					+" did not close properly at end of run")
+		self._log.log(logging.INFO, "end run")
+
+	def _handle(self, evt:ProtocolEvent):
+		if isinstance(evt , CurrentNegoState) and \
+			evt.getState().isFinal(1000 * time.time()):
+				self._stop()
+
+	def _stop(self):
+		self._logFinal(logging.INFO, self._protocol.getState())
+		self._properlyStopped = True
+
+	def _logFinal(self, level:int , state: NegoState):
+		'''
+		Separate so that we can intercept this when mocking, as this will crash
+		on mocks because {@link #jackson} can not handle mocks.
+		
+		@param level the log {@link Level}, eg logging.WARNING
+		@param state the {@link NegoState} to log
+		'''
+		try:
+			self._log.log(level, "protocol ended normally: "
+					+json.dumps(self._jackson.toJson(self._protocol.getState())))
+		except Exception as e:  # catch json issues
+			traceback.print_exc()
+
+	def getProtocol(self) -> NegoProtocol:
+		'''
+		@return protocol that runs/ran the session.
+		'''
+		return self._protocol
Index: geniuswebcore/geniusweb/simplerunner/gui/GUI.py
===================================================================
--- geniuswebcore/geniusweb/simplerunner/gui/GUI.py	(revision 88)
+++ geniuswebcore/geniusweb/simplerunner/gui/GUI.py	(revision 88)
@@ -0,0 +1,87 @@
+import logging
+import sys
+from typing import List, Optional
+
+from PyQt5 import QtCore
+from PyQt5.QtWidgets import QWidget, QTextEdit, QGridLayout, QPushButton, \
+	QApplication, QMainWindow
+from tudelft_utilities_logging.Reporter import Reporter
+
+from geniusweb.simplerunner.ClassPathConnectionFactory import ClassPathConnectionFactory
+from geniusweb.simplerunner.Runner import Runner
+from geniusweb.simplerunner.gui.SettingsPanel import SettingsPanel
+
+
+class GUI (QWidget):
+	'''
+	Simple GUI allowing users to create the settings interactively, much like on
+	the runserver
+	'''
+	# dataChannel needed to pipe output text from the runner
+	# into the PyQt system
+	dataChannel = QtCore.pyqtSignal(str)
+
+	def __init__(self):
+		super().__init__()
+		self.dataChannel.connect(self.receive)
+		self.resultarea = QTextEdit()
+		self.resultarea.setReadOnly(True)
+		self.resultarea.setText("Results will appear here after run")
+		# set dimension , 9, 40
+
+		layout = QGridLayout()
+		self.setLayout(layout)
+	
+		self.settingsPanel = SettingsPanel()
+		layout.addWidget(self.settingsPanel)
+		layout.addWidget(self.startPanel())
+
+	def startPanel(self) -> QWidget:
+		startpanel = QWidget()
+		layout = QGridLayout()
+		startpanel.setLayout(layout)
+		
+		startbutton = QPushButton()
+		startbutton.setText("Start Session")
+		layout.addWidget(startbutton)
+		layout.addWidget(self.resultarea)
+
+		this = self
+		startbutton.clicked.connect(this.runsession)  # type:ignore
+		return startpanel;
+
+	def runsession(self, b):
+		self.resultarea.setText("")
+		this = self
+		
+		class MyReporter(Reporter):
+
+			# override
+			def log(self, level:int, msg:str, thrown:Optional[BaseException]=None):
+				text = "[" + logging._levelToName[level] + "] " + msg + "\n"
+				# in python you can not turn a exception into a string
+				if thrown:
+					text = text + str(thrown) + "\n"
+				this.dataChannel.emit(text)
+
+		myreporter = MyReporter() 
+		runner = Runner(self.settingsPanel.getSettings(),
+			ClassPathConnectionFactory(), myreporter, 0)
+		runner.run()
+
+	def receive(self, text:str):
+		self.resultarea.append(text)
+
+	@staticmethod
+	def main(args:List[str]):
+		app = QApplication([])
+		
+		win = QMainWindow()
+		win.setWindowTitle("GeniusWeb SimpleRunner GUI")
+		win.setCentralWidget(GUI())
+		win.show()
+		sys.exit(app.exec_())
+
+
+if __name__ == '__main__':
+	GUI.main([])
Index: geniuswebcore/geniusweb/simplerunner/gui/MyFileChooser.py
===================================================================
--- geniuswebcore/geniusweb/simplerunner/gui/MyFileChooser.py	(revision 88)
+++ geniuswebcore/geniusweb/simplerunner/gui/MyFileChooser.py	(revision 88)
@@ -0,0 +1,32 @@
+from PyQt5.QtWidgets import QWidgetAction, QTextEdit, QPushButton, QGridLayout, \
+	QFileDialog, QWidget, QLineEdit
+
+
+class MyFileChooser (QWidget):
+	'''
+	one-line panel with on left the chosen file and on the right a "Browse"
+	button. If you click browse button you can browse for a file. You can also
+	just type the file location.
+	'''
+
+	def __init__(self):
+		super().__init__()
+		self.filenamearea = QLineEdit()
+		self.filenamearea.setText("select a file")
+		self.browsebutton = QPushButton()
+		self.browsebutton.setText("Browse")
+
+		layout = QGridLayout()
+		self.setLayout(layout)
+
+		layout.addWidget(self.filenamearea, 0, 0)
+		layout.addWidget(self.browsebutton, 0, 1)
+		self.browsebutton.clicked.connect(self.browseclick)
+
+	def browseclick(self):
+				fc = QFileDialog()
+				if fc.exec_() > 0:  # ? what does this function return?
+					self.filenamearea.setText(fc.selectedFiles()[0])
+
+	def getFile(self) -> str:
+		return self.filenamearea.text();
Index: geniuswebcore/geniusweb/simplerunner/gui/SelectionModel.py
===================================================================
--- geniuswebcore/geniusweb/simplerunner/gui/SelectionModel.py	(revision 88)
+++ geniuswebcore/geniusweb/simplerunner/gui/SelectionModel.py	(revision 88)
@@ -0,0 +1,54 @@
+from typing import List
+
+from PyQt5.QtCore import QAbstractTableModel, QModelIndex, QVariant
+from PyQt5.QtCore import Qt
+
+from geniusweb.protocol.session.TeamInfo import TeamInfo
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+
+
+class SelectionModel(QAbstractTableModel):
+	'''
+	The selected parties, profiles etc
+	'''
+
+	colnames = ["Party", "Parameters", "Profile"]
+
+	def __init__(self):
+		super().__init__()
+		self._teams:List[TeamInfo] = []
+		# pyqt does not use listeners
+
+	def getTeams(self) -> List[TeamInfo]:
+		return self._teams
+
+	def addTeam(self, team:TeamInfo):
+		self._teams.append(team)
+		self.beginResetModel()
+		self.endResetModel()
+
+	# Override
+	def rowCount(self, parent=QModelIndex()):
+		return len(self._teams) 
+
+	# Override
+	def columnCount(self, parent=QModelIndex()):
+		return 3 
+
+	def data(self, index, role=None):
+		if role == Qt.ItemDataRole.DisplayRole:
+			party:PartyWithProfile = self._teams[index.row()].getParties()[0]
+			if index.column() == 0:
+				return str(party.getParty().getPartyRef().getURI())
+			if index.column() == 1:
+				return str(party.getParty().getParameters())
+			if index.column() == 2:
+				return str(party.getProfile().getURI())
+			return "???"
+		return None  # without this you get checkboxes everywhere in table...
+
+	def headerData(self, column:int, orientation, role=Qt.ItemDataRole.DisplayRole):
+		if role != Qt.ItemDataRole.DisplayRole:
+			return QVariant()
+		if orientation == Qt.Orientation.Horizontal:
+			return QVariant(self.colnames[column]) 
Index: geniuswebcore/geniusweb/simplerunner/gui/SettingsPanel.py
===================================================================
--- geniuswebcore/geniusweb/simplerunner/gui/SettingsPanel.py	(revision 88)
+++ geniuswebcore/geniusweb/simplerunner/gui/SettingsPanel.py	(revision 88)
@@ -0,0 +1,157 @@
+import json
+import traceback
+
+from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel, QComboBox, QSpinBox, \
+	QFrame, QSplitter, QLineEdit, QPushButton, QTableView, QScrollArea, QVBoxLayout, \
+	QTextEdit, QMessageBox
+from pyson.ObjectMapper import ObjectMapper
+from uri.uri import URI
+
+from geniusweb.deadline.Deadline import Deadline
+from geniusweb.deadline.DeadlineRounds import DeadlineRounds
+from geniusweb.deadline.DeadlineTime import DeadlineTime
+from geniusweb.protocol.NegoSettings import NegoSettings
+from geniusweb.protocol.session.TeamInfo import TeamInfo
+from geniusweb.protocol.session.saop.SAOPSettings import SAOPSettings
+from geniusweb.references.Parameters import Parameters
+from geniusweb.references.PartyRef import PartyRef
+from geniusweb.references.PartyWithParameters import PartyWithParameters
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+from geniusweb.references.ProfileRef import ProfileRef
+from geniusweb.simplerunner.gui.MyFileChooser import MyFileChooser
+from geniusweb.simplerunner.gui.SelectionModel import SelectionModel
+
+
+class SettingsPanel(QWidget):
+	
+	def __init__(self):
+		super().__init__()
+		self.jackson = ObjectMapper()
+		self.protocolcombo = QComboBox()
+		self.protocolcombo.addItems([ "SAOP" ])
+		self.timepanel = TimePanel()
+		self.nextparty = QLineEdit()
+		self.nextparameters = QLineEdit()
+		
+		self.nextprofile = MyFileChooser()
+		self.addButton = QPushButton()
+		self.addButton.setText("add")
+		self.selectedparties = SelectionModel()
+
+		layout = QGridLayout()
+		self.setLayout(layout)
+		
+		layout.addWidget(WithLabel("protocol", self.protocolcombo))
+		layout.addWidget(WithLabel("deadline", self.timepanel))
+		layout.addWidget(Separator())
+		
+		partic = QLabel()
+		partic.setText("participants")
+		layout.addWidget(partic)
+		
+		layout.addWidget(WithLabel("party:", self.nextparty))
+		layout.addWidget(WithLabel("parameters:", self.nextparameters))
+		layout.addWidget(WithLabel("profile:", self.nextprofile))
+		
+		layout.addWidget(Separator())
+
+		layout.addWidget(self.addButton)
+		
+		label = QLabel()
+		label.setText("Selected Profiles, parties for the session")
+		layout.addWidget(label)
+		
+		table = QTableView()
+		table.setModel(self.selectedparties)
+		layout.addWidget(table)
+		self.addButton.clicked.connect(self.addparty);
+
+	def addparty(self):
+		try:
+			self.addparty1()
+		except ValueError as e:
+			traceback.print_exc()
+			mbox = QMessageBox()
+			mbox.setText(str(e))
+			mbox.setWindowTitle("Failed to add party")
+			mbox.setIcon(QMessageBox.Icon.Warning)
+			mbox.exec_()
+ 
+	def addparty1(self):
+		try:
+			partyref = PartyRef(URI("pythonpath:" + self.nextparty.text()))
+		except Exception as e:
+			raise ValueError("Party not set correctly:" + str(e))
+
+		try:
+			parameters = self.jackson.parse(json.loads("{" + self.nextparameters.text() + "}"),
+					Parameters)
+		except Exception as e:
+			raise ValueError("Parameters not set correctly:" + str(e))
+
+		try:
+			profile = ProfileRef(URI("file:" + self.nextprofile.getFile()))
+		except Exception as  e:
+			raise ValueError("Profile not set correctly:" + str(e))
+
+		partywithparams = PartyWithParameters(partyref, 	parameters);
+		partywithprofile = PartyWithProfile(partywithparams, profile);
+		teaminfo = TeamInfo([partywithprofile])
+		self.selectedparties.addTeam(teaminfo)
+ 
+	def getSettings(self) -> NegoSettings:
+		return SAOPSettings(self.selectedparties.getTeams(),
+				self.timepanel.getDeadline())
+
+
+class WithLabel (QWidget):
+	'''
+	adds label to left of component
+	'''
+
+	def __init__(self, label:str, component:QWidget):
+		super().__init__()
+		layout = QGridLayout()
+		self.setLayout(layout)
+		
+		qlabel = QLabel()
+		qlabel.setText(label)
+
+		layout.addWidget(qlabel, 0, 0)
+		layout.addWidget(component, 0, 1)
+
+
+class TimePanel(QWidget):
+	DEFAULTDEADLINE = 10000  # millisecs
+	
+	def __init__(self):
+		super().__init__()
+		self.unitcombo = QComboBox()
+		self.unitcombo.addItems(["seconds", "rounds"])
+		self.time = QSpinBox()
+		self.time.setMinimum(1)
+		self.time.setMaximum(1000)
+		self.time.setValue(10)
+
+		layout = QGridLayout()
+		self.setLayout(layout)
+
+		layout.addWidget(self.time, 0, 0)
+		layout.addWidget(self.unitcombo, 0, 1)
+
+	def getDeadline(self) -> Deadline:
+		if self.unitcombo.currentIndex() == 0:  # time
+			return DeadlineTime(self.time.value() * 1000);
+		return DeadlineRounds(self.time.value(), self.DEFAULTDEADLINE)
+
+
+class Separator(QFrame):
+	'''
+	Equivalent of java's JSeparator: just a horizontal line for layout
+	'''
+	
+	def __init__(self):
+		super().__init__()
+		self.setFrameShape(QFrame.Shape.HLine)
+		self.setFrameShadow(QFrame.Shadow.Sunken)
+	
Index: geniuswebcore/requirements.txt
===================================================================
--- geniuswebcore/requirements.txt	(revision 87)
+++ geniuswebcore/requirements.txt	(revision 88)
@@ -4,4 +4,5 @@
 https://tracinsy.ewi.tudelft.nl/pubtrac/Utilities/export/226/loggingpy/dist/logging-1.0.0.tar.gz
 websocket-client==1.0.1
+PyQt5==5.15.6
 
 #for testing only
Index: geniuswebcore/setup.py
===================================================================
--- geniuswebcore/setup.py	(revision 87)
+++ geniuswebcore/setup.py	(revision 88)
@@ -1,18 +1,21 @@
 from setuptools import setup, find_packages
+
+ver = '1.2.0'
 
 setup(
     name='geniusweb',
-    version='1.1.4',    
+    version=ver,
     description='GeniusWeb glue code to connect python3 parties',
     url='https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWeb',
     author='W.Pasman',
-    #packages={'src':'*'},
-    packages = find_packages(exclude=["test", "test.*","test.*.*"]),
-    package_data = { 'geniusweb': ['py.typed'],'tudelft': ['py.typed']    },
+    # packages={'src':'*'},
+    packages=find_packages(exclude=["test", "test.*", "test.*.*"]),
+    package_data={ 'geniusweb': ['py.typed'], 'tudelft': ['py.typed']    },
     install_requires=[ 
         "pyson@https://tracinsy.ewi.tudelft.nl/pubtrac/Utilities/export/312/pyson/dist/pyson-1.1.3.tar.gz",
         "utilities@https://tracinsy.ewi.tudelft.nl/pubtrac/Utilities/export/314/utilitiespy/dist/utilities-1.0.5.tar.gz",
         "logging@https://tracinsy.ewi.tudelft.nl/pubtrac/Utilities/export/226/loggingpy/dist/logging-1.0.0.tar.gz",
-        "websocket-client==1.0.1"
+        "websocket-client==1.0.1",
+        "PyQt5==5.15.6"
         ],
 
Index: geniuswebcore/test/geniusweb/opponentmodel/FrequencyOppModelTest.py
===================================================================
--- geniuswebcore/test/geniusweb/opponentmodel/FrequencyOppModelTest.py	(revision 87)
+++ geniuswebcore/test/geniusweb/opponentmodel/FrequencyOppModelTest.py	(revision 88)
@@ -1,23 +1,26 @@
-from unitpy.GeneralTests import GeneralTests
-from geniusweb.opponentmodel.FrequencyOpponentModel import FrequencyOpponentModel
-from geniusweb.issuevalue.DiscreteValue import DiscreteValue
-from unittest.mock import Mock
-from geniusweb.progress.Progress import Progress
-from geniusweb.actions.PartyId import PartyId
-from geniusweb.actions.Offer import Offer
 from decimal import Decimal
 from typing import Dict, List
+import unittest
+from unittest.mock import Mock
+
+from tudelft.utilities.immutablelist.Range import Range
+from unitpy.GeneralTests import GeneralTests
+
+from geniusweb.actions.Offer import Offer
+from geniusweb.actions.Offer import Offer
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.issuevalue.Bid import Bid
+from geniusweb.issuevalue.DiscreteValue import DiscreteValue
+from geniusweb.issuevalue.DiscreteValueSet import DiscreteValueSet
+from geniusweb.issuevalue.Domain import Domain
+from geniusweb.issuevalue.NumberValue import NumberValue
+from geniusweb.issuevalue.NumberValueSet import NumberValueSet
+from geniusweb.issuevalue.Value import Value
 from geniusweb.issuevalue.ValueSet import ValueSet
-from geniusweb.issuevalue.DiscreteValueSet import DiscreteValueSet
-from geniusweb.issuevalue.NumberValueSet import NumberValueSet
-from geniusweb.issuevalue.Domain import Domain
-from geniusweb.issuevalue.Value import Value
-from geniusweb.issuevalue.NumberValue import NumberValue
-from geniusweb.issuevalue.Bid import Bid
-from geniusweb.actions.Offer import Offer
-import unittest
-from tudelft.utilities.immutablelist.Range import Range
+from geniusweb.opponentmodel.FrequencyOpponentModel import FrequencyOpponentModel
+from geniusweb.progress.Progress import Progress
 
-class FrequencyOppModelTest (unittest.TestCase, GeneralTests[FrequencyOpponentModel] ):
+
+class FrequencyOppModelTest (unittest.TestCase, GeneralTests[FrequencyOpponentModel]):
 	ISS1 = "issue1"
 	ISS2 = "issue2";
@@ -30,15 +33,15 @@
 	other = PartyId("other")
 	HALF = Decimal("0.5")
-	#list:List[List[Bid]]  = []
+	# list:List[List[Bid]]  = []
 
 	def setUp(self):
-		issues:Dict[str, ValueSet]  = {}
-		discretevalues1:List[DiscreteValue]  = []
+		issues:Dict[str, ValueSet] = {}
+		discretevalues1:List[DiscreteValue] = []
 		discretevalues1.append(self.I1V1);
 		discretevalues1.append(self.I1V2);
 		values1 = DiscreteValueSet(discretevalues1)
-		issues[self.ISS1]= values1
-		values2 = NumberValueSet(Range(Decimal(0),Decimal(10), Decimal("0.3")))
-		issues[self.ISS2]=values2
+		issues[self.ISS1] = values1
+		values2 = NumberValueSet(Range(Decimal(0), Decimal(10), Decimal("0.3")))
+		issues[self.ISS2] = values2
 		self.domain = Domain("test", issues)
 		self.domain2 = Domain("test2", issues)
@@ -50,21 +53,21 @@
 		discretevalues1.append(self.I1V2b)
 		values1 = DiscreteValueSet(discretevalues1)
-		issues[self.ISS1]= values1
-		values2 = NumberValueSet(Range(Decimal(0), Decimal(10),Decimal("0.3")))
-		issues[self.ISS2]= values2
+		issues[self.ISS1] = values1
+		values2 = NumberValueSet(Range(Decimal(0), Decimal(10), Decimal("0.3")))
+		issues[self.ISS2] = values2
 		self.domain3 = Domain("test", issues)
 
 		# all bids are for domain
-		issuevalues:Dict[str, Value]  = {}
-		issuevalues[self.ISS1]= self.I1V1
-		issuevalues[self.ISS2]=NumberValue(Decimal("1.2"))
+		issuevalues:Dict[str, Value] = {}
+		issuevalues[self.ISS1] = self.I1V1
+		issuevalues[self.ISS2] = NumberValue(Decimal("1.2"))
 		self.bid1 = Bid(issuevalues)
 
-		issuevalues[self.ISS1]= self.I1V1
-		issuevalues[self.ISS2]= NumberValue(Decimal("1.5"))
+		issuevalues[self.ISS1] = self.I1V1
+		issuevalues[self.ISS2] = NumberValue(Decimal("1.5"))
 		self.bid2 = Bid(issuevalues)
 
-		issuevalues[self.ISS1]= self.I1V2
-		issuevalues[self.ISS2]= NumberValue(Decimal("1.5"))
+		issuevalues[self.ISS1] = self.I1V2
+		issuevalues[self.ISS2] = NumberValue(Decimal("1.5"))
 		self.bid3 = Bid(issuevalues)
 
@@ -75,18 +78,18 @@
 		self.oppModel4 = self.oppModel3.WithAction(Offer(self.other, self.bid1), self.progress)
 
-	#Override
-	def getGeneralTestData(self) -> List[List[FrequencyOpponentModel]] :
+	# Override
+	def getGeneralTestData(self) -> List[List[FrequencyOpponentModel]]:
 		return [[self.oppModel1, self.oppModel1b],
-				[self.oppModel2], [self.oppModel3],	[self.oppModel4]]
+				[self.oppModel2], [self.oppModel3], 	[self.oppModel4]]
 
-	#Override
-	def getGeneralTestStrings(self)->List[str] :
+	# Override
+	def getGeneralTestStrings(self) -> List[str]:
 		return ["FrequencyOpponentModel\\[0,\\{issue.=\\{\\}, issue.=\\{\\}\\}\\]",
 				"FrequencyOpponentModel\\[0,\\{issue.=\\{\\}, issue.=\\{\\}\\}\\]",
 				"FrequencyOpponentModel\\[0,\\{issue.=\\{\\}, issue.=\\{\\}\\}\\]",
-				"FrequencyOpponentModel\\[1,\\{issue2=\\{1.2=1\\}, issue1=\\{\"i1v1\"=1\\}\\}\\]"]
+				"FrequencyOpponentModel\\[1,\\{issue.*, issue.*"]
 
-	def testsmokeTestNull(self) :
-		self.assertRaises(ValueError, lambda:FrequencyOpponentModel.create().With(None,None))
+	def testsmokeTestNull(self):
+		self.assertRaises(ValueError, lambda:FrequencyOpponentModel.create().With(None, None))
 
 	def testsmokeTest(self):
@@ -99,5 +102,5 @@
 		
 	def testEmptyModelPartialBid(self):
-		oppModel =FrequencyOpponentModel.create().With(self.domain, None)
+		oppModel = FrequencyOpponentModel.create().With(self.domain, None)
 		bid = Bid({})
 		self.assertEqual(1, oppModel.getUtility(bid))
@@ -105,31 +108,30 @@
 	def testPartialUtility(self):
 		freqs:Dict[str, Dict[Value, int]] = {}
-		freqs1:Dict[Value, int]  = {}
-		freqs2:Dict[Value, int]  = {}
+		freqs1:Dict[Value, int] = {}
+		freqs2:Dict[Value, int] = {}
 
-		freqs1[self.I1V1]= 2
-		freqs1[self.I1V2]= 0
-		freqs2[self.I2V1]= 1
-		freqs2[self.I2V2]= 1
+		freqs1[self.I1V1] = 2
+		freqs1[self.I1V2] = 0
+		freqs2[self.I2V1] = 1
+		freqs2[self.I2V2] = 1
 
-		freqs[self.ISS1]= freqs1
-		freqs[self.ISS2]= freqs2
+		freqs[self.ISS1] = freqs1
+		freqs[self.ISS2] = freqs2
 		total = Decimal(2)
 
-		oppModel = FrequencyOpponentModel(self.domain,	freqs, total,None)
+		oppModel = FrequencyOpponentModel(self.domain, 	freqs, total, None)
 
 		E = 0.0000001
-		self.assertAlmostEqual(0.5, oppModel.getUtility(Bid({self.ISS1: self.I1V1})), None,E)
+		self.assertAlmostEqual(0.5, oppModel.getUtility(Bid({self.ISS1: self.I1V1})), None, E)
 		self.assertAlmostEqual(0, oppModel.getUtility(Bid({self.ISS1: self.I1V2})))
 		self.assertAlmostEqual(0.25,
-				oppModel.getUtility(Bid({self.ISS2:self.I2V1})), None,E)
+				oppModel.getUtility(Bid({self.ISS2:self.I2V1})), None, E)
 		self.assertAlmostEqual(0.25, oppModel.getUtility(Bid({self.ISS2: self.I2V2})))
-
 
 	def testUpdate(self):
 		oppModel = self.oppModel1.WithAction(Offer(self.other, self.bid1),
 				self.progress);
-		self.assertEqual(1, oppModel.getUtility(self.bid1) )
-		self.assertEqual(0,oppModel.getUtility(self.bid3) )
+		self.assertEqual(1, oppModel.getUtility(self.bid1))
+		self.assertEqual(0, oppModel.getUtility(self.bid3))
 		# bid2 has 1 of 2 issue values same as bid1.
 		self.assertTrue(0.5, oppModel.getUtility(self.bid2))
@@ -156,6 +158,6 @@
 
 		values1:Dict[str, Value] = {}
-		values1[self.ISS1]= self.I1V1
-		values1[self.ISS2]= self.I2V1
+		values1[self.ISS1] = self.I1V1
+		values1[self.ISS2] = self.I2V1
 		offer1 = Offer(self.other, Bid(values1))
 
@@ -174,6 +176,6 @@
 
 		values2:Dict[str, Value] = {}
-		values2[self.ISS1]=self.I1V2
-		values2[self.ISS2]=self.I2V2
+		values2[self.ISS1] = self.I1V2
+		values2[self.ISS2] = self.I2V2
 		offer2 = Offer(self.other, Bid(values2))
 
@@ -186,5 +188,4 @@
 		self.assertEqual(1, oppmod.getCounts(self.ISS2)[self.I2V2])
 
-
 	def testStableName(self):
 		name = self.oppModel1.getName()
Index: geniuswebcore/test/geniusweb/simplerunner/BlockingQueueTest.py
===================================================================
--- geniuswebcore/test/geniusweb/simplerunner/BlockingQueueTest.py	(revision 87)
+++ geniuswebcore/test/geniusweb/simplerunner/BlockingQueueTest.py	(revision 88)
@@ -5,8 +5,8 @@
 
 
+class BlockingQueueTest(unittest.TestCase):
 
-class BlockingQueueTest(unittest.TestCase):
     def testSimple(self):
-        q=BlockingQueue[int](4)
+        q = BlockingQueue[int](4)
         self.assertEqual(0, q.size())
         q.put(1)
@@ -14,11 +14,11 @@
         q.put(3)
         self.assertEqual(3, q.size())
-        self.assertEqual(1,q.take())
-        self.assertEqual(2,q.take())
-        self.assertEqual(3,q.take())
+        self.assertEqual(1, q.take())
+        self.assertEqual(2, q.take())
+        self.assertEqual(3, q.take())
         self.assertEqual(0, q.size())
         
     def testThreading(self):
-        q=BlockingQueue[int](4)
+        q = BlockingQueue[int](4)
 
         def pushnumbers():
@@ -27,11 +27,10 @@
                 q.put(n)
         
-        t = Thread(target = pushnumbers)
+        t = Thread(target=pushnumbers)
         t.start()
         
         for n in range(4):
-            val=q.take()
+            val = q.take()
             print(val)
             self.assertEqual(n, val)
         
-        
Index: geniuswebcore/test/geniusweb/simplerunner/SessionRunnerE2ETest.py
===================================================================
--- geniuswebcore/test/geniusweb/simplerunner/SessionRunnerE2ETest.py	(revision 87)
+++ geniuswebcore/test/geniusweb/simplerunner/SessionRunnerE2ETest.py	(revision 88)
@@ -11,4 +11,5 @@
 from geniusweb.simplerunner.ClassPathConnectionFactory import ClassPathConnectionFactory
 from geniusweb.simplerunner.NegoRunner import NegoRunner
+from geniusweb.simplerunner.Runner import Runner
 
 
@@ -23,7 +24,7 @@
 	logger = Mock(ReportToLogger("test"))
 	filename:str
-	runner:NegoRunner
+	runner:Runner
 
-	params = ["test/resources/settings.json", 
+	params = ["test/resources/settings.json",
 # 				 "test/resources/settings2.json" ,          PARTIALORDERING NOT YET
  				 "test/resources/settingsbadprofile.json",
@@ -32,6 +33,4 @@
 # 				 "test/resources/tournament.json"			ALLPERMUTATIONSETTINGS NOT YET
 				 ]
-
-
 	
 	def testAll(self):
@@ -41,8 +40,8 @@
 		'''
 		for filename in self.params:
-			self.filename=filename
-			with self.subTest("before "+self.filename):
+			self.filename = filename
+			with self.subTest("before " + self.filename):
 				self.before()
-			with self.subTest("runMain"+self.filename):
+			with self.subTest("runMain" + self.filename):
 				self.runTestMainFunction()
 
@@ -50,12 +49,12 @@
 		print("Running from file " + self.filename)
 		serialized = Path(self.filename).read_text("utf-8")
-		settings=self.jackson.parse(json.loads(serialized), NegoSettings)
-		self.runner = NegoRunner(settings, ClassPathConnectionFactory(),
-				self.logger,self.TEST_RUNTIME)
+		settings = self.jackson.parse(json.loads(serialized), NegoSettings)
+		self.runner = Runner(settings, ClassPathConnectionFactory(),
+				self.logger, self.TEST_RUNTIME)
 			
-	def runTestMainFunction(self) :
+	def runTestMainFunction(self):
 		NegoRunner.main([self.filename])
 
-	def runTest(self) :
+	def runTest(self):
 		self.runner.run()
 		print("Final state:\n" + str(self.runner.getProtocol().getState()))
