Skip to content

Commit c56c413

Browse files
committed
Fix Settings issues;
Add new Menubar items; Change Menubar items to be cherry-picked per Project type; Persist Side-menu states and Project Files tree.
1 parent 2ffd271 commit c56c413

24 files changed

Lines changed: 1807 additions & 476 deletions

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ plugins {
1414
ksp { arg("verbose", "true") }
1515

1616
group = "io.github.footermandev.tritium"
17-
version = "0.1.3"
17+
version = "0.1.4"
1818
val tritiumVersion = project.version.toString()
1919

2020
val os: OperatingSystem = OperatingSystem.current()

src/main/kotlin/io/github/footermandev/tritium/Main.kt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package io.github.footermandev.tritium
33
import io.github.footermandev.tritium.accounts.MicrosoftAuth.attemptAutoSignIn
44
import io.github.footermandev.tritium.bootstrap.runLowPriorityTasks
55
import io.github.footermandev.tritium.bootstrap.startHost
6+
import io.github.footermandev.tritium.extension.core.CoreSettingValues
67
import io.github.footermandev.tritium.font.loadFont
78
import io.github.footermandev.tritium.git.Git
89
import io.github.footermandev.tritium.logging.Logs
10+
import io.github.footermandev.tritium.platform.GameProcessMngr
911
import io.github.footermandev.tritium.platform.Platform
1012
import io.github.footermandev.tritium.ui.dashboard.Dashboard
1113
import io.github.footermandev.tritium.ui.logging.Hotkeys
@@ -16,6 +18,7 @@ import io.qt.core.Qt
1618
import io.qt.gui.QFont
1719
import io.qt.gui.QIcon
1820
import io.qt.widgets.QApplication
21+
import io.qt.widgets.QMessageBox
1922
import io.qt.widgets.QWidget
2023
import kotlinx.coroutines.runBlocking
2124
import org.slf4j.Logger
@@ -71,6 +74,7 @@ class Main {
7174
QApplication.setWindowIcon(QIcon(resourceIcon("icons/tritium.png", TConstants.classLoader)!!))
7275
QApplication.setDesktopFileName("tritium")
7376
QApplication.setApplicationName("tritium")
77+
TApp.aboutToQuit.connect { handleRunningGamesOnExit() }
7478

7579
Dashboard.createAndShow()
7680

@@ -81,6 +85,42 @@ class Main {
8185
QApplication.exec()
8286
}
8387

88+
private fun handleRunningGamesOnExit() {
89+
val running = GameProcessMngr.active().filter { it.isRunning }
90+
if (running.isEmpty()) return
91+
92+
val policy = CoreSettingValues.closeGameOnExitPolicy()
93+
val shouldClose = when (policy) {
94+
CoreSettingValues.CloseGameOnExitPolicy.Never -> false
95+
CoreSettingValues.CloseGameOnExitPolicy.Always -> true
96+
CoreSettingValues.CloseGameOnExitPolicy.Ask -> {
97+
val count = running.size
98+
val question = if (count == 1) {
99+
"Close the running game process before exiting?"
100+
} else {
101+
"Close $count running game processes before exiting?"
102+
}
103+
val parent = QApplication.activeWindow() ?: Dashboard.I
104+
val choice = QMessageBox.question(
105+
parent,
106+
"Close Running Game",
107+
question,
108+
QMessageBox.StandardButtons(
109+
QMessageBox.StandardButton.Yes,
110+
QMessageBox.StandardButton.No
111+
),
112+
QMessageBox.StandardButton.Yes
113+
)
114+
choice == QMessageBox.StandardButton.Yes
115+
}
116+
}
117+
if (!shouldClose) return
118+
119+
running.forEach { ctx ->
120+
GameProcessMngr.killByScope(ctx.projectScope, force = true)
121+
}
122+
}
123+
84124
private fun applyStartupFont() {
85125
val prefs = Preferences.userRoot().node("/tritium")
86126
val defaultLoaded = loadFont("/fonts/Inter/InterVariable.ttf")?.let { QFont(it, 10) }

src/main/kotlin/io/github/footermandev/tritium/core/project/ModpackProject.kt

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import io.github.footermandev.tritium.*
44
import io.github.footermandev.tritium.accounts.MCVersion
55
import io.github.footermandev.tritium.accounts.MCVersionType
66
import io.github.footermandev.tritium.accounts.MicrosoftAuth
7-
import io.github.footermandev.tritium.core.project.settings.ProjectSettingDefinition
87
import io.github.footermandev.tritium.core.project.templates.ProjectTemplateExecutor
98
import io.github.footermandev.tritium.core.project.templates.TemplateExecutionResult
109
import io.github.footermandev.tritium.core.project.templates.generation.GeneratorStepDescriptor
@@ -18,6 +17,7 @@ import io.github.footermandev.tritium.platform.Platform
1817
import io.github.footermandev.tritium.ui.helpers.runOnGuiThread
1918
import io.github.footermandev.tritium.ui.notifications.NotificationMngr
2019
import io.github.footermandev.tritium.ui.project.ProjectTaskMngr
20+
import io.github.footermandev.tritium.ui.project.menu.builtin.BuiltinMenuItems
2121
import io.github.footermandev.tritium.ui.theme.TIcons
2222
import io.github.footermandev.tritium.ui.theme.qt.setStyle
2323
import io.github.footermandev.tritium.ui.theme.setInvalid
@@ -41,20 +41,14 @@ class ModpackProjectType : ProjectType {
4141
override val description: String = "Create a ModPack project"
4242
override val icon: QIcon = QIcon(TIcons.TrMeta)
4343
override val order: Int = 1
44-
override val projectSettings: List<ProjectSettingDefinition> = listOf(
45-
ProjectSettingDefinition(
46-
key = "mc_java_path",
47-
comments = listOf("Optional Java executable path used only for this project.")
48-
),
49-
ProjectSettingDefinition(
50-
key = "mc_args",
51-
comments = listOf("Extra JVM args applied when launching Minecraft.")
52-
),
53-
ProjectSettingDefinition(
54-
key = "mc_memory",
55-
defaultValue = "6144",
56-
comments = listOf("Default memory allocation (MB) for Minecraft.")
57-
),
44+
override val menuScope: ProjectMenuScope = ProjectMenuScope.only(
45+
BuiltinMenuItems.Play,
46+
BuiltinMenuItems.Stop,
47+
BuiltinMenuItems.File,
48+
BuiltinMenuItems.Edit,
49+
BuiltinMenuItems.View,
50+
BuiltinMenuItems.Game,
51+
BuiltinMenuItems.Help
5852
)
5953

6054
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package io.github.footermandev.tritium.core.project
2+
3+
import io.github.footermandev.tritium.ui.project.menu.MenuItem
4+
5+
/**
6+
* Per-project-type menu visibility rules applied by the project menu bar.
7+
*
8+
* - [strict] = `true`: only [includedItems] (and their descendants) are shown.
9+
* - [strict] = `false`: all items are shown except [excludedItems].
10+
*/
11+
data class ProjectMenuScope(
12+
val includedItems: Set<MenuItem> = emptySet(),
13+
val excludedItems: Set<MenuItem> = emptySet(),
14+
val strict: Boolean = false
15+
) {
16+
internal fun includedIds(): Set<String> = includedItems.asSequence().map { it.id }.toSet()
17+
internal fun excludedIds(): Set<String> = excludedItems.asSequence().map { it.id }.toSet()
18+
19+
companion object {
20+
/**
21+
* Show all menu items.
22+
*/
23+
fun all(): ProjectMenuScope = ProjectMenuScope()
24+
25+
/**
26+
* Show only [items] and their descendants.
27+
*/
28+
fun only(vararg items: MenuItem): ProjectMenuScope =
29+
ProjectMenuScope(
30+
includedItems = items.toSet(),
31+
strict = true
32+
)
33+
34+
/**
35+
* Show all items except [items] and their descendants.
36+
*/
37+
fun allExcept(vararg items: MenuItem): ProjectMenuScope =
38+
ProjectMenuScope(
39+
excludedItems = items.toSet(),
40+
strict = false
41+
)
42+
}
43+
}

src/main/kotlin/io/github/footermandev/tritium/core/project/ProjectMngr.kt

Lines changed: 48 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
package io.github.footermandev.tritium.core.project
22

33
import io.github.footermandev.tritium.TConstants
4-
import io.github.footermandev.tritium.core.project.settings.ProjectScopedSettingsMngr
54
import io.github.footermandev.tritium.core.project.templates.MigrationRegistry
65
import io.github.footermandev.tritium.core.project.templates.ProjectFileLoader
76
import io.github.footermandev.tritium.core.project.templates.TemplateDescriptor
87
import io.github.footermandev.tritium.core.project.templates.TemplateRegistry
9-
import io.github.footermandev.tritium.extension.core.BuiltinRegistries
108
import io.github.footermandev.tritium.extension.core.CoreSettingValues
119
import io.github.footermandev.tritium.fromTR
1210
import io.github.footermandev.tritium.io.VPath
1311
import io.github.footermandev.tritium.logger
1412
import io.github.footermandev.tritium.ui.dashboard.Dashboard
1513
import io.github.footermandev.tritium.ui.project.ProjectWindows
1614
import io.github.footermandev.tritium.ui.theme.TIcons
15+
import io.qt.widgets.QApplication
16+
import io.qt.widgets.QMessageBox
1717
import kotlinx.serialization.KSerializer
1818
import kotlinx.serialization.Serializable
1919
import kotlinx.serialization.SerializationException
@@ -91,39 +91,6 @@ object ProjectMngr {
9191
listeners.forEach { it.onProjectsFinishedLoading(snapshot) }
9292
}
9393

94-
private fun resolveProjectTypeForSettings(typeId: String): ProjectType? {
95-
BuiltinRegistries.ProjectType.get(typeId)?.let { return it }
96-
val localId = typeId.substringAfterLast(':', missingDelimiterValue = typeId)
97-
if (localId != typeId) {
98-
BuiltinRegistries.ProjectType.get(localId)?.let { resolved ->
99-
logger.debug(
100-
"Resolved project type '{}' to local id '{}' for project-scoped settings",
101-
typeId,
102-
localId
103-
)
104-
return resolved
105-
}
106-
}
107-
return null
108-
}
109-
110-
private fun ensureProjectScopedSettings(project: ProjectBase) {
111-
try {
112-
val type = resolveProjectTypeForSettings(project.typeId)
113-
if (type == null) {
114-
logger.debug(
115-
"Skipping project-scoped settings initialization for {} (unknown project type '{}')",
116-
project.path,
117-
project.typeId
118-
)
119-
return
120-
}
121-
ProjectScopedSettingsMngr.ensureProjectFiles(project, type.projectSettings)
122-
} catch (t: Throwable) {
123-
logger.warn("Failed to initialize project-scoped settings for {}", project.path, t)
124-
}
125-
}
126-
12794
private fun loadProjectFromDir(dir: VPath): ProjectBase? {
12895
val trMeta = ProjectFiles.readTrProject(dir) ?: run {
12996
logger.warn("No trproj.json found in {}", dir)
@@ -139,14 +106,10 @@ object ProjectMngr {
139106
val descriptor = TemplateRegistry.get(typeId)
140107
if(descriptor is ProjectFileLoader) {
141108
return try {
142-
descriptor.loadFromProjectFile(trMeta, dir).also { project ->
143-
ensureProjectScopedSettings(project)
144-
}
109+
descriptor.loadFromProjectFile(trMeta, dir)
145110
} catch (e: Exception) {
146111
logger.error("Failed to load project via ProjectFileLoader for type=$typeId in $dir", e)
147-
ProjectBase(typeId, dir, name, icon, metaElem.jsonObjectOrEmpty()).also { project ->
148-
ensureProjectScopedSettings(project)
149-
}
112+
ProjectBase(typeId, dir, name, icon, metaElem.jsonObjectOrEmpty())
150113
}
151114
}
152115

@@ -165,21 +128,15 @@ object ProjectMngr {
165128
val typed = json.decodeFromJsonElement(serializer, migratedMeta)
166129
@Suppress("UNCHECKED_CAST")
167130
val typedDescriptor = descriptor as TemplateDescriptor<Any>
168-
return typedDescriptor.createProjectFromMeta(typed, descriptor.currentSchema, dir).also { project ->
169-
ensureProjectScopedSettings(project)
170-
}
131+
return typedDescriptor.createProjectFromMeta(typed, descriptor.currentSchema, dir)
171132
} catch (e: Exception) {
172133
logger.error("Failed to decode meta for type=$typeId in $dir", e)
173-
return ProjectBase(typeId, dir, name, icon, metaElem.jsonObjectOrEmpty()).also { project ->
174-
ensureProjectScopedSettings(project)
175-
}
134+
return ProjectBase(typeId, dir, name, icon, metaElem.jsonObjectOrEmpty())
176135
}
177136
}
178137

179138
logger.warn("Unknown project type: $typeId (directory $dir)")
180-
return ProjectBase(typeId, dir, name, icon, metaElem.jsonObjectOrEmpty()).also { project ->
181-
ensureProjectScopedSettings(project)
182-
}
139+
return ProjectBase(typeId, dir, name, icon, metaElem.jsonObjectOrEmpty())
183140
}
184141

185142
/**
@@ -507,12 +464,18 @@ object ProjectMngr {
507464
fun openProject(project: ProjectBase) {
508465
logger.info("Loading project {}", project.name)
509466
addProjectToCatalog(project.projectDir, project.name)
510-
val wasDifferent = activeProject !== project
467+
val previousActive = activeProject
468+
val openMode = resolveOpenMode(project) ?: return
469+
val wasDifferent = previousActive !== project
511470
activeProject = project
512471
val closeDashboard = CoreSettingValues.closeDashboardOnProjectOpen() && wasDifferent
513472

514473
try {
515-
ProjectWindows.openProject(project, closeDashboard = closeDashboard)
474+
ProjectWindows.openProject(
475+
project = project,
476+
closeDashboard = closeDashboard,
477+
mode = openMode
478+
)
516479
} catch (e: Exception) {
517480
logger.debug("Failed to open project", e)
518481
}
@@ -567,6 +530,39 @@ object ProjectMngr {
567530
return getProjectsFromCatalog(source)
568531
}
569532

533+
private fun resolveOpenMode(project: ProjectBase): ProjectWindows.OpenMode? {
534+
val existing = ProjectWindows.anyOpenWindow() ?: return ProjectWindows.OpenMode.NEW_WINDOW
535+
val targetCanonical = project.path.toString().trim()
536+
if (existing.projectCanonicalPath() == targetCanonical) {
537+
return ProjectWindows.OpenMode.NEW_WINDOW
538+
}
539+
540+
return when (CoreSettingValues.projectOpenPromptMode()) {
541+
CoreSettingValues.ProjectOpenPromptMode.Always -> promptOpenMode(project)
542+
CoreSettingValues.ProjectOpenPromptMode.Never -> when (CoreSettingValues.projectOpenDefaultTarget()) {
543+
CoreSettingValues.ProjectOpenDefaultTarget.Current -> ProjectWindows.OpenMode.CURRENT_WINDOW
544+
CoreSettingValues.ProjectOpenDefaultTarget.New -> ProjectWindows.OpenMode.NEW_WINDOW
545+
}
546+
}
547+
}
548+
549+
private fun promptOpenMode(project: ProjectBase): ProjectWindows.OpenMode? {
550+
val parent = QApplication.activeWindow() ?: Dashboard.I
551+
val box = QMessageBox(parent)
552+
box.icon = QMessageBox.Icon.Question
553+
box.windowTitle = "Open Project"
554+
box.text = "Open '${project.name}' in current window or a new window?"
555+
val currentButton = box.addButton("Current Window", QMessageBox.ButtonRole.AcceptRole)
556+
val newButton = box.addButton("New Window", QMessageBox.ButtonRole.ActionRole)
557+
box.addButton(QMessageBox.StandardButton.Cancel)
558+
box.exec()
559+
return when (box.clickedButton()) {
560+
currentButton -> ProjectWindows.OpenMode.CURRENT_WINDOW
561+
newButton -> ProjectWindows.OpenMode.NEW_WINDOW
562+
else -> null
563+
}
564+
}
565+
570566
private fun isDashboardActive(): Boolean {
571567
val dash = Dashboard.I ?: return false
572568
return dash.isVisible
@@ -581,7 +577,6 @@ object ProjectMngr {
581577
* Notify listeners that a project was created outside the manager.
582578
*/
583579
fun notifyCreatedExternal(project: ProjectBase) {
584-
ensureProjectScopedSettings(project)
585580
addProjectToCatalog(project.projectDir, project.name)
586581
synchronized(_projectsLock) {
587582
val existing = _projects.any { it.projectDir.toAbsolute() == project.projectDir.toAbsolute() }

src/main/kotlin/io/github/footermandev/tritium/core/project/ProjectType.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package io.github.footermandev.tritium.core.project
22

3-
import io.github.footermandev.tritium.core.project.settings.ProjectSettingDefinition
43
import io.github.footermandev.tritium.core.project.templates.TemplateExecutionResult
54
import io.github.footermandev.tritium.registry.Registrable
65
import io.qt.gui.QIcon
@@ -16,7 +15,11 @@ interface ProjectType: Registrable {
1615
val description: String
1716
val icon: QIcon
1817
val order: Int
19-
val projectSettings: List<ProjectSettingDefinition> get() = emptyList()
18+
/**
19+
* Controls which menu items appear for this project type in [io.github.footermandev.tritium.ui.project.menu.ProjectMenuBar].
20+
*/
21+
val menuScope: ProjectMenuScope
22+
get() = ProjectMenuScope.all()
2023

2124
/**
2225
* Build a setup widget for collecting project variables.

0 commit comments

Comments
 (0)