From 015bfaa0070a088fe8a644e224cd4868e5720648 Mon Sep 17 00:00:00 2001 From: Dmitry Barashev Date: Fri, 17 Jul 2026 03:11:22 +0400 Subject: [PATCH 1/5] Fixed double-activation of the barrier --- .../java/net/sourceforge/ganttproject/GanttProjectImpl.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ganttproject/src/main/java/net/sourceforge/ganttproject/GanttProjectImpl.kt b/ganttproject/src/main/java/net/sourceforge/ganttproject/GanttProjectImpl.kt index c8794ca33..f4558b043 100644 --- a/ganttproject/src/main/java/net/sourceforge/ganttproject/GanttProjectImpl.kt +++ b/ganttproject/src/main/java/net/sourceforge/ganttproject/GanttProjectImpl.kt @@ -145,7 +145,7 @@ open class GanttProjectImpl( } protected open fun fireProjectOpened() { - val barrier = TwoPhaseBarrierImpl("Project opened", this) + val barrier = TwoPhaseBarrierImpl("Project opened") for (l in listeners) { l.projectOpened(barrier, barrier) } @@ -319,4 +319,4 @@ internal fun createProjectModificationListener(project: IGanttProject, uiFacade: timerBarrier.isPaused = false } } -} \ No newline at end of file +} From 835960b4fdd3cf20f0566def5ed7a0574dc19dd3 Mon Sep 17 00:00:00 2001 From: Dmitry Barashev Date: Fri, 17 Jul 2026 03:12:17 +0400 Subject: [PATCH 2/5] Fixed issue with the spanning labels and controls that occupy only one column --- .../src/main/java/biz/ganttproject/app/PropertySheet.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ganttproject/src/main/java/biz/ganttproject/app/PropertySheet.kt b/ganttproject/src/main/java/biz/ganttproject/app/PropertySheet.kt index 8c3260e9e..29afd0084 100644 --- a/ganttproject/src/main/java/biz/ganttproject/app/PropertySheet.kt +++ b/ganttproject/src/main/java/biz/ganttproject/app/PropertySheet.kt @@ -489,7 +489,7 @@ class PropertyPaneBuilderImpl(private val localizer: Localizer, private val grid GridPane.setHalignment(label, options?.labelHAlignment ?: HPos.RIGHT) } LabelPosition.ABOVE -> { - grid.add(label, 0, idx, 2, 1) + grid.add(label, 0, idx, grid.columnCount, 1) GridPane.setHgrow(label, Priority.NEVER) GridPane.setHalignment(label, options?.labelHAlignment ?: HPos.LEFT) resultRow++ @@ -521,7 +521,7 @@ class PropertyPaneBuilderImpl(private val localizer: Localizer, private val grid GridPane.setHgrow(hbox, Priority.SOMETIMES) } LabelPosition.ABOVE -> { - grid.add(hbox, 0, resultRow, 2, 1) + grid.add(hbox, 0, resultRow, grid.columnCount, 1) GridPane.setHgrow(hbox, Priority.ALWAYS) } } From 4414905d17eb48b28614eede3e357353da292209 Mon Sep 17 00:00:00 2001 From: Dmitry Barashev Date: Fri, 17 Jul 2026 03:19:49 +0400 Subject: [PATCH 3/5] UI for pasting a token --- biz.ganttproject.app.localization | 2 +- .../storage/cloud/GPCloudHttpImpl.kt | 2 + .../ganttproject/storage/cloud/SigninPane.kt | 101 +++++++++++------- .../storage/cloud/GPCloudSignupPane.scss} | 16 ++- 4 files changed, 80 insertions(+), 41 deletions(-) rename ganttproject/src/main/{java/biz/ganttproject/storage/cloud/GPCloudSignupPane.css => sass/biz/ganttproject/storage/cloud/GPCloudSignupPane.scss} (81%) diff --git a/biz.ganttproject.app.localization b/biz.ganttproject.app.localization index b54143b8b..25b96efc7 160000 --- a/biz.ganttproject.app.localization +++ b/biz.ganttproject.app.localization @@ -1 +1 @@ -Subproject commit b54143b8bbba27e00911bd7f57a25d52ed357d6f +Subproject commit 25b96efc7f926c964156e139a8fb42af363411de diff --git a/ganttproject/src/main/java/biz/ganttproject/storage/cloud/GPCloudHttpImpl.kt b/ganttproject/src/main/java/biz/ganttproject/storage/cloud/GPCloudHttpImpl.kt index 4bd99b090..dd5460db5 100644 --- a/ganttproject/src/main/java/biz/ganttproject/storage/cloud/GPCloudHttpImpl.kt +++ b/ganttproject/src/main/java/biz/ganttproject/storage/cloud/GPCloudHttpImpl.kt @@ -446,6 +446,7 @@ typealias AuthStartCallback = ()->Unit class HttpServerImpl : NanoHTTPD("localhost", 0) { var onTokenReceived: AuthTokenCallback? = null + var onAuthReceived: (() -> Unit)? = null var onStart: AuthStartCallback? = null private fun getParam(session: IHTTPSession, key: String): String? { @@ -465,6 +466,7 @@ class HttpServerImpl : NanoHTTPD("localhost", 0) { LOG.debug("Received Auth Token:{} validity:{}", token, validity) onTokenReceived?.invoke(token, validity, userId, websocketToken) + onAuthReceived?.invoke() newFixedLengthResponse("").apply { addHeader("Access-Control-Allow-Origin", GPCLOUD_ORIGIN) } diff --git a/ganttproject/src/main/java/biz/ganttproject/storage/cloud/SigninPane.kt b/ganttproject/src/main/java/biz/ganttproject/storage/cloud/SigninPane.kt index a5017c538..c61e5266e 100644 --- a/ganttproject/src/main/java/biz/ganttproject/storage/cloud/SigninPane.kt +++ b/ganttproject/src/main/java/biz/ganttproject/storage/cloud/SigninPane.kt @@ -19,33 +19,26 @@ along with GanttProject. If not, see . package biz.ganttproject.storage.cloud import biz.ganttproject.FXUtil -import biz.ganttproject.app.DIALOG_STYLESHEET -import biz.ganttproject.app.RootLocalizer -import biz.ganttproject.app.Spinner +import biz.ganttproject.app.* +import biz.ganttproject.core.option.LabelPosition +import biz.ganttproject.core.option.ObservableString import biz.ganttproject.lib.fx.VBoxBuilder import biz.ganttproject.lib.fx.isBrowseSupported import biz.ganttproject.lib.fx.openInBrowser -import biz.ganttproject.lib.fx.vbox import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView import javafx.beans.property.SimpleStringProperty import javafx.event.ActionEvent -import javafx.event.EventHandler +import javafx.geometry.HPos import javafx.geometry.Pos -import javafx.scene.Node import javafx.scene.control.Button import javafx.scene.control.ContentDisplay import javafx.scene.control.Label -import javafx.scene.control.TextField import javafx.scene.input.Clipboard import javafx.scene.input.ClipboardContent import javafx.scene.layout.BorderPane import javafx.scene.layout.Pane import javafx.scene.layout.Priority -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.javafx.JavaFx -import kotlinx.coroutines.launch import java.util.* import kotlin.concurrent.schedule @@ -59,6 +52,10 @@ import kotlin.concurrent.schedule * @author dbarashev@bardsoftware.com */ class SigninPane() : FlowPage() { + companion object { + private const val SIGNIN_TIMEOUT_SECONDS = 60 + } + enum class Status { INIT, WAITING_FOR_BROWSER, WAITING_FOR_AUTH, AUTH_COMPLETED } @@ -71,9 +68,11 @@ class SigninPane() : FlowPage() { private val indicatorPane = BorderPane().apply { styleClass.add("indicator-pane") } + val urlOption = ObservableString("foo1", "") + val tokenOption = ObservableString("foo2", "") private fun onStartCallback() { - GlobalScope.launch(Dispatchers.JavaFx) { + FXThread.runLater { status = Status.WAITING_FOR_AUTH spinner.state = Spinner.State.ATTENTION @@ -82,7 +81,7 @@ class SigninPane() : FlowPage() { } fun createSigninPane(): Pane { - val uri = "$GPCLOUD_SIGNIN_URL?callback=${controller.httpd.listeningPort}" + val uri = "$GPCLOUD_SIGNIN_URL?callback=${controller.httpd.listeningPort}&timeout=$SIGNIN_TIMEOUT_SECONDS" val vboxBuilder = VBoxBuilder("signin-pane", "pane-service-contents") vboxBuilder.addTitle(ourLocalizer.formatText("title")).also { @@ -99,24 +98,51 @@ class SigninPane() : FlowPage() { spinner.state = Spinner.State.WAITING statusText.value = ourLocalizer.formatText("text.browser_opening") + val copyButton = Button(ourLocalizer.formatText("button.copyLink"), FontAwesomeIconView(FontAwesomeIcon.COPY)).apply { + contentDisplay = ContentDisplay.RIGHT + addEventHandler(ActionEvent.ACTION) { + Clipboard.getSystemClipboard().setContent(ClipboardContent().apply { + putString(uri) + }) + } + } + + urlOption.value = uri + urlOption.isWritable.value = false + val submitButton = Button(ourLocalizer.formatText("button.submitToken")).apply { + addEventHandler(ActionEvent.ACTION) { submitToken(tokenOption.value ?: "") } + } + + val tokenPane = properties(ourLocalizer) { + text(urlOption) { + labelText = ourLocalizer.formatText("text.browser_failed") + labelPosition = LabelPosition.ABOVE + labelHAlignment = HPos.LEFT + rightNode = copyButton + } + text(tokenOption) { + labelText = ourLocalizer.formatText("label.pasteToken") + labelHAlignment = HPos.LEFT + labelPosition = LabelPosition.ABOVE + rightNode = submitButton + } + } + vboxBuilder.add(tokenPane, Pos.CENTER, Priority.NEVER) + vboxBuilder.vbox.let { it.stylesheets.addAll( DIALOG_STYLESHEET, "/biz/ganttproject/app/Util.css", "biz/ganttproject/storage/StorageDialog.css", - "/biz/ganttproject/storage/cloud/GPCloudSignupPane.css" + "biz/ganttproject/storage/cloud/GPCloudSignupPane.css" ) } - GlobalScope.launch(Dispatchers.IO) { + FXThread.runLater { if (isBrowseSupported()) { status = Status.WAITING_FOR_BROWSER openInBrowser(uri.trim()) startBrowserTimeout(uri) - } else { - GlobalScope.launch(Dispatchers.JavaFx) { - FXUtil.transitionCenterPane(indicatorPane, createUrlPane(uri)) {} - } } } return vboxBuilder.vbox @@ -125,33 +151,25 @@ class SigninPane() : FlowPage() { private fun startBrowserTimeout(uri: String) { Timer().schedule(60000) { if (status == Status.WAITING_FOR_BROWSER) { - GlobalScope.launch(Dispatchers.JavaFx) { - FXUtil.transitionCenterPane(indicatorPane, createUrlPane(uri), {}) + FXThread.runLater { statusText.value = ourLocalizer.formatText("text.browser_failed") } } } } - private fun createUrlPane(uri: String): Node { - return vbox { - i18n = ourLocalizer - vbox.spacing = 5.0 - add(TextField().apply { - text = uri - isEditable = false - onMouseClicked = EventHandler { this.selectAll() } - }, Pos.CENTER, Priority.NEVER) - add(Button(i18n.formatText("button.copyLink"), FontAwesomeIconView(FontAwesomeIcon.COPY)).apply { - contentDisplay = ContentDisplay.RIGHT - styleClass.addAll("btn-attention") - addEventHandler(ActionEvent.ACTION) { - Clipboard.getSystemClipboard().setContent(ClipboardContent().apply { - putString(uri) - }) - } - }, Pos.CENTER, Priority.NEVER) + private fun submitToken(rawInput: String) { + if (rawInput.isBlank()) return + val params = rawInput.split("&").associate { + val parts = it.split("=", limit = 2) + if (parts.size == 2) parts[0] to parts[1] else parts[0] to "" } + controller.httpd.onTokenReceived?.invoke( + params["token"], + params["validity"], + params["userId"], + params["websocketToken"] + ) } override fun createUi(): Pane = createSigninPane() @@ -161,6 +179,11 @@ class SigninPane() : FlowPage() { override fun setController(controller: GPCloudUiFlow) { this.controller = controller controller.httpd.onStart = ::onStartCallback + controller.httpd.onAuthReceived = { + FXUtil.runLater { + tokenOption.isWritable.value = false + } + } } } diff --git a/ganttproject/src/main/java/biz/ganttproject/storage/cloud/GPCloudSignupPane.css b/ganttproject/src/main/sass/biz/ganttproject/storage/cloud/GPCloudSignupPane.scss similarity index 81% rename from ganttproject/src/main/java/biz/ganttproject/storage/cloud/GPCloudSignupPane.css rename to ganttproject/src/main/sass/biz/ganttproject/storage/cloud/GPCloudSignupPane.scss index e82c23de8..36cd33c09 100644 --- a/ganttproject/src/main/java/biz/ganttproject/storage/cloud/GPCloudSignupPane.css +++ b/ganttproject/src/main/sass/biz/ganttproject/storage/cloud/GPCloudSignupPane.scss @@ -29,8 +29,22 @@ /* -fx-fill: white;*/ /* -fx-text-fill: white;*/ /*}*/ +@import "../../app/textfields"; +@import "../../app/theme"; - +.signin-pane { + .property-pane { + HBox { + -fx-padding: 0 0 15 0; + -fx-border-color: red; + -fx-border-width: 0px; + } + -fx-padding: 30 10 0 10; + .custom-text-field { + @include textfield-with-button($gp-light-gray, $gp-dark-gray); + } + } +} .signup-pane { -fx-padding: 20px; -fx-background-color: transparent; From c08212ee120174810df98881f37ddccae675a631 Mon Sep 17 00:00:00 2001 From: Dmitry Barashev Date: Fri, 17 Jul 2026 12:25:58 +0400 Subject: [PATCH 4/5] Code review round --- .../ganttproject/storage/cloud/SigninPane.kt | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/ganttproject/src/main/java/biz/ganttproject/storage/cloud/SigninPane.kt b/ganttproject/src/main/java/biz/ganttproject/storage/cloud/SigninPane.kt index c61e5266e..187cf3977 100644 --- a/ganttproject/src/main/java/biz/ganttproject/storage/cloud/SigninPane.kt +++ b/ganttproject/src/main/java/biz/ganttproject/storage/cloud/SigninPane.kt @@ -22,6 +22,7 @@ import biz.ganttproject.FXUtil import biz.ganttproject.app.* import biz.ganttproject.core.option.LabelPosition import biz.ganttproject.core.option.ObservableString +import biz.ganttproject.core.option.ValidationException import biz.ganttproject.lib.fx.VBoxBuilder import biz.ganttproject.lib.fx.isBrowseSupported import biz.ganttproject.lib.fx.openInBrowser @@ -39,6 +40,7 @@ import javafx.scene.input.ClipboardContent import javafx.scene.layout.BorderPane import javafx.scene.layout.Pane import javafx.scene.layout.Priority +import java.net.URLDecoder import java.util.* import kotlin.concurrent.schedule @@ -51,7 +53,7 @@ import kotlin.concurrent.schedule * * @author dbarashev@bardsoftware.com */ -class SigninPane() : FlowPage() { +class SigninPane : FlowPage() { companion object { private const val SIGNIN_TIMEOUT_SECONDS = 60 } @@ -68,8 +70,8 @@ class SigninPane() : FlowPage() { private val indicatorPane = BorderPane().apply { styleClass.add("indicator-pane") } - val urlOption = ObservableString("foo1", "") - val tokenOption = ObservableString("foo2", "") + private val urlOption = ObservableString("foo1", "") + private val tokenOption = ObservableString("foo2", "", validator = this::validateTokenString) private fun onStartCallback() { FXThread.runLater { @@ -159,19 +161,33 @@ class SigninPane() : FlowPage() { } private fun submitToken(rawInput: String) { - if (rawInput.isBlank()) return - val params = rawInput.split("&").associate { - val parts = it.split("=", limit = 2) - if (parts.size == 2) parts[0] to parts[1] else parts[0] to "" - } + val params = parseTokenString(rawInput) controller.httpd.onTokenReceived?.invoke( params["token"], params["validity"], params["userId"], params["websocketToken"] ) + controller.httpd.onAuthReceived?.invoke() } + private fun parseTokenString(rawInput: String): Map = + if (rawInput.isBlank()) emptyMap() + else rawInput.split("&").associate { + val parts = it.split("=", limit = 2) + val key = URLDecoder.decode(parts[0], Charsets.UTF_8.name()) + key to (parts.getOrNull(1)?.let { v -> URLDecoder.decode(v, Charsets.UTF_8.name()) } ?: "") + } + + private fun validateTokenString(value: String) = + parseTokenString(value).let { + if (it["token"].isNullOrEmpty() || it["validity"].isNullOrEmpty() || it["userId"].isNullOrEmpty() || it["websocketToken"].isNullOrEmpty()) { + throw ValidationException("Invalid token string") + } + value + } + + override fun createUi(): Pane = createSigninPane() override fun resetUi() {} From 83c83a49aa46500c8327e6bbce19d5fe95ce2f0e Mon Sep 17 00:00:00 2001 From: Dmitry Barashev Date: Sat, 18 Jul 2026 02:41:07 +0400 Subject: [PATCH 5/5] Code review round --- .../core/option/PropertyPaneBuilder.kt | 1 + .../java/biz/ganttproject/app/PropertySheet.kt | 2 ++ .../ganttproject/storage/cloud/SigninPane.kt | 18 +++++++++--------- .../storage/cloud/GPCloudSignupPane.scss | 2 -- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/biz.ganttproject.core/src/main/java/biz/ganttproject/core/option/PropertyPaneBuilder.kt b/biz.ganttproject.core/src/main/java/biz/ganttproject/core/option/PropertyPaneBuilder.kt index 09104cd46..b152f023c 100644 --- a/biz.ganttproject.core/src/main/java/biz/ganttproject/core/option/PropertyPaneBuilder.kt +++ b/biz.ganttproject.core/src/main/java/biz/ganttproject/core/option/PropertyPaneBuilder.kt @@ -101,6 +101,7 @@ data class TextDisplayOptions( var isScreened: Boolean = false, var columnCount: Int = 40, var rightNode: Node? = null, + val isValid: ObservableBoolean = ObservableBoolean("",true), ) : PropertyDisplayOptions() data class FileExtensionFilter(val description: String, val extensions: List) diff --git a/ganttproject/src/main/java/biz/ganttproject/app/PropertySheet.kt b/ganttproject/src/main/java/biz/ganttproject/app/PropertySheet.kt index 29afd0084..3d72357dc 100644 --- a/ganttproject/src/main/java/biz/ganttproject/app/PropertySheet.kt +++ b/ganttproject/src/main/java/biz/ganttproject/app/PropertySheet.kt @@ -279,9 +279,11 @@ class PropertyPaneBuilderImpl(private val localizer: Localizer, private val grid validatedText.validationMessage.addWatcher { if (it.newValue == null) { + displayOptions?.isValid?.value = true textField.markValid() validationErrors.remove(property) } else { + displayOptions?.isValid?.value = false textField.markInvalid() validationErrors[property] = it.newValue } diff --git a/ganttproject/src/main/java/biz/ganttproject/storage/cloud/SigninPane.kt b/ganttproject/src/main/java/biz/ganttproject/storage/cloud/SigninPane.kt index 187cf3977..c6e3a6c33 100644 --- a/ganttproject/src/main/java/biz/ganttproject/storage/cloud/SigninPane.kt +++ b/ganttproject/src/main/java/biz/ganttproject/storage/cloud/SigninPane.kt @@ -54,9 +54,6 @@ import kotlin.concurrent.schedule * @author dbarashev@bardsoftware.com */ class SigninPane : FlowPage() { - companion object { - private const val SIGNIN_TIMEOUT_SECONDS = 60 - } enum class Status { INIT, WAITING_FOR_BROWSER, WAITING_FOR_AUTH, AUTH_COMPLETED @@ -70,8 +67,8 @@ class SigninPane : FlowPage() { private val indicatorPane = BorderPane().apply { styleClass.add("indicator-pane") } - private val urlOption = ObservableString("foo1", "") - private val tokenOption = ObservableString("foo2", "", validator = this::validateTokenString) + private val urlOption = ObservableString("cloud.signin.url", "") + private val tokenOption = ObservableString("cloud.signin.token", "", validator = this::validateTokenString) private fun onStartCallback() { FXThread.runLater { @@ -110,7 +107,6 @@ class SigninPane : FlowPage() { } urlOption.value = uri - urlOption.isWritable.value = false val submitButton = Button(ourLocalizer.formatText("button.submitToken")).apply { addEventHandler(ActionEvent.ACTION) { submitToken(tokenOption.value ?: "") } } @@ -127,11 +123,14 @@ class SigninPane : FlowPage() { labelHAlignment = HPos.LEFT labelPosition = LabelPosition.ABOVE rightNode = submitButton + isValid.addWatcher { + submitButton.isDisable = !it.newValue + } + submitButton.isDisable = true } } vboxBuilder.add(tokenPane, Pos.CENTER, Priority.NEVER) - vboxBuilder.vbox.let { it.stylesheets.addAll( DIALOG_STYLESHEET, @@ -151,7 +150,7 @@ class SigninPane : FlowPage() { } private fun startBrowserTimeout(uri: String) { - Timer().schedule(60000) { + Timer().schedule(SIGNIN_TIMEOUT_SECONDS * 1000L) { if (status == Status.WAITING_FOR_BROWSER) { FXThread.runLater { statusText.value = ourLocalizer.formatText("text.browser_failed") @@ -196,13 +195,14 @@ class SigninPane : FlowPage() { this.controller = controller controller.httpd.onStart = ::onStartCallback controller.httpd.onAuthReceived = { - FXUtil.runLater { + FXThread.runLater { tokenOption.isWritable.value = false } } } } +private const val SIGNIN_TIMEOUT_SECONDS = 60 private val ourLocalizer = RootLocalizer.createWithRootKey( rootKey = "cloud.signin", diff --git a/ganttproject/src/main/sass/biz/ganttproject/storage/cloud/GPCloudSignupPane.scss b/ganttproject/src/main/sass/biz/ganttproject/storage/cloud/GPCloudSignupPane.scss index 36cd33c09..ce53b1756 100644 --- a/ganttproject/src/main/sass/biz/ganttproject/storage/cloud/GPCloudSignupPane.scss +++ b/ganttproject/src/main/sass/biz/ganttproject/storage/cloud/GPCloudSignupPane.scss @@ -36,8 +36,6 @@ .property-pane { HBox { -fx-padding: 0 0 15 0; - -fx-border-color: red; - -fx-border-width: 0px; } -fx-padding: 30 10 0 10; .custom-text-field {