Compare commits

...

7 Commits

Author SHA1 Message Date
Zlatin Balevsky
6508522c9c tunnel quantity and length sliders step 2020-05-29 11:05:58 +01:00
Zlatin Balevsky
f38b8217c2 steps for embedded or external router, formatting 2020-05-29 10:52:58 +01:00
Zlatin Balevsky
c9c5e8617a Directory validation and creation 2020-05-29 01:51:13 +01:00
Zlatin Balevsky
8c4bafda82 move the button enabling logic in view 2020-05-29 01:50:53 +01:00
Zlatin Balevsky
c2044044c0 add a final step 2020-05-29 01:27:36 +01:00
Zlatin Balevsky
cb54b30967 apply steps at the end, add ability to cancel wizard 2020-05-29 01:15:56 +01:00
Zlatin Balevsky
c041f6baaa skeleton of setup wizard 2020-05-28 20:08:57 +01:00
12 changed files with 588 additions and 21 deletions

View File

@@ -141,4 +141,9 @@ mvcGroups {
view = 'com.muwire.gui.SignView'
controller = 'com.muwire.gui.SignController'
}
'wizard' {
model = 'com.muwire.gui.wizard.WizardModel'
view = 'com.muwire.gui.wizard.WizardView'
controller = 'com.muwire.gui.wizard.WizardController'
}
}

View File

@@ -0,0 +1,49 @@
package com.muwire.gui.wizard
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import javax.swing.JOptionPane
@ArtifactProviderFor(GriffonController)
class WizardController {
@MVCMember @Nonnull
WizardModel model
@MVCMember @Nonnull
WizardView view
@ControllerAction
void previous() {
model.currentStep--
view.updateLayout()
}
@ControllerAction
void next() {
def errors = model.steps[model.currentStep].validate()
if (errors != null && !errors.isEmpty()) {
String errorMessage = String.join("\n", errors)
JOptionPane.showMessageDialog(model.parent, errorMessage, "Invalid Input", JOptionPane.ERROR_MESSAGE)
} else {
model.currentStep++
view.updateLayout()
}
}
@ControllerAction
void finish() {
model.steps.each {
it.apply(model.muSettings, model.i2pProps)
}
model.finished['applied'] = true
view.hide()
}
@ControllerAction
void cancel() {
model.finished['applied'] = false
view.hide()
}
}

View File

@@ -54,28 +54,33 @@ class Ready extends AbstractLifecycleHandler {
} else {
log.info("creating new properties")
props = new MuWireSettings()
props.incompleteLocation = new File(home, "incompletes")
props.embeddedRouter = Boolean.parseBoolean(System.getProperties().getProperty("embeddedRouter"))
props.updateType = System.getProperty("updateType","jar")
props.setNickname(selectNickname())
def portableDownloads = System.getProperty("portable.downloads")
if (portableDownloads != null) {
props.downloadLocation = new File(portableDownloads)
} else {
def chooser = new JFileChooser()
chooser.setFileHidingEnabled(false)
chooser.setDialogTitle("Select a directory where downloads will be saved")
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
int rv = chooser.showOpenDialog(null)
if (rv != JFileChooser.APPROVE_OPTION) {
JOptionPane.showMessageDialog(null, "MuWire will now exit")
System.exit(0)
}
props.downloadLocation = chooser.getSelectedFile()
boolean embeddedRouterAvailable = Boolean.parseBoolean(System.getProperties().getProperty("embeddedRouter"))
def parent = application.windowManager.findWindow("event-list")
Properties i2pProps = new Properties()
def params = [:]
params['parent'] = parent
params['embeddedRouterAvailable'] = embeddedRouterAvailable
params['muSettings'] = props
params['i2pProps'] = i2pProps
def finished = [:]
params['finished'] = finished
application.mvcGroupManager.createMVCGroup("wizard", params)
if (!finished['applied']) {
JOptionPane.showMessageDialog(parent, "MuWire will now exit")
System.exit(0)
}
File i2pPropsFile = new File(home, "i2p.properties")
i2pPropsFile.withPrintWriter { i2pProps.store(it, "") }
props.embeddedRouter = embeddedRouterAvailable
props.updateType = System.getProperty("updateType","jar")
propsFile.withPrintWriter("UTF-8", {
props.write(it)
})

View File

@@ -0,0 +1,42 @@
package com.muwire.gui.wizard
import java.awt.Component
import com.muwire.core.MuWireSettings
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class WizardModel {
Component parent
boolean embeddedRouterAvailable
MuWireSettings muSettings
Properties i2pProps
def finished
final List<WizardStep> steps = []
int currentStep
@Observable boolean finishButtonEnabled
@Observable boolean previousButtonEnabled
@Observable boolean nextButtonEnabled
void mvcGroupInit(Map<String,String> args) {
steps << new NicknameStep()
steps << new DirectoriesStep()
if (embeddedRouterAvailable)
steps << new EmbeddedRouterStep()
else
steps << new ExternalRouterStep()
steps << new TunnelStep()
steps << new LastStep(embeddedRouterAvailable)
currentStep = 0
previousButtonEnabled = false
nextButtonEnabled = steps.size() > (currentStep + 1)
finishButtonEnabled = steps.size() == currentStep + 1
}
}

View File

@@ -0,0 +1,78 @@
package com.muwire.gui.wizard
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.SwingConstants
import java.awt.BorderLayout
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class WizardView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
WizardModel model
def dialog
def p
void initUI() {
dialog = new JDialog(model.parent, "Setup Wizard", true)
p = builder.panel {
borderLayout()
panel (id : "cards-panel", constraints : BorderLayout.CENTER) {
cardLayout()
model.steps.each {
it.buildUI(builder)
}
}
panel (constraints : BorderLayout.SOUTH) {
gridLayout(rows:1, cols:2)
panel {
button(text : "Cancel", cancelAction)
}
panel {
button(text : "Previous", enabled : bind {model.previousButtonEnabled}, previousAction)
button(text : "Next", enabled : bind {model.nextButtonEnabled}, nextAction)
button(text : "Finish", enabled : bind {model.finishButtonEnabled}, finishAction)
}
}
}
}
void updateLayout() {
model.previousButtonEnabled = model.currentStep > 0
model.nextButtonEnabled = model.steps.size() > (model.currentStep + 1)
model.finishButtonEnabled = model.steps.size() == (model.currentStep + 1)
String constraints = model.steps[model.currentStep].getConstraint()
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel, constraints)
}
void mvcGroupInit(Map<String,String> args) {
dialog.getContentPane().add(p)
dialog.pack()
dialog.setLocationRelativeTo(model.parent)
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
dialog.addWindowListener( new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
dialog.show()
}
void hide() {
dialog.setVisible(false)
mvcGroup.destroy()
}
}

View File

@@ -0,0 +1,92 @@
package com.muwire.gui.wizard
import java.awt.GridBagConstraints
import javax.swing.JFileChooser
import com.muwire.core.MuWireSettings
class DirectoriesStep extends WizardStep {
def downloadLocationField
def incompleteLocationField
def downloadLocationButton
def incompleteLocationButton
public DirectoriesStep(String constraint) {
super("directories")
}
@Override
protected void buildUI(FactoryBuilderSupport builder) {
File defaultDownloadLocation = new File(System.getProperty("user.home"), "MuWire Downloads")
File defaultIncompleteLocation = new File(System.getProperty("user.home"), "MuWire Incompletes")
builder.panel(constraints : getConstraint()) {
gridBagLayout()
label(text : "Select directories for saving downloaded and incomplete files.",
constraints : gbc(gridx: 0, gridy: 0, gridwidth : 2, insets: [10,0,0,0]))
label(text : "They will be created if they do not already exist.",
constraints : gbc(gridx:0, gridy: 1, gridwidth: 2, insets: [0,0,10,0]))
label(text : "Directory for saving downloaded files", constraints : gbc(gridx:0, gridy: 2))
downloadLocationField = textField(text : defaultDownloadLocation.getAbsolutePath(),
constraints : gbc(gridx : 0, gridy : 3, fill : GridBagConstraints.HORIZONTAL, weightx: 100))
downloadLocationButton = button(text : "Choose", constraints : gbc(gridx: 1, gridy: 3), actionPerformed : showDownloadChooser)
label(text : "Directory for storing incomplete files", constraints : gbc(gridx:0, gridy: 4))
incompleteLocationField = textField(text : defaultIncompleteLocation.getAbsolutePath(),
constraints : gbc(gridx:0, gridy:5, fill : GridBagConstraints.HORIZONTAL, weightx: 100))
incompleteLocationButton = button(text : "Choose", constraints : gbc(gridx: 1, gridy: 5), actionPerformed : showIncompleteChooser)
}
}
@Override
protected List<String> validate() {
def rv = []
if (!canWrite(downloadLocationField.text))
rv << "Download location not writeable"
if (!canWrite(incompleteLocationField.text))
rv << "Incomplete location not writeable"
rv
}
private static boolean canWrite(String location) {
File f = new File(location)
if (f.exists())
return f.isDirectory() && f.canWrite()
canWrite(f.getParentFile().getAbsolutePath())
}
@Override
protected void apply(MuWireSettings muSettings, Properties i2pSettings) {
muSettings.downloadLocation = new File(downloadLocationField.text)
muSettings.incompleteLocation = new File(incompleteLocationField.text)
muSettings.downloadLocation.mkdirs()
muSettings.incompleteLocation.mkdirs()
}
def showDownloadChooser = {
String text = chooseFile("Select directory for downloaded files")
if (text != null)
downloadLocationField.text = text
}
def showIncompleteChooser = {
String text = chooseFile("Select directory for incomplete files")
if (text != null)
incompleteLocationField.text = text
}
private String chooseFile(String title) {
def chooser = new JFileChooser()
chooser.setFileHidingEnabled(false)
chooser.setDialogTitle(title)
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION)
return chooser.getSelectedFile().getAbsolutePath()
else
return null
}
}

View File

@@ -0,0 +1,82 @@
package com.muwire.gui.wizard
import java.awt.GridBagConstraints
import javax.swing.border.TitledBorder
import com.muwire.core.MuWireSettings
class EmbeddedRouterStep extends WizardStep {
def udpPortField
def tcpPortField
def inBwField
def outBwField
public EmbeddedRouterStep() {
super("router")
}
@Override
protected void buildUI(FactoryBuilderSupport builder) {
Random r = new Random()
int port = 9151 + r.nextInt(1 + 30777 - 9151) // this range matches what the i2p router would choose
builder.panel(constraints : getConstraint()) {
gridBagLayout()
panel(border : titledBorder(title : "Port Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
constraints : gbc(gridx: 0, gridy : 0, fill : GridBagConstraints.HORIZONTAL, weightx: 100))) {
gridBagLayout()
label(text : "TCP port", constraints : gbc(gridx :0, gridy: 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
tcpPortField = textField(text : String.valueOf(port), columns : 4, constraints : gbc(gridx:1, gridy:0, anchor : GridBagConstraints.LINE_END))
label(text : "UDP port", constraints : gbc(gridx :0, gridy: 1, anchor : GridBagConstraints.LINE_START, weightx : 100))
udpPortField = textField(text : String.valueOf(port), columns : 4, constraints : gbc(gridx:1, gridy:1, anchor : GridBagConstraints.LINE_END))
}
panel( border : titledBorder(title : "Bandwidth Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
constraints : gbc(gridx : 0, gridy : 1, fill : GridBagConstraints.HORIZONTAL, weightx : 100)) {
gridBagLayout()
label(text : "Inbound bandwidth (KB)", constraints : gbc(gridx: 0, gridy : 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
inBwField = textField(text : "512", columns : 3, constraints : gbc(gridx : 1, gridy : 0, anchor : GridBagConstraints.LINE_END))
label(text : "Outbound bandwidth (KB)", constraints : gbc(gridx: 0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx : 100))
outBwField = textField(text : "256", columns : 3, constraints : gbc(gridx : 1, gridy : 1, anchor : GridBagConstraints.LINE_END))
}
panel (constraints : gbc(gridx: 0, gridy : 2, weighty: 100))
}
}
@Override
protected List<String> validate() {
def rv = []
try {
int udpPort = Integer.parseInt(udpPortField.text)
int tcpPort = Integer.parseInt(tcpPortField.text)
if (udpPort <= 0 || udpPort > 0xFFFF)
rv << "Invalid UDP Port"
if (tcpPort <= 0 || tcpPort > 0xFFFF)
rv << "Invalid TCP Port"
} catch (NumberFormatException e) {
rv << "Invalid port"
}
try {
int outBw = Integer.parseInt(outBwField.text)
int inBw = Integer.parseInt(inBwField.text)
if (outBw <= 0)
rv << "Out bandwidth cannot be negative"
if (inBw <= 0)
rv << "In bandwidth cannot be ngative"
} catch (NumberFormatException e) {
rv << "Invalid bandwidth"
}
rv
}
@Override
protected void apply(MuWireSettings muSettings, Properties i2pSettings) {
i2pSettings['i2np.ntcp.port'] = tcpPortField.text
i2pSettings['i2np.udp.port'] = udpPortField.text
muSettings.outBw = Integer.parseInt(outBwField.text)
muSettings.inBw = Integer.parseInt(inBwField.text)
}
}

View File

@@ -0,0 +1,59 @@
package com.muwire.gui.wizard
import java.awt.GridBagConstraints
import javax.swing.border.TitledBorder
import com.muwire.core.MuWireSettings
class ExternalRouterStep extends WizardStep {
def addressField
def portField
public ExternalRouterStep() {
super("router")
}
@Override
protected void buildUI(FactoryBuilderSupport builder) {
builder.panel(constraints : getConstraint()) {
gridBagLayout()
panel(border : titledBorder(title : "External Router I2CP Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
constraints : gbc(gridx: 0, gridy : 0, fill : GridBagConstraints.HORIZONTAL, weightx: 100))) {
gridBagLayout()
label(text : "Host", constraints : gbc(gridx: 0, gridy : 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
addressField = textField(text : "127.0.0.1", constraints : gbc(gridx:1, gridy:0, anchor: GridBagConstraints.LINE_END))
label(text : "Port", constraints : gbc(gridx: 0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx : 100))
portField = textField(text : "7654", constraints : gbc(gridx:1, gridy:1, anchor: GridBagConstraints.LINE_END))
}
panel(constraints : gbc(gridx:0, gridy:1, weighty: 100))
}
}
@Override
protected List<String> validate() {
def rv = []
try {
InetAddress.getAllByName(addressField.text)
} catch (UnknownHostException iox) {
rv << "Not a valid InetAddress"
}
try {
int port = Integer.parseInt(portField.text)
if (port <= 0 && port > 0xFFFF)
rv << "Not a valid port"
} catch (NumberFormatException e) {
rv << "Not a valid port"
}
rv
}
@Override
protected void apply(MuWireSettings muSettings, Properties i2pSettings) {
i2pSettings['i2cp.tcp.host'] = addressField.text
i2pSettings['i2cp.tcp.port'] = portField.text
}
}

View File

@@ -0,0 +1,32 @@
package com.muwire.gui.wizard
import com.muwire.core.MuWireSettings
class LastStep extends WizardStep {
private final boolean embeddedRouterAvailable
public LastStep(boolean embeddedRouterAvailable) {
super("last")
this.embeddedRouterAvailable = embeddedRouterAvailable
}
@Override
protected void buildUI(FactoryBuilderSupport builder) {
builder.panel(constraints: getConstraint()) {
gridBagLayout()
label(text: "The wizard is complete. Press \"Finish\" to launch MuWire.", constraints : gbc(gridx: 0, gridy: 0))
if (embeddedRouterAvailable)
label(text : "MuWire will launch an embedded I2P router. This can take a few minutes.", constraints: gbc(gridx:0, gridy:1))
}
}
@Override
protected List<String> validate() {
return null
}
@Override
protected void apply(MuWireSettings muSettings, Properties i2pSettings) {
}
}

View File

@@ -0,0 +1,40 @@
package com.muwire.gui.wizard
import com.muwire.core.Constants
import com.muwire.core.MuWireSettings
import com.muwire.core.util.DataUtil
class NicknameStep extends WizardStep {
volatile def nickField
public NicknameStep() {
super("nickname")
}
@Override
protected void buildUI(FactoryBuilderSupport builder) {
builder.panel(constraints : getConstraint()) {
label(text: "Select a nickname")
nickField = textField(columns: 30)
}
}
@Override
protected List<String> validate() {
String nickname = nickField.text
if (nickname == null)
return ['Please select a nickname']
nickname = nickname.trim()
if (nickname.length() == 0)
return ['Nickname cannot be blank']
if (!DataUtil.isValidName(nickname))
return ["Nickname cannot contain any of ${Constants.INVALID_NICKNAME_CHARS} and must be no longer than ${Constants.MAX_NICKNAME_LENGTH} characters. Choose another."]
null
}
@Override
protected void apply(MuWireSettings muSettings, Properties i2pSettings) {
muSettings.nickname = nickField.text.trim()
}
}

View File

@@ -0,0 +1,61 @@
package com.muwire.gui.wizard
import java.awt.GridBagConstraints
import javax.swing.JLabel
import javax.swing.border.TitledBorder
import com.muwire.core.MuWireSettings
class TunnelStep extends WizardStep {
def tunnelLengthSlider
def tunnelQuantitySlider
public TunnelStep() {
super("tunnels")
}
@Override
protected void buildUI(FactoryBuilderSupport builder) {
builder.panel (constraints : getConstraint()) {
gridBagLayout()
panel (border : titledBorder(title : "Speed vs. Anonymity", border : etchedBorder(), titlePosition: TitledBorder.TOP,
constraints : gbc(gridx: 0, gridy: 0, fill : GridBagConstraints.HORIZONTAL, weightx : 100))) {
def lengthTable = new Hashtable()
lengthTable.put(1, new JLabel("Max Speed"))
lengthTable.put(3, new JLabel("Max Anonymity"))
tunnelLengthSlider = slider(minimum : 1, maximum : 3, value : 3,
majorTickSpacing : 1, snapToTicks: true, paintTicks: true, labelTable : lengthTable,
paintLabels : true)
}
panel (border : titledBorder(title : "Reliability vs. Resource Usage", border : etchedBorder(), titlePosition: TitledBorder.TOP,
constraints : gbc(gridx: 0, gridy: 1, fill : GridBagConstraints.HORIZONTAL, weightx : 100))) {
def quantityTable = new Hashtable()
quantityTable.put(1, new JLabel("Min Resources"))
quantityTable.put(6, new JLabel("Max Reliability"))
tunnelQuantitySlider = slider(minimum : 1, maximum : 6, value : 4,
majorTickSpacing : 1, snapToTicks : true, paintTicks: true, labelTable : quantityTable,
paintLabels : true)
}
panel(constraints : gbc(gridx:0, gridy: 2, weighty: 100))
}
}
@Override
protected List<String> validate() {
return null
}
@Override
protected void apply(MuWireSettings muSettings, Properties i2pSettings) {
String tunnelLength = String.valueOf(tunnelLengthSlider.value)
i2pSettings['inbound.length'] = tunnelLength
i2pSettings['outbound.length'] = tunnelLength
String tunnelQuantity = tunnelQuantitySlider.value
i2pSettings['inbound.quantity'] = tunnelQuantity
i2pSettings['outbound.quantity'] = tunnelQuantity
}
}

View File

@@ -0,0 +1,22 @@
package com.muwire.gui.wizard
import com.muwire.core.MuWireSettings
abstract class WizardStep {
final String constraint
protected WizardStep(String constraint) {
this.constraint = constraint
}
protected abstract void buildUI(FactoryBuilderSupport builder)
/**
* @return list of errors, null if validation is successful
*/
protected abstract List<String> validate()
protected abstract void apply(MuWireSettings muSettings, Properties i2pSettings)
}