diff --git a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/InstanceGridEntryFactory.kt b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/InstanceGridEntryFactory.kt index 46e8133..12b7b45 100644 --- a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/InstanceGridEntryFactory.kt +++ b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/InstanceGridEntryFactory.kt @@ -6,28 +6,28 @@ import org.gnome.gtk.* import org.pango.EllipsizeMode import org.pango.WrapMode -class InstanceGridEntryFactory(instanceList: List) : SignalListItemFactory() { - init { - //TODO better design - onSetup { item -> - val box = Box(Orientation.VERTICAL, 5) +class InstanceGridEntryFactory( + private val instanceList: List +) : KSignalListItemFactory() { + override fun setup(): Box { + val box = Box(Orientation.VERTICAL, 5) - val thumbnail = InstanceThumbnail() - box.append(thumbnail) + val thumbnail = InstanceThumbnail() + box.append(thumbnail) - val label = Label(null as String?) - label.setSizeRequest(192, -1) - label.maxWidthChars = 20 - label.justify = Justification.CENTER - label.halign = Align.START - label.hexpand = true - label.valign = Align.CENTER - label.ellipsize = EllipsizeMode.MIDDLE - label.lines = 3 - label.wrap = true - label.wrapMode = WrapMode.WORD_CHAR - label.marginTop = 10 - box.append(label) + val label = Label(null as String?) + label.setSizeRequest(192, -1) + label.maxWidthChars = 20 + label.justify = Justification.CENTER + label.halign = Align.START + label.hexpand = true + label.valign = Align.CENTER + label.ellipsize = EllipsizeMode.MIDDLE + label.lines = 3 + label.wrap = true + label.wrapMode = WrapMode.WORD_CHAR + label.marginTop = 10 + box.append(label) // Label label = new Label(Str.NULL); // label.setXalign(0); // label.setWidthChars(20); @@ -45,11 +45,12 @@ class InstanceGridEntryFactory(instanceList: List) : SignalListItemFac // openDir.setTooltipText(I18n.str("instance.directory")); // openDir.setHasTooltip(GTK.TRUE); // box.append(openDir); - (item as ListItem).setChild(box) - //TODO server launch with network-server-symbolic - //TODO kill current instance - } - onBind { item -> + //TODO server launch with network-server-symbolic + //TODO kill current instance + return box + } + + override fun BindContext.bind(widget: Box, data: Decomposed) { // Label label = new Label(item.getChild().getFirstChild().cast()); // Button launch = new Button(label.getNextSibling().cast()); // Button openDir = new Button(launch.getNextSibling().cast()); @@ -57,16 +58,19 @@ class InstanceGridEntryFactory(instanceList: List) : SignalListItemFac // label.setText(new Str(instance.toString())); // launch.onClicked(() -> GtkMenubar.launch(instance)); // openDir.onClicked(() -> Utils.openFile(instance.path().toFile())); + val thumbnail = InstanceThumbnail.castFrom(widget.firstChild as Stack) + val label = thumbnail.nextSibling as Label - val li = item as ListItem - - val box = li.getChild() as Box - val thumbnail = InstanceThumbnail.castFrom(box.firstChild as Stack) - val label = thumbnail.nextSibling as Label - - val instance = instanceList[(li.item as StringObject).string] - thumbnail.bind(instance!!) - label.text = instance.toString() - } + val instance = instanceList[data.id] + thumbnail.bind(instance!!) + label.text = instance.toString() } + + override fun UnbindContext.unbind(widget: Box, data: Decomposed) { + } + + override fun ActionContext.castWidget(widget: Widget): Box = widget as Box + override fun ActionContext.lookup(id: String, widget: Box): Decomposed = Decomposed(id) + + class Decomposed(val id: String) } diff --git a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/InstanceListEntryFactory.kt b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/InstanceListEntryFactory.kt index 1ec8eee..581a07f 100644 --- a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/InstanceListEntryFactory.kt +++ b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/InstanceListEntryFactory.kt @@ -1,6 +1,5 @@ package io.gitlab.jfronny.inceptum.gtk.control -import io.github.jwharm.javagi.base.Signal import io.gitlab.jfronny.commons.io.JFiles import io.gitlab.jfronny.commons.ref.R import io.gitlab.jfronny.inceptum.common.MetaHolder @@ -20,125 +19,128 @@ import org.gnome.adw.ActionRow import org.gnome.gio.Menu import org.gnome.gtk.* import java.io.IOException -import java.util.function.Consumer -class InstanceListEntryFactory(app: Application?, instanceList: List) : SignalListItemFactory() { - init { - onSetup { - val li = it as ListItem +class InstanceListEntryFactory( + private val app: Application?, + private val instanceList: List +) : KSignalListItemFactory() { + override fun setup(): ActionRow { + val thumbnail = InstanceThumbnail() + thumbnail.name = "inceptum-thumbnail" - val thumbnail = InstanceThumbnail() - thumbnail.name = "inceptum-thumbnail" + val launch = Button.newFromIconName("media-playback-start-symbolic") + launch.addCssClass("flat") + launch.name = "inceptum-launch" + launch.tooltipText = I18n["instance.launch"] + launch.hasTooltip = true - val launch = Button.newFromIconName("media-playback-start-symbolic") - launch.addCssClass("flat") - launch.name = "inceptum-launch" - launch.tooltipText = I18n["instance.launch"] - launch.hasTooltip = true + val menu = MenuButton() + menu.addCssClass("flat") + menu.iconName = "view-more-symbolic" + menu.setPopover(PopoverMenu.newFromModel(Menu())) - val menu = MenuButton() - menu.addCssClass("flat") - menu.iconName = "view-more-symbolic" - menu.setPopover(PopoverMenu.newFromModel(Menu())) + val row = ActionRow() + row.margin = 8 + row.name = "inceptum-row" + row.removeCssClass("activatable") //TODO remove this workaround if a better way to support opening the menu is found + row.addPrefix(thumbnail) + row.addSuffix(launch) + row.addSuffix(menu) + row.fixSubtitle() - val row = ActionRow() - row.margin = 8 - row.name = "inceptum-row" - row.removeCssClass("activatable") //TODO remove this workaround if a better way to support opening the menu is found - row.addPrefix(thumbnail) - row.addSuffix(launch) - row.addSuffix(menu) - row.fixSubtitle() + val rightClicked = GestureClick() + rightClicked.button = 3 + rightClicked.onPressed { nPress, _, _ -> if (nPress == 1) menu.emitActivate() } + row.addController(rightClicked) - val rightClicked = GestureClick() - rightClicked.button = 3 - rightClicked.onPressed { nPress, _, _ -> if (nPress == 1) menu.emitActivate() } - row.addController(rightClicked) - - li.child = row - } - val toDisconnect: MutableMap>> = HashMap() - onBind { - val li = Decomposed.of(it as ListItem, instanceList) - - if (li.instance?.isLocked ?: true) { - li.item.activatable = false - li.row.subtitle = if (li.instance?.isRunningLocked ?: false) I18n["instance.launch.locked.running"] - else I18n["instance.launch.locked.setup"] - } - - li.row.title = li.instance.toString() - - li.thumbnail.bind(li.instance!!) - - val menuBuilder = MenuBuilder(li.popoverMenu, li.instanceId) - val launchSection = menuBuilder.literalSection("launch", null) - val kill = launchSection.literalButton("kill", I18n["instance.kill"]) { - //TODO test - LauncherEnv.showOkCancel(I18n["instance.kill.prompt"], I18n["instance.kill.details"]) { - if (!li.instance.kill()) LauncherEnv.showError(I18n["instance.kill.fail"], I18n["failed"]) - } - } - kill.enabled = li.instance.isRunningLocked - - launchSection.literalButton( - "launch.client", I18n["instance.launch.client"] - ) { GtkMenubar.launch(li.instance, LaunchType.Client) }.iconName = "media-playback-start-symbolic" - launchSection.literalButton( - "launch.server", I18n["instance.launch.server"] - ) { GtkMenubar.launch(li.instance, LaunchType.Server) }.iconName = "network-server-symbolic" - val settingsSection = menuBuilder.literalSection("settings", null) - settingsSection.literalButton("settings", I18n["instance.settings"]) { - //TODO keep track of properties windows and don't allow opening two - InstanceSettingsWindow(app, li.instance).visible = true - }.iconName = "document-edit-symbolic" - settingsSection.literalButton( - "directory", I18n["instance.directory"] - ) { Utils.openFile(li.instance.path.toFile()) }.iconName = "folder-symbolic" - settingsSection.literalButton("copy", I18n["instance.copy"]) { - LauncherEnv.getInput( - I18n["instance.copy.prompt"], - I18n["instance.copy.details"], - InstanceNameTool.getNextValid(li.instance.name), - { s: String? -> - try { - JFiles.copyRecursive( - li.instance.path, - MetaHolder.INSTANCE_DIR.resolve(InstanceNameTool.getNextValid(s)) - ) - } catch (e: IOException) { - LauncherEnv.showError(I18n["instance.copy.fail"], e) - } - }) { R.nop() } - }.iconName = "edit-copy-symbolic" - settingsSection.literalButton("delete", I18n["instance.delete"]) { - LauncherEnv.showOkCancel( - I18n["instance.delete.confirm"], - I18n["instance.delete.confirm.title"] - ) { - try { - JFiles.deleteRecursive(li.instance.path) - } catch (e: IOException) { - LauncherEnv.showError(I18n["instance.delete.fail"], e) - } - } - }.iconName = "edit-delete-symbolic" - - val dc = Consumer { s: Signal<*> -> - toDisconnect.computeIfAbsent(li.instanceId) { _ -> HashSet() } - .add(s) - } - - dc.accept(li.launch.onClicked { GtkMenubar.launch(li.instance, LaunchType.Client) }) - } - onUnbind { - val li = Decomposed.of(it as ListItem, instanceList) - li.popoverMenu.insertActionGroup(li.instanceId, null) - toDisconnect[li.instanceId]?.forEach(Consumer { obj: Signal<*> -> obj.disconnect() }) - } + return row } - private class Decomposed( + override fun BindContext.bind(widget: ActionRow, data: Decomposed) { + if (data.instance?.isLocked ?: true) { + data.item.activatable = false + data.row.subtitle = if (data.instance?.isRunningLocked ?: false) I18n["instance.launch.locked.running"] + else I18n["instance.launch.locked.setup"] + } + + data.row.title = data.instance.toString() + + data.thumbnail.bind(data.instance!!) + + val menuBuilder = MenuBuilder(data.popoverMenu, data.instanceId) + val launchSection = menuBuilder.literalSection("launch", null) + val kill = launchSection.literalButton("kill", I18n["instance.kill"]) { + //TODO test + LauncherEnv.showOkCancel(I18n["instance.kill.prompt"], I18n["instance.kill.details"]) { + if (!data.instance.kill()) LauncherEnv.showError(I18n["instance.kill.fail"], I18n["failed"]) + } + } + kill.enabled = data.instance.isRunningLocked + + launchSection.literalButton( + "launch.client", I18n["instance.launch.client"] + ) { GtkMenubar.launch(data.instance, LaunchType.Client) }.iconName = "media-playback-start-symbolic" + launchSection.literalButton( + "launch.server", I18n["instance.launch.server"] + ) { GtkMenubar.launch(data.instance, LaunchType.Server) }.iconName = "network-server-symbolic" + val settingsSection = menuBuilder.literalSection("settings", null) + settingsSection.literalButton("settings", I18n["instance.settings"]) { + //TODO keep track of properties windows and don't allow opening two + InstanceSettingsWindow(app, data.instance).visible = true + }.iconName = "document-edit-symbolic" + settingsSection.literalButton( + "directory", I18n["instance.directory"] + ) { Utils.openFile(data.instance.path.toFile()) }.iconName = "folder-symbolic" + settingsSection.literalButton("copy", I18n["instance.copy"]) { + LauncherEnv.getInput( + I18n["instance.copy.prompt"], + I18n["instance.copy.details"], + InstanceNameTool.getNextValid(data.instance.name), + { s: String? -> + try { + JFiles.copyRecursive( + data.instance.path, + MetaHolder.INSTANCE_DIR.resolve(InstanceNameTool.getNextValid(s)) + ) + } catch (e: IOException) { + LauncherEnv.showError(I18n["instance.copy.fail"], e) + } + }) { R.nop() } + }.iconName = "edit-copy-symbolic" + settingsSection.literalButton("delete", I18n["instance.delete"]) { + LauncherEnv.showOkCancel( + I18n["instance.delete.confirm"], + I18n["instance.delete.confirm.title"] + ) { + try { + JFiles.deleteRecursive(data.instance.path) + } catch (e: IOException) { + LauncherEnv.showError(I18n["instance.delete.fail"], e) + } + } + }.iconName = "edit-delete-symbolic" + + registerForUnbind(data.launch.onClicked { GtkMenubar.launch(data.instance, LaunchType.Client) }) + } + + override fun UnbindContext.unbind(widget: ActionRow, data: Decomposed) { + data.popoverMenu.insertActionGroup(data.instanceId, null) + } + + override fun ActionContext.castWidget(widget: Widget): ActionRow = widget as ActionRow + + override fun ActionContext.lookup(id: String, widget: ActionRow): Decomposed { + val instance = instanceList[id] + val prefixes = widget.firstChild!!.firstChild as Box + val suffixes = widget.firstChild!!.lastChild as Box + val thumbnail = InstanceThumbnail.castFrom(prefixes.firstChild as Stack) + val launch = suffixes.firstChild as Button + val menuButton = launch.nextSibling as MenuButton + val popoverMenu = menuButton.popover as PopoverMenu + return Decomposed(listItem, id, instance, widget, thumbnail, launch, popoverMenu) + } + + class Decomposed( val item: ListItem, val instanceId: String, val instance: Instance?, @@ -146,20 +148,5 @@ class InstanceListEntryFactory(app: Application?, instanceList: List) val thumbnail: InstanceThumbnail, val launch: Button, val popoverMenu: PopoverMenu - ) { - companion object { - fun of(item: ListItem, instanceList: List): Decomposed { - val instanceId = (item.item as StringObject).string - val instance = instanceList[instanceId] - val row = item.child as ActionRow - val prefixes = row.firstChild!!.firstChild as Box - val suffixes = row.firstChild!!.lastChild as Box - val thumbnail = InstanceThumbnail.castFrom(prefixes.firstChild as Stack) - val launch = suffixes.firstChild as Button - val menuButton = launch.nextSibling as MenuButton - val popoverMenu = menuButton.popover as PopoverMenu - return Decomposed(item, instanceId, instance, row, thumbnail, launch, popoverMenu) - } - } - } + ) } diff --git a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/InstanceThumbnail.kt b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/InstanceThumbnail.kt index 208d20b..d93ce67 100644 --- a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/InstanceThumbnail.kt +++ b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/InstanceThumbnail.kt @@ -40,7 +40,7 @@ class InstanceThumbnail : Stack { private const val SPINNER = "spinner" private const val IMAGE = "image" private const val GENERIC = "generic" - @JvmStatic + fun castFrom(stack: Stack): InstanceThumbnail = InstanceThumbnail(stack.handle()) } } diff --git a/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/KSignalListItemFactory.kt b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/KSignalListItemFactory.kt new file mode 100644 index 0000000..4942a72 --- /dev/null +++ b/launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/KSignalListItemFactory.kt @@ -0,0 +1,58 @@ +package io.gitlab.jfronny.inceptum.gtk.control + +import io.github.jwharm.javagi.base.Signal +import org.gnome.gtk.ListItem +import org.gnome.gtk.SignalListItemFactory +import org.gnome.gtk.StringObject +import org.gnome.gtk.Widget + +abstract class KSignalListItemFactory : SignalListItemFactory() { + private val toDisconnect: MutableMap>> = HashMap() + init { + onSetup { + val li = it as ListItem + li.child = setup() + } + onBind { + val li = it as ListItem + val id = (li.item as StringObject).string + val context = BindContextImpl(id, li) + val widget = context.castWidget(li.child!!) + context.bind(widget, context.lookup(id, widget)) + } + onUnbind { + val li = it as ListItem + val id = (li.item as StringObject).string + val context = UnbindContextImpl(li) + val widget = context.castWidget(li.child!!) + toDisconnect.remove(id)?.forEach { it.disconnect() } + context.unbind(widget, context.lookup(id, widget)) + } + } + + abstract fun setup(): TWidget + abstract fun BindContext.bind(widget: TWidget, data: TData) + abstract fun UnbindContext.unbind(widget: TWidget, data: TData) + abstract fun ActionContext.lookup(id: String, widget: TWidget): TData + abstract fun ActionContext.castWidget(widget: Widget): TWidget + + interface ActionContext { + val listItem: ListItem + } + + interface BindContext: ActionContext { + fun registerForUnbind(signal: Signal<*>) + } + + interface UnbindContext: ActionContext { + } + + private inner class BindContextImpl(private val id: String, override val listItem: ListItem) : BindContext { + override fun registerForUnbind(signal: Signal<*>) { + toDisconnect.computeIfAbsent(id) { _ -> HashSet() } + .add(signal) + } + } + + private inner class UnbindContextImpl(override val listItem: ListItem): UnbindContext +} \ No newline at end of file