Skip to content

[phase-02] Reach launcher services through LauncherContext - #190

Draft
YongDo-Hyun wants to merge 64 commits into
feature/phase-01-qt6-only-qml-skeletonfrom
feature/phase-02-launcher-context
Draft

YongDo-Hyun wants to merge 64 commits into
feature/phase-01-qt6-only-qml-skeletonfrom
feature/phase-02-launcher-context

Conversation

@YongDo-Hyun

Copy link
Copy Markdown
Collaborator

Application derives from QApplication and owns the main window, the settings dialog and the theme manager. Every core file that wanted the network manager or the settings object reached for it through APPLICATION->, and so depended transitively on QtWidgets and on the entire widget page tree. That is what stops the core from being reused under a QML user interface.

So the dependency is inverted. core/LauncherContext.h declares the services code outside the user interface is allowed to reach for; Application implements it and registers itself; the core says LAUNCHER-> instead. Measuring first paid off -- the interface is ten accessors, not the dozens the API surface suggested, because most of Application is only ever touched from ui/.

Files outside launcher/ui/ that include Application.h: 44 before, 5 after. The five that remain are Application.cpp and main.cpp (the shell itself), PluginManager.cpp (the plugin host, which keeps QtWidgets until the plugin ABI work), InstancePageProvider.h (pure widget glue that dies with the widget UI), and LaunchController.cpp.

Two files were not coupled to widgets so much as misfiled, and moved rather than being ported:

  • JavaCommon is entirely message boxes and a java-check driver. It was already listed in the UI source group; only its path said otherwise. Now ui/.
  • ShortcutUtils reports every failure with a QMessageBox and opens a QFileDialog, and its only caller is CreateShortcutDialog. Now ui/.

PasteUpload took a QWidget* and stored it in m_window, which nothing ever read. The parameter and the member are gone, so a network task no longer has a widget in its signature.

AuthRequest called PluginManager directly to run MMCO_HOOK_AUTH_REQUEST, and PluginManager builds plugin-supplied user interface -- so that one line tied authentication to the widget toolkit. The hook body moved behind core/AuthRequestDecorator.h: the core declares what it needs, the plugin layer supplies it, neither knows the other. This edge had to go before the core can be a target that cannot link QtWidgets at all.

While moving it, its contract turned out to be documented backwards: the function returns true when a plugin CANCELLED the request, not when it modified it. The interface says so explicitly now.

Also fixed along the way: InstanceImportTask.cpp included Application.h twice, PackFetchTask.cpp included it without using it, and MeshMCPartLaunch.cpp was reaching Logging.h transitively through Application.h -- it declares that include itself now.

grxtor and others added 2 commits September 22, 2026 23:06
Application derives from QApplication and owns the main window, the settings
dialog and the theme manager. Every core file that wanted the network manager
or the settings object reached for it through APPLICATION->, and so depended
transitively on QtWidgets and on the entire widget page tree. That is what
stops the core from being reused under a QML user interface.

So the dependency is inverted. core/LauncherContext.h declares the services
code outside the user interface is allowed to reach for; Application implements
it and registers itself; the core says LAUNCHER-> instead. Measuring first paid
off -- the interface is ten accessors, not the dozens the API surface suggested,
because most of Application is only ever touched from ui/.

Files outside launcher/ui/ that include Application.h: 44 before, 5 after. The
five that remain are Application.cpp and main.cpp (the shell itself),
PluginManager.cpp (the plugin host, which keeps QtWidgets until the plugin ABI
work), InstancePageProvider.h (pure widget glue that dies with the widget UI),
and LaunchController.cpp.

Two files were not coupled to widgets so much as misfiled, and moved rather than
being ported:

  - JavaCommon is entirely message boxes and a java-check driver. It was already
    listed in the UI source group; only its path said otherwise. Now ui/.
  - ShortcutUtils reports every failure with a QMessageBox and opens a
    QFileDialog, and its only caller is CreateShortcutDialog. Now ui/.

PasteUpload took a QWidget* and stored it in m_window, which nothing ever read.
The parameter and the member are gone, so a network task no longer has a
widget in its signature.

AuthRequest called PluginManager directly to run MMCO_HOOK_AUTH_REQUEST, and
PluginManager builds plugin-supplied user interface -- so that one line tied
authentication to the widget toolkit. The hook body moved behind
core/AuthRequestDecorator.h: the core declares what it needs, the plugin layer
supplies it, neither knows the other. This edge had to go before the core can
be a target that cannot link QtWidgets at all.

While moving it, its contract turned out to be documented backwards: the
function returns true when a plugin CANCELLED the request, not when it modified
it. The interface says so explicitly now.

Also fixed along the way: InstanceImportTask.cpp included Application.h twice,
PackFetchTask.cpp included it without using it, and MeshMCPartLaunch.cpp was
reaching Logging.h transitively through Application.h -- it declares that
include itself now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
LaunchController orchestrates account selection, authentication, progress
reporting and window handling. It opens roughly fifteen modal dialogs: message
boxes, yes/no questions, custom-button prompts, text input, a profile picker, a
profile setup dialog and two ProgressDialog runs. That is not a core service
with an unfortunate dependency -- it is user interface.

The build already said so: it sits in MESHMC_SOURCES alongside MainWindow, and
setParentWidget() is only ever called from Application. Its callers are
Application, MainWindow and InstanceWindow. The three references to it from
outside the UI (launch/steps/CreateBackup.h, plugin/PluginHooks.h,
plugin/PluginManager.h) are all comments, not code. Only its path claimed it
was core.

So it moves rather than being inverted. Building a UiHost interface wide enough
to serve fifteen widget-shaped interactions, purely so a file could keep living
in the wrong directory, would have bought worse architecture than it removed.
When the QML shell needs launch orchestration it gets its own controller; this
one dies with the widget UI it belongs to.

InstanceImportTask is the opposite case and is not touched here: it is a real
Task doing real work, and its modals are a genuine layering violation that has
to be inverted rather than relocated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
@YongDo-Hyun
YongDo-Hyun added this pull request to stack #191 September 22, 2026 20:15
grxtor and others added 27 commits September 22, 2026 23:15
InstanceImportTask stopped five times to ask the user something, and each time
it did so by constructing a QDialog. A task that downloads and unpacks modpacks
had ui/dialogs/ in its include list and a QWidget* threaded through its API
purely to have something to parent those dialogs to.

core/UiHost.h states the questions instead: acknowledge this, confirm that,
choose between these, resolve these blocked files, approve these untrusted
ones. The shell answers them; how the answer is obtained on screen is no longer
the task's business. WidgetUiHost supplies today's message boxes, and parents
them to whatever window is active when the question is asked rather than to a
widget handed over in advance -- which is what setDialogParent() existed for,
and why it is gone along with m_dialogParent and its three call sites.

BlockedMod moved from ui/dialogs/BlockedModsDialog.h to modplatform/. It is a
plain struct describing a file that could not be downloaded; it was only living
in a QDialog header, which forced the install task to include that header to
describe its own data.

The wording of every prompt is unchanged, including the two that name their
actions rather than answering yes or no ("Remove saves"/"Keep saves", and the
three-way choice between updating an instance and creating a separate one).
confirm() takes optional labels for that reason: "Yes" and "No" make the reader
go back and re-read the question.

These calls are synchronous, deliberately. They are decision points in the
middle of a task that branches immediately on the answer, so making them
asynchronous would mean restructuring modpack installation to gain nothing
today -- the widget implementation blocks either way. The interface says
nothing about how an answer is obtained, so a later implementation can pump an
event loop or run the caller on a worker thread. The cost is documented in the
header rather than hidden: a blocking call reached from the GUI thread runs a
nested event loop.

launcher/InstanceImportTask.cpp now includes nothing from ui/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
MeshMCExternalUpdater sits in UPDATE_SOURCES, which is headed for the core
library that cannot link QtWidgets. It built its own QMessageBoxes through a
file-local showMessage() helper, constructed UpdateAvailableDialog directly, and
had a QWidget* threaded through its constructor to parent both.

The seven plain notices now go through UiHost::message(). The update offer does
not: it shows the running version, the offered one and formatted release notes,
and flattening that into the generic choose() would have degraded the notes to
a message-box body. It gets its own method instead, the same way
resolveBlockedMods() does -- UiHost::offerUpdate() returning Install, Later or
Skip. WidgetUiHost answers it with the existing UpdateAvailableDialog, and maps
anything that is neither Install nor Skip to Later, so closing the window keeps
meaning "remind me later" as it did.

The settings side effects are unchanged: a skip is remembered, an install
forgets any earlier skip and syncs before returning, a deferral forgets it.

One presentational change, deliberately: UiHost::message() has no collapsible
"Show Details" section, so the four notices that carried details now append
them to the body. Nothing is lost, but the details are no longer folded away.

The QWidget* constructor parameter, m_parent and the forward declaration are
gone; the file no longer mentions QWidget, QMessageBox or anything under ui/.
checkForUpdates() still builds a QProgressDialog, now unparented. That one is
progress reporting rather than a question, and belongs with the TaskRunner
work, not UiHost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
Three more files outside launcher/ui/ turned out to be user interface living
in the wrong directory rather than core code with a widget dependency -- the
same finding as JavaCommon, ShortcutUtils and LaunchController earlier:

  - FastFileIconProvider was listed in CORE_SOURCES, but its icons come from
    QApplication::style()->standardIcon(), and its only users are
    ExportPackDialog and ExportInstanceDialog. Moved to ui/. (FileIgnoreProxy,
    listed next to it, stays: QFileSystemModel moved to QtGui in Qt 6.)
  - InstancePageProvider.h includes nineteen ui/pages headers and is included
    only by MainWindow and InstanceWindow. It was already in MESHMC_SOURCES;
    only its path disagreed. Moved to ui/.

And one genuine inversion: ContentProviderModel included
ui/widgets/ProjectItemDelegate.h purely to get the ProjectItemRole enum, so a
data model depended on the thing that paints it. The enum now lives with the
model that produces those roles, and the delegate includes the model instead.
The role values are unchanged -- other code passes them to data() as bare
integers.

With these, nothing in launcher/ outside ui/ and plugin/ includes QtWidgets
except Application itself, which is the shell and goes to the UI target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
The QML theme will be token-based, and every text-on-surface pair it ships has
to clear WCAG AA. Those ratios need to be computed and asserted, not written
into a table by hand -- hand-written ratios in this project's design notes have
already turned out wrong more than once.

theme/Contrast provides relative luminance, the contrast ratio, AA/AAA checks
with the large-text variants, and compositeOver() for translucent tokens, which
have no meaningful contrast until they are placed on something. It uses QColor
only; nothing from QtWidgets or ui/.

Decisions worth knowing about:
  - ratio() ignores alpha rather than silently compositing against an assumed
    background. Forgetting to composite should show up, not be papered over.
  - The thresholds are inclusive: WCAG says "at least", so 4.5:1 passes AA.
  - "Large text" is the caller's call. This has no font metrics, so deciding
    what counts as 18pt, or 14pt bold, belongs to the QML side.

The test pins black on white at exactly 21:1, symmetry, the identity case, a
hand-derived reference pair (pure red on white, 1.05/0.2626 = 3.998477), and
both sides of every threshold. The boundary helpers reimplement the inverse
sRGB transfer independently so the test is not the production code checking
itself, and stay 0.1 away from each threshold: QColor's 16-bit channels can
push a value built for an exact ratio to either side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
MeshMC_logic was one static library holding the core, the plugin host and the
entire widget UI. It is now two:

  MeshMC_core   instances, downloads, auth, mod platforms, settings, icons.
                Links QtCore/Gui/Network/NetworkAuth/Concurrent/Xml. Not
                QtWidgets, and it may not know the plugin host or ui/ exist.
  MeshMC_logic  the plugin host and the widget UI, on top of the core. Keeps
                its name, so the application and the unit tests that link it
                need no change.

The plugin host sits in MeshMC_logic for now rather than in a target of its
own: PluginManager renders plugin-supplied widgets and includes MainWindow, so
it and the widget UI depend on each other until the declarative plugin ABI
replaces that.

Leaving QtWidgets off the core's link line is not, by itself, enough to keep it
out. A static library is never linked alone -- unresolved symbols are deferred
to the final executable, which does link QtWidgets -- and on macOS every Qt
framework sits in one directory, so #include <QtWidgets/QWidget> resolves
through the framework search path without the target. Both are real: probing
the core with exactly that include compiled cleanly.

So MeshMC_core_link_check links the whole core archive, every object whether
referenced or not, into a program given nothing else. If any core object needs
a QtWidgets symbol, it fails to link and the build breaks. Verified by adding a
QWidget to tasks/Task.cpp: the build failed on QWidget::show() and its
constructor and destructor, and passed again once reverted. It is part of `all`
and is also a ctest named CoreLinksWithoutQtWidgets.

Getting it to link surfaced what the text scans had missed:

  - minecraft/auth/flows/AuthFlow.cpp included <Application.h> -- angle
    brackets, which is why grepping for "Application.h" never found it -- and
    used nothing from it. Removed.
  - IconList, MMCIcon and DesktopServices were listed in the UI source group
    although the core calls into them and none of them use QtWidgets (QIcon
    and QDesktopServices are QtGui). Moved to ICONS_SOURCES and CORE_SOURCES.
  - MeshMCExternalUpdater's "Checking for updates..." QProgressDialog became
    UiHost::showBusy(), which returns a scoped indicator so it cannot be left
    on screen by an early return.

MacSparkleUpdater.mm is core and imports Cocoa, so the Apple frameworks and
Sparkle now attach to MeshMC_core (PUBLIC), and ThemeManager.mm inherits them.
AppKit is Cocoa, not QtWidgets; the invariant is about the latter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
The first time the QML side shows anything: QmlShell owns a
QQmlApplicationEngine, hands it the core's InstanceList and loads Main.qml,
now an ApplicationWindow listing every instance with its group. It is
deliberately unstyled -- the point is the path from the core's models to the
screen, with real data, before the design system lands on top.

Both interfaces are always built; which one opens is decided at startup.
MeshMC_QML_UI (OFF) picks the default and MESHMC_QML_UI=1/0 in the environment
overrides it, so the two can be compared from one binary without rebuilding.
Compiling the QML path unconditionally is what keeps it from rotting: CI builds
it even while nobody opts in. If the QML fails to load, the launcher logs it
and opens the widget window instead rather than leaving the user with nothing.

The QML window is counted and closed through the same on_windowClose() path as
MainWindow, so the launcher still quits when its last window goes.

Models reach QML as required properties of the root window, not context
properties: their types stay visible to the tooling and a missing one is a load
error rather than a silent undefined. QmlShell::expose() is the single place
that pins C++ ownership -- the engine would otherwise take any parentless
QObject that crosses into JavaScript and delete a core model out from under the
rest of the launcher.

MESHMC_QML_SNAPSHOT=<file.png> renders the real window into an image and
exits. With QT_QPA_PLATFORM=offscreen it never touches a display, so it runs on
a CI runner and can be diffed -- the basis for visual regression checks of the
QML UI. It is also the only way screenshots of this work get taken: capturing
the desktop picks up whatever else is on it.

Build plumbing:
  - MeshMC_core publishes launcher/ as a PUBLIC include root. Core headers are
    included by their path under launcher/, and CMAKE_INCLUDE_CURRENT_DIR only
    covered targets declared in that directory -- not the QML module in
    launcher/qml/.
  - MeshMC_qml links MeshMC_core; MeshMC_logic links MeshMC_qml.
  - The executable imports the static QML plugin with qt_import_qml_plugins,
    for the same reason QmlModule_test does, and its link line switched to the
    keyword signature because CMake will not mix the two forms on one target.
  - QmlModule_test now supplies the root's required model; leaving it unset
    would itself be a load error, which is worth the test catching.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
ThemePalette is the value type the QML theme reads: 35 semantic colour tokens
(surfaces, text, accent, lines, status, interaction, overlays), each a
Q_PROPERTY so QML can bind to it, with Mesh Dark and Mesh Light as the two
shipped palettes. Names describe a role, never a hue -- "accentText", not
"cyan600" -- so a theme can change a colour without every use of it lying.

It is core (QtCore/QtGui only) and its test links MeshMC_core alone, which
proves it.

The brand inputs came from the repository rather than taste: the cyan accent
and the rose from the logo SVG, the green from the legacy GreenDark theme. Two
of those three failed WCAG AA as they stand, and the test is what caught it:

  - The logo's rose (#FF003C) as danger text on its own tinted background is
    4.42:1 on Mesh Dark -- just under 4.5 -- and about 3.4:1 on Mesh Light.
    It is #FF5C79 on dark and #B80035 on light.
  - The legacy green (#96DB59) is far too light for text on Mesh Light; that
    theme uses #1E6823.
  - The light theme's accent (#00798F) is 4.49:1 as text on the canvas, so
    accentText there is one step darker (#005F73) while the accent fill keeps
    the brand value.

Every ratio is measured with theme/Contrast and asserted, never typed into a
table: primary and secondary text >= 4.5 on canvas, surface and raised
surface; tertiary >= 3.0 (it is for large or supplementary text, and says so);
text on accent, on selection, on tooltips and on each status tint >= 4.5; and
borderStrong and focusRing >= 3.0 against canvas and surface (WCAG 1.4.11,
non-text contrast). The test prints the measured table so the numbers quoted
anywhere else can come from it.

The one exception is deliberate and documented: `border` is an 8% hairline for
decoration and is not held to 3:1. Anything that has to be perceived as a
boundary uses borderStrong.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
The QML window now shows what the widget grid shows, in the same order.

InstanceFilterModel (core) replaces the widget layer's InstanceProxyModel for
QML. Its ordering is ported faithfully -- groups compared locale-aware, names
within a group compared with QCollator's numeric mode so "Pack 2" comes before
"Pack 10", and the InstSortMode "LastLaunch" override honoured -- because a
user switching interfaces must not find their instances reshuffled. On top it
adds what QML needs: a live filterText, a group filter and a count for empty
states. It finds the name/group/lastLaunch roles by name from the source
model's roleNames() rather than hard-coding integers, which is what lets the
test drive it with a stand-in model on different role numbers.

It calls sort(0) itself, after resolving those roles. A QSortFilterProxyModel
does not sort until asked, so leaving that to callers meant one forgotten call
showed instances in discovery order -- and sorting before the roles resolve
would order by the wrong ones.

The old proxy also turned Qt::DecorationRole into a QIcon through
APPLICATION->icons(). The core cannot reach APPLICATION, so iconKey is now
forwarded as the plain string InstanceList already stores, and turning it into
a picture is the image provider's job.

IdSelectionModel keeps the selection as a set of instance ids. QML has no
QItemSelectionModel, and neither rows nor persistent indexes survive a
filtering proxy: a persistent index is invalidated the moment its row drops
out of the filter, which is exactly what happens while typing a search.

InstanceIconProvider serves image://instanceicon/<key>. It is a Pixmap-type
provider on purpose: Qt only guarantees those run on the GUI thread, and QIcon
and QPixmap must not be touched anywhere else. requestedSize already arrives
in device pixels and is not scaled again; a "?rev=N" suffix is ignored so
callers can bust QML's image cache when an icon changes; unknown keys fall
back through IconList to the default icon, and to a plain pixmap if even that
fails, so QML never receives a null image. Its test initialises multimc.qrc
and the icon theme search path explicitly -- built-in icons resolve through
QIcon::fromTheme -- because a test in which every key quietly falls back to
the default proves nothing.

QmlShell owns the filter and the selection and hands both to Main.qml as
required properties; they are declared before the engine so they outlive it.

Verified by rendering the real launcher offscreen against a scratch data
directory: all nine instances, their own icons, ungrouped first then
"Modpacks" and "Vanilla", and "Pack 2" ahead of "Pack 10".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
Every size, colour, radius and duration the QML interface uses comes from here,
so nothing on screen hard-codes one. After `import MeshMC.Theme`:

  Theme.palette.<token>  the 35 ThemePalette colours (already WCAG-checked)
  Theme.dark / .mode     "system" | "dark" | "light", writable
  Theme.space            2 4 8 12 16 24 32
  Theme.radius           4 8 12 16 pill
  Theme.control          28 36 44 heights
  Theme.icon             16 20 24
  Theme.motion           120 180 260 ms, OutCubic
  Theme.font             Inter / JetBrains Mono (bundled later; Qt falls back)
  Theme.type.<style>     caption label body bodyStrong title heading display,
                         each a size, a weight and a line height

ThemeService (C++) owns the palette. Switching emits one changed() and replaces
the whole palette at once, so no binding ever sees half a theme. In "system"
mode it follows the OS: QStyleHints::colorScheme() is 6.5+ and the floor is
6.4, so it decides from the lightness of the application palette's window
colour -- the same test the widget ThemeManager already makes -- and re-checks
when that palette changes.

The module sits in launcher/qml/Theme/ for the resource-alias reason the MeshMC
module has its own directory, and is linked into MeshMC_qml so everything that
loads the QML gets it.

Its test reads tokens from QML, flips the mode both ways and counts changed().
Two build details it needed, both non-obvious:
  - It links MeshMC_qml_theme explicitly, because it observes ThemeService from
    C++; qt_import_qml_plugins only brings in what QML reaches.
  - It links the plugin target MeshMC_qml_themeplugin explicitly, because its
    QML is inline (setData) and qmlimportscanner cannot see an import in a
    string, so it cannot work out that the module is needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
The plugin ABI floor is about to rise -- the next version removes the
QtWidgets-based plugin UI -- and that will make older modules refuse to load.
Today a refused module simply disappears.

PluginLoader::loadModule() ran its ABI range check before reading the module's
name or version, unloaded the module on failure and returned it unloaded, and
scanDirectory() then skipped anything unloaded. So a module built for the
wrong ABI never reached PluginManager or the Plugins dialog: the only trace was
a log line naming a file path, and the user had no way to learn that a plugin
needed updating.

Now the name and version are read right after the magic check, before the ABI
gate, and a mismatch keeps the module as disabled-but-visible -- the same
treatment the loader already gives modules that fail their signature or are
superseded -- with a new reason, AbiTooOld or AbiTooNew, and a message such as:

  "Foo" was built for plugin ABI 2; this MeshMC supports 4-4. It needs to be
  updated by its author.

What is read early is deliberately narrow: only name and version, whose offsets
MMCOFormat.h documents as unchanged since the format's first revision. The
fields that moved with later ABIs are left alone, nothing in the module is
called, and every existing bounds, magic and symbol check stays where it was.

A side effect worth having: PluginDependencyResolver now reports a dependency
on such a module as disabled, with the reason, instead of "not found".

MMCO_ABI_VERSION and the floor are unchanged; this only makes refusal explain
itself. loadModule() needs a real dlopen-able library, so the test exercises
classifyAbiMismatch() -- the reason and message logic loadModule() delegates to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
The QML window is now a sidebar, a top bar and a card grid, drawn entirely
from MeshMC.Theme tokens by two new modules:

  MeshMC.Style       a Qt Quick Controls style: Button (default, flat,
                     highlighted), ToolButton, TextField, TextArea, CheckBox,
                     RadioButton, Switch, ComboBox, SpinBox, Slider,
                     ProgressBar, BusyIndicator, ScrollBar, ScrollIndicator,
                     ToolTip, Menu, MenuItem, MenuSeparator, Popup, Dialog,
                     Label, Frame, Pane, TabBar, TabButton, ItemDelegate.
                     Hover and press are overlays; disabled drops to
                     textDisabled; the focus ring shows only on keyboard focus.
  MeshMC.Components  InstanceCard, InstanceGrid, EmptyState, SidebarNav,
                     NavItem, AccountChip, TopBar, SectionHeader, StatusBadge,
                     and Gallery, a page showing every one in every state.

Main.qml wires them to the core: the search box drives the filter model's
filterText, and a click in the grid becomes the id selection. The account chip
reads "Guest / Not signed in" -- true for a launcher with no account yet, and
not a made-up name.

The style is selected in QmlShell before the first engine exists, because Qt
Quick Controls binds its style on the first QtQuick.Controls import and cannot
change it afterwards. It falls back to QtQuick.Controls.Basic: without that
IMPORTS line, any control the style does not define -- DialogButtonBox,
RoundButton, Drawer -- fails to load.

The plugin targets of all three modules are linked PUBLIC from MeshMC_qml, not
just their backing libraries. qt_import_qml_plugins only imports plugins it
finds in the link closure, and a missing one fails at load time after a green
build.

Launcher defaults to the dark theme, as decided for this design; "system"
follows the OS and "light" is available.

Bugs found by rendering the real window, not by the tests:

  - Instance names that wrapped drew their second line on top of the first.
    The theme's lineHeight is a multiplier, and InstanceCard paired it with
    Text.FixedHeight, which reads it as pixels: 1.45 px per line.
  - The same misreading sized the cards. InstanceGrid summed lineHeight as
    pixels and reserved about four pixels for three lines of text, so the
    centred content spilled out of the top and bottom of every card.
    Two separate components making the same mistake says the contract invited
    it, so every type style now also offers lineHeightPx.
  - The grid sat flush against the sidebar with the gap only on the right:
    each cell leaves its gutter on the right. The right margin now hands that
    gutter back, so both sides get the same page padding.

The gallery's sample account used a real person's name; it is "Steve" now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
…eeds

Instances now tell QML what they run and how they look:

- InstanceList gains gameVersion, loader (empty for vanilla, so the UI
  can word it in any language) and iconTint roles. The tint is the icon's
  opacity- and saturation-weighted average, cached in IconList until the
  icon changes.
- InstanceFilterModel learns exactGroup (empty = ungrouped only),
  instanceId and recentFirst, and publishes groups and firstId. Roles are
  now resolved before the base class filters the first rows; before, a
  filter set ahead of setSourceModel() judged every row with unresolved
  roles and dropped them all.
- QmlShell hands QML a recent-first model, a one-row hero model and one
  cached model per group, plus the default account's name and kind.
  Play, stop, edit, open folder, new instance, settings and accounts are
  signals that Application wires to the same actions the widget UI runs;
  the new-instance dialog flow is lifted out of MainWindow into a static
  helper both of them call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
The first QML main window was a grid of mostly empty boxes with a tiny
icon, placeholder glyphs for navigation and no way to play anything.
This replaces it with a screen built around the one thing a launcher is
for:

- A hero for the instance you most likely want next -- the selected one,
  else the last played, else the first -- with a big Play button, Edit,
  Open folder, and what it runs: loader, Minecraft version, when it was
  last played and for how long in total.
- Groups as collapsible sections of cover cards. Each cover is tinted
  from the instance's own icon, carries the name and "Fabric 1.21.4" with
  the last-played time, and shows Play (or Stop while running) and a
  menu on hover. Double-click plays, right-click opens the menu.
- A sidebar with the real logo, Library/Discover/Settings, the four most
  recently played instances with a quick Play, and the account -- or a
  sign-in prompt instead of a fake "Guest".
- A monochrome line-icon set (Lucide geometry, ISC) behind MeshIcon and
  Icons.url(), replacing the Unicode glyphs.
- Buttons with weight and a primary fill that has depth, a search field
  with icon, clear button and Escape, Ctrl+F and Ctrl+N.

Searching hides the hero and lists only matches; an empty library or an
empty search each get their own empty state with the next action.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
The gallery still used the old component APIs and the deleted
InstanceGrid. It now shows every new piece in its states -- cards at
rest, hovered, selected, running and never played; the hero; sections;
Play/Stop in both sizes; tags, search, the sidebar with recents and the
three account states -- plus the whole icon set by name, all from its
own sample data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
A new image provider, image://accountface/<id>, draws the default
account's face with its hat layer from the skin the launcher already
stores, scaled nearest-neighbour so the pixel art stays crisp. The
compositing is a pure helper with its own test (transparent hat pixels
must not cover the face).

QmlShell publishes the face url with a revision that moves on every
account change, since QML caches images by url and a new skin keeps the
same account id. The chip draws the face over the initial, so an account
without a skin still shows its letter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
SettingsAdapter wraps a SettingsObject for QML: value, defaultValue,
contains, setValue, reset, and one valueChanged(id, value) that fires
for every change, whoever made it, including resets.

setValue converts what QML sends to the type of the setting's default
before storing it: QML numbers are doubles, and an int setting such as
MaxMemAlloc must not end up written as "4096.0".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
Settings in the sidebar now opens a real page instead of the classic
dialog. Sections: General (sort order, new-instance behaviour, folders),
Java & memory, Minecraft (game window, play time, native libraries),
Console, Appearance and Custom commands. Every change is saved the moment
it is made, through SettingsAdapter; there is no OK/Cancel.

- Memory is a slider over the RAM this machine has, spelled out in GiB,
  warning past three quarters of it. Lowering the maximum below the
  starting memory pulls that down too, instead of silently swapping the
  two as the widget page did.
- Window width and height grey out while "start maximized" is on.
- Accounts, language, proxy, external tools and log upload are listed
  under More and open straight on their page of the classic dialog --
  openSettings() now takes a page id.
- The QML colour scheme is a setting of its own (UiThemeMode) and is
  applied at startup; changing the sort order re-sorts the library.

Rows are small reusable pieces -- SettingSwitch, SettingChoice,
SettingNumber, SettingText -- reading through one SettingsStore that
turns config-file strings like "false" into real booleans.

Also: fonts fall back to the platform's UI and monospace fonts until
Inter and JetBrains Mono ship, and the off switch and slider knob are
visible on dark cards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
Pressing Play used to give the library nothing to show until the game
window appeared; the progress lived only in the widget dialogs.

- LaunchTask now relays the running step's status and progress as its
  own, so the launch as a whole is observable -- until now only the
  Update step reported anything, and only to itself.
- LaunchProgressTracker watches one launch task and turns it into a
  status text and a 0..1 progress (-1 when there is no telling),
  coalescing updates to about ten a second; going idle is immediate.
- InstanceList publishes that as launchStatus and launchProgress, and
  emits isRunning changes it never forwarded before. The tracker goes
  idle when the game process is up: the task keeps running as long as
  the game does, but from then on the instance is simply running.

In the library a launching card dims its cover, runs a bar along its
foot and says what it is doing ("Downloading assets 45%"); the hero's
Play button turns into "Starting..." with the same status and bar
underneath. The gallery shows the new state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
The modpack browser lived in widget pages, with the version fetch and
the task building in the view. ModrinthModpackModel is the widget-free
replacement a QML page can drive:

- query, loader, gameVersion and sort in (sortOptions lists the sorts),
  search()/fetchMore() to run, one row per pack out (title, author,
  description, logo url, downloads, follows, updated, versions,
  categories), with searching, canFetchMore and error to show state.
- loadDetail() fills `detail` with the project's markdown body and its
  versions (game versions, loaders, date, download url, featured).
- install() builds exactly what the widget flow builds -- an
  InstanceImportTask for the version's mrpack, trusted source, pack
  source hint, name/group/icon/target dir -- wraps it the same way and
  returns a TaskWatcher. The pack icon is taken from the widget page's
  cache if it is already there; nothing is downloaded just for it.

TaskWatcher is the QML face of any running Task: status, progress (-1
when indeterminate, at most ~10 updates a second), running, succeeded,
failed, error.

ModrinthApi's modpack search URL learns optional game version and
loader facets, and the pack index keeps a few more fields it already
receives; the widget page's calls are unchanged. The parsing is tested
on canned replies, TaskWatcher on a hand-driven task.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
Editing an instance meant the widget InstanceWindow; nothing described a
single instance to QML. InstanceDetails does, for one instance at a time:

- its settings as a SettingsAdapter (per-instance overrides), its notes,
  name and folders;
- its loader mods (ModFolderModel) with enable, delete and install, and
  contentChangesAllowed, false while the game runs -- the rule the mods
  page enforced; its worlds (WorldList) with delete. Both start watching
  their folders while the bridge exists, as their pages did;
- its screenshots, through a new ScreenshotListModel: images in one
  folder, newest first, a debounced watcher, remove() to the trash;
- the log of its current launch, through InstanceLogBridge, which
  follows a new launch replacing the old LogModel (as LogPage did) and
  offers clear, text and suspend;
- its PackProfile for a read-only component list.

ScreenshotThumbnailProvider is an asynchronous image provider for
image://screenshot/<encoded path>: decoding and scaling run on its own
thread pool, results are cached by path, mtime and size, and missing
files fail cleanly. Tests cover the log bridge, the screenshot model
and the thumbnailer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
Discover used to open the classic new-instance dialog. It is now a page
of its own:

- A search field (searching once typing pauses), loader chips (Any,
  Fabric, Forge, NeoForge, Quilt) and Modrinth's sorts; results as rows
  with logo, name, author, the pitch and a compact download count, more
  loaded as the end of the list comes into view.
- Opening a pack shows its header, description and markdown body, a
  version picker that starts on the version the author features, the
  instance name, and Install -- with progress in place and a way to the
  library once it is done. The install is started by the shell and owned
  by C++, so it survives leaving the page.
- The first search runs when the page is first shown, never at startup;
  "nothing found" only appears after a search, and a failed request says
  so with a retry. CurseForge, FTB, ATLauncher and Technic are one click
  away in the classic dialog.

Main routes the sidebar's Discover to the page and stops any page's
minimum width from widening the window. QmlShell registers the
screenshot thumbnail provider and hands QML the modpack model, the
install entry point and per-instance details; MESHMC_QML_SNAPSHOT_DELAY
lets a snapshot wait for network results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
Edit on a card, the hero or the menu used to open the widget
InstanceWindow. It now opens an instance page in the main window: the
library's banner (Play/Stop with launch progress, folder, what it runs)
over tabs --

- Overview: Minecraft version, loader, last played, time played, mod and
  world counts, and the instance's notes, saved on leaving the field.
- Mods: each with an on/off switch, removable after a confirmation;
  locked while the game runs, as the classic page was.
- Worlds: name, game mode and last played, icon when the world has one;
  deleting one asks first and says it cannot be undone.
- Screenshots: a thumbnail grid, newest first, decoded off the UI thread;
  click opens the image, the trash button moves it to the trash.
- Log: the live output of the current launch, coloured by level, with
  Follow (scrolling up turns it off), Copy all and Clear.
- Settings: memory, Java, JVM arguments, game window, console and custom
  commands, each following the launcher-wide value until switched to
  "custom for this instance"; switching back drops the instance's
  values. Locked while running.

Everything not here yet -- versions, servers, backups, resource packs --
is behind "Classic editor" on the banner.

Setting rows now read and write through a SettingsSource, the launcher's
by default or any SettingsAdapter, so the same rows serve the global and
the per-instance pages. A ConfirmDialog names its action ("Delete
world") instead of offering a bare OK. QmlShell gains a one-row model for
the page, separate from the library's hero.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
The mod folder model lists files in whatever order the directory hands
them out. InstanceDetails now gives QML a case-insensitive name-sorted
proxy instead, and maps rows back to the folder model when a mod is
switched on or off or removed, so the row the user clicked is the one
that changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
Two flows the QML shell still sent to widget dialogs:

Accounts. AccountsController wraps the AccountList for QML: set or
clear the default, remove, refresh, add an offline account with the
classic page's rules (a Microsoft account must exist first; no empty or
duplicate names), and loginMicrosoft(), which runs the same
browser-based OAuth flow the widget dialog ran and publishes its status,
the browser url, success or failure, and cancel/openBrowser. The first
account signed in becomes the default, as before. AccountList gains the
roles QML needs: isDefault, isMSA, stateKey and accountId (the id the
face provider takes).

New instances. NewInstanceController offers the Minecraft version list
(releases by default, snapshots and old versions on request, newest
first, loading and error state), a loader choice with that loader's
versions for the chosen Minecraft version, a suggested name, the
existing groups, and create(), which runs the same creation task the
widget dialog does. InstanceCreationTask can now add the loader
component at creation, with the same PackProfile calls "Install loader"
makes afterwards -- the classic dialog could not pick one up front. The
loader version defaults to the newest, also when the Minecraft version
changes under an already loaded list.

QmlShell exposes both; the new-instance controller is made on first
use, so starting the launcher does not fetch the version list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
- The account chip opens an Accounts page instead of the classic
  settings: each account with its face, Microsoft or Offline, whether it
  still works, and Default -- or "Use this account". Signing in with
  Microsoft shows a sheet that says the browser has opened, follows the
  sign-in, can reopen the browser page, and ends in "Signed in" or the
  reason it failed with Try again. Removing asks first; offline accounts
  are added from a small form that explains when they are allowed.
- New instance (button, Ctrl+N, the empty library) opens a dialog:
  name, group, the Minecraft version list with release/snapshot badges,
  "Recommended" and dates, snapshot and old-version switches, and a mod
  loader with its version. It starts on the newest version, names itself
  after the choice ("26.3 Fabric") until the user types a name, and shows
  progress while creating. Importing and other sources are one click away
  in the classic dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
…om QML

The management actions of the widget main window, without its dialogs,
on QmlShell:

- renameInstance: trimmed, line breaks folded to spaces, never empty --
  what the widget's inline rename committed (sanitizedInstanceName(),
  tested on its own).
- setInstanceGroup and a `groups` property (the library's own list of
  groups, already kept up to date for its sections).
- setInstanceIcon, the IconList as `iconsModel` and `iconsDir`, and
  importIcon() for a picked file, as the picker's "Add icon" did.
  IconList gains roleNames: key, name and isBuiltin.
- duplicateInstance: the same copy task the copy dialog starts, with its
  defaults (saves copied, play time kept), returned as a C++-owned
  TaskWatcher.
- deleteInstance: refused while the game runs; to the trash when
  possible, deleted otherwise; the selected instance setting is cleared
  as the widget did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
The instance menu (right-click, or "..." on a card or the hero) gains
Rename, Change icon, Move to group, Duplicate and Delete, next to Play,
Open and Open folder:

- PromptDialog asks for one line -- a name, a group -- with Enter to
  confirm and existing groups as one-click chips.
- IconPickerDialog shows every icon MeshMC knows, built-in and from the
  icons folder, which it can open to add more.
- Delete asks first and says what goes with the instance; it is disabled
  while the instance runs.
- A Toast reports outcomes that need no dialog: a copy starting and
  finishing, a delete, or why one failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
grxtor and others added 11 commits September 23, 2026 05:21
GitVersioning and ErrorOracle enumerated instances with instance_get_id()
and passed that pointer on after calling instance_get_path(). The host
returns strings in one per-module buffer, so the second call overwrote
the id and every per-instance surface was anchored to garbage -- which
is why GitVersioning's history page never matched an instance. The id
is now copied first, as SystemTray already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
Two things still assumed the widget main window:

Plugins. main_window_show/hide/is_visible and the close filter resolved
a QWidget named "MainWindow"; under the QML shell they did nothing. They
now fall back to the shell's QQuickWindow (show/hide/raise/
requestActivate), and a plugin that vetoes a close keeps the QML window
open exactly as it keeps the widget one: QmlShell now reports a close
from its own event filter, which a vetoing plugin's filter runs before,
instead of inferring it from the window turning invisible -- which a
plugin's hide() would also have triggered. MMCO_HOOK_UI_MAIN_READY is
dispatched once when the shell is shown, with null widget handles (no
in-tree plugin reads them), so plugins that set up per-instance
surfaces there -- GitVersioning's version history -- now do so under
QML too.

Questions. QmlUiHost implements UiHost for the shell: each question
(message, confirm, choose, blocked mods, untrusted files, update) is
published as a request object and the core waits in a local loop until
QML answers; nested requests stack, and quitting or destroying the host
rejects whatever is pending. Application only routes to it once the QML
dialog has said it is ready to show requests, so nothing can wait on a
question no one can see; until then the widget dialogs answer as
before. UiRequestDialog shows every kind in the shell's style -- blocked
mods with their download pages and "found" as the folder is watched,
untrusted files with a deliberate pause before they can be trusted,
updates with version and release notes -- and BusyOverlay covers
showBusy(). QmlUiHost is tested headless.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
On first run the widget SetupWizard (language, Java) had to be finished
before any window appeared -- also when the QML shell was the UI. The
shell now skips it and runs its own onboarding over the main window:

- A brand panel with the steps, and one step at a time: the language
  list (native names, searchable, how complete each translation is),
  applied the moment it is picked -- the QML engine retranslates
  live -- and saved; Java and memory (automatic downloads, the memory
  slider, and the Java installs found on this computer, redetectable);
  an account step when there is none yet (sign in now, which opens the
  Accounts page's Microsoft flow, or later); and "Start playing".
- Only the steps still needed are shown, decided by the classic
  wizard's rules, now pure functions with tests (language unset; Java
  unresolvable or the machine's hostname changed). An empty Java list
  says what happens next instead of looking broken.

QmlShell exposes setupSteps, finishSetupStep(), the translations model,
selectLanguage(), the Java install list with detectJava()/useJava();
LauncherContext gains translations() and javalist() so the shell,
outside the widget layer, can reach them. The widget path still shows
the classic wizard exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
Qt 6.10 added QtQuick.Controls.SearchField. Wherever a file imports
QtQuick.Controls, that type now wins the name over our component, so
TopBar's `SearchField { placeholderText: ... }` failed to load with
"Cannot assign to non-existent property" and took the whole main window
with it. CI builds with Qt 6.10, so a release would have shipped this;
it surfaced here when the local Qt moved to 6.11. The component and its
five users now say SearchBox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
Our own QML modules are compiled into the launcher, but the Qt ones they
import (QtQuick, QtQuick.Controls and its impl/Basic, Layouts,
Templates, QtQml...) are loaded at run time -- and no package carried
them, so the QML shell would die on first launch everywhere except a
developer's machine:

- Windows: windeployqt ran with --no-quick-import, which skips QML
  entirely; it now gets --qmldir launcher/qml so qmlimportscanner sees
  our imports. The NSIS installer lists deploy directories by hand and
  had no qml/, so Setup.exe would have lacked it even then; it installs
  and removes qml/ now.
- macOS: qt_deploy_runtime_dependencies() never deploys QML modules.
  The install step now asks qmlimportscanner which Qt modules
  launcher/qml imports and copies exactly those into
  Contents/Resources/qml (following Homebrew's symlinks), deploys the
  frameworks they need (e.g. QtQuickControls2Impl) through
  ADDITIONAL_LIBRARIES with destination-relative paths, and fixes their
  rpaths. Verified by installing into a scratch prefix and running the
  installed app offscreen.
- qt.conf now carries QmlImports -- Qt 6 ignores Qml2Imports -- and its
  macOS paths are relative to Contents/, as Qt resolves them there.
- Linux: the package action copies the imported Qt QML modules into
  qml/ and hands their plugins to sharun; the portable launcher script
  exports the QML import path. The AppImage's .env gets it too (only CI
  can confirm sharun expands $APPDIR there), and its second line no
  longer overwrites the first (> instead of >> dropped
  LAUNCHER_DISABLE_GLVULKAN).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
update.sh built the translation template from .h, .cpp and .ui files
only, so none of the QML shell's 419 qsTr() strings could ever reach
translators, and every language would have shown the new UI in English.
lupdate reads QML itself; the file list now includes .qml. Contexts come
out as the component names ("InstanceCard", ...), which is exactly what
qsTr() looks up at run time, so the existing .po pipeline serves them
unchanged once the template is regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
LaunchController asked and told the user things with widget dialogs of
its own. Its questions now go through LAUNCHER->uiHost(), so under the
QML shell they appear in the shell's question dialog and under the
widget UI in message boxes as before:

- "No accounts", "play the demo?", refresh failures, the account being
  gone, an unloadable instance profile, profiler errors and the three
  stop/kill questions become message()/confirm().
- The demo and offline player-name prompts use a new UiHost::askText()
  (QInputDialog in the widget host; a "text" request with a prefilled
  value in QML, answered with accept(text)).
- Choosing among several accounts with no default: the widget UI keeps
  ProfileSelectDialog and its "use as default" box; the QML shell asks
  with choose() and never changes the default.
- The console: Application::showInstanceLog() opens the widget console
  as before, or asks the shell to show the instance page on its Log tab
  (QmlShell::openInstanceLog).
- Under QML the account-refresh wait uses showBusy() instead of a
  progress dialog, and the launch-step progress dialog is skipped: the
  instance card already shows the running step. That drops their
  "Play offline" and "Abort" buttons in the shell for now.
- JavaCommon gains a UI-free jvmArgsWarning(); the widget-only
  checkJVMArgs() is unchanged for the Java settings page.

One widget-side difference: "Kill Minecraft?" now defaults to No, as
every confirm() does, where its dialog used to default to Yes.
ProfileSetupDialog (an account that owns the game but has no profile
yet) stays a widget dialog; it validates the name against the network
as you type. The QML dialog handles the new text kind, and the shell
opens the Log tab when the launch flow asks for the console.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
Three failures on PR #190, none visible on the macOS dev machine:

- Linux: InstanceList returned totalTimePlayed() -- an int64_t -- as a
  QVariant. int64_t is long on LP64 Linux and QVariant has no
  constructor for long, so the conversion was ambiguous; on macOS it is
  long long and happened to match. It is now returned as qint64.
- macOS arm64: check-plugin-independence.sh used mapfile, which the
  runners' bash 3.2 does not have; the step died with "command not
  found". It collects the targets with a while-read loop instead
  (checked with /bin/bash 3.2: 15 .mmco targets clean).
- MinGW: the MSYS2 environment installed Qt without qt6-declarative, so
  configuring failed on the Qml component the QML shell requires.

The macOS x86_64 job failed installing ccache (a Homebrew checksum
mismatch on the runner), unrelated to this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
Language was one of the pages still sent to the classic settings window.
Appearance now has a Display language row over the same translations
model the first-run flow uses; picking one switches the whole shell at
once (the QML engine retranslates) and is saved. It is removed from the
classic pages listed under More.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
…ttings to QML

InstanceDetails now serves name-sorted models for resource packs, shader
packs and (on instances old enough to use them) texture packs, next to
the mods model, plus kind-generic setEnabled/remove/install invokables;
the mods-specific ones forward to them. QmlShell::applyProxySettings()
applies the stored proxy through a new LauncherContext entry point, so
the QML settings page can change it without the widget dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
MESHMC_QML_ROUTE (e.g. "theme=light;instance=<id>;tab=mods") opens a
page, instance tab, settings section, dialog or the gallery at startup.
Together with MESHMC_QML_SNAPSHOT it lets a script picture any screen
without editing the QML first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
@YongDo-Hyun YongDo-Hyun added this to the Backlog milestone Sep 23, 2026
@YongDo-Hyun YongDo-Hyun added the AI label Sep 23, 2026
@YongDo-Hyun YongDo-Hyun self-assigned this Sep 23, 2026
grxtor and others added 15 commits September 23, 2026 13:15
Library and instance pages now use the player's own newest screenshot as
art: InstanceList gains a cached coverImage role (dropped when a game
session ends) and a CoverArt component fills cards and heroes edge to
edge, masking its corners to the card colour since Qt 6.4 has no rounded
clip. Instances without screenshots get a tinted plate with their icon.
Text over artwork uses new Theme.media tokens so it stays light on a
dark fade in both themes.

- Library: sort menu (name, last played, time played) and a list view;
  the sidebar gains an active-page indicator and running markers; page
  changes and group collapse animate; toasts are reworked.
- Instance page: an overview dashboard (stats, recent screenshots, mods
  preview, notes), a Content tab for mods, resource packs and shader
  packs, and reworked worlds and screenshots tabs.
- Discover: a card grid with gallery covers, categories and update
  times from Modrinth, skeleton placeholders, and a richer detail view.
- Accounts: a full-body skin render and a hero card for the account
  games launch with.
- Dialogs and controls: one modal style, popups with shadows, an icon
  picker in the new-instance dialog, settings layout fixes.

Also makes the tests pass on Linux: natural name order no longer relies
on QCollator's numeric mode, which Qt without ICU ignores, and the QML
module test links MeshMC_qml and selects the app's controls style.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
A settings group box deletes its child widgets, the surface's root among
them, before the RendererOwner sibling that owns the RenderedSurface;
the surface then deleted the root a second time. Hold the root in a
QPointer so the surface notices, and refuse updates once it is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
The teal-grey neutrals and the icon-tinted plates read as washed out.
ThemePalette now builds three schemes -- hue-less graphite with an
amethyst accent (the default), warm charcoal with lava orange, and cool
navy with diamond blue -- each in a dark and a light variant, sharing
the status colours so danger or success never change meaning. Every
scheme and mode is held to the same contrast checks as before.

ThemeService gains a scheme property next to the mode, and Settings >
Appearance shows the schemes as miniature launchers to pick from.
Instance tint plates are desaturated so they stop turning into murky
teal and olive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
…room

Add content kept a single install watcher: while one download ran the
Install button and the version list were locked, and picking another
project dropped the running install from view, so the launcher looked
frozen. Installs are now tracked per project and run side by side; each
result row shows its own progress and state, and the toolbar counts the
installs in flight.

The picked project's panel is compact (logo, title, author, a three-line
description that expands on click) so the version list gets the rest of
the height, and the instance page's banner shrinks to a slim strip on
every tab except Overview.

Also fixes cards whose hover lift was clipped along the top of a library
group, and Discover covers poking out past the card's rounded corners.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
Under the QML shell nothing may open a Qt Widgets window, but several
paths still did:

- Creating an instance from QML called MainWindow::createInstanceFromDialog
  with a null parent, which crashed; the slot now refuses under QML.
- A Microsoft account without a Minecraft profile opened the widget
  ProfileSetupDialog from the launch flow. UiHost gains setupProfile(),
  shown in QML as a name picker that checks availability like the widget
  dialog, including ignoring stale answers to earlier checks.
- The "no accounts" launch path opened the classic settings page; it now
  says what is missing through the UiHost.
- Plugin message, confirm, text and file dialogs go through the UiHost
  (a new pickFile() request renders a native QML file dialog), and
  ui_modal_run no longer builds widgets under QML.
- Update markers found at startup are held until the QML window can show
  them instead of popping raw message boxes before any UI exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
"Import or more sources" and Discover's other-platforms button used to
open the widget NewInstanceDialog, the crash users hit under the QML
shell. The dialog now has an Import mode: pick a .zip, .mrpack or
instance export, drop one on the dialog, or paste a download link; the
name follows the file, progress and errors show inline, and the import
runs through InstanceImportTask like the classic import page. A local
path is only accepted when it exists and looks like an archive. Discover
opens the dialog in this mode, and nothing in QML reaches the widget
dialog any more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
Register UiPalette (and the sidebar, reduce-motion and cat settings the
redesign reads) and apply the saved palette when the QML shell starts.
Adds a scheme= step to the dev route for review snapshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
… log upload in QML

Microsoft accounts get a skin and cape editor on the Accounts page:
upload a 64x64 or 64x32 PNG with classic or slim arms, reset the skin,
and pick an owned cape, all through the existing SkinUpload,
SkinDelete and CapeChange services with a refresh afterwards.

The Proxy, External tools and Log upload settings move from the
"More" rows into SettingsPage sections of their own, so under the QML
shell settingsRequested no longer opens the widget settings dialog for
any page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
…ay bar

The approved layout: menus stay in a left sidebar with a sliding
selection pill, hover states and a remembered collapse to an icon rail;
the account moves to a profile button in every page header's title row,
with a menu to switch, manage or add accounts, open skin and cape, or
sign out; a persistent play bar along the bottom shows the selected
instance with a searchable picker and puts PLAY in the bottom-right
corner, turning into launch progress with cancel and into Playing/Stop.
The library's Continue playing hero is gone and the instance header no
longer carries its own Play button.

Also: combo box chevrons no longer clip at the border, Discover keeps
its import button in the filter row, modpack cards show loading
skeletons, a colour plate instead of an upscaled icon when a pack has
no gallery, two-line titles and a +N chip for tags that do not fit.
Hover changes a single property; the Play button lost its glow, bevel,
scale and idle glint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
…e crash handler

Home page data: InstanceList gains a hasCrashed role, and a new
RecentWorldsModel lists the most recently played worlds across every
instance. Its scan (directory listing and level.dat) runs on the thread
pool, is debounced, and reruns only when instances are added or removed
or one stops running.

Windows startup hang (bisected to 7ab7813): the cover image role
scanned each instance's screenshots folder synchronously inside
InstanceList::data(), on the GUI thread, while the Library's delegates
were being built. With real screenshots on NTFS that stalls the first
layout; the scan now runs on the thread pool and publishes through
dataChanged, discarding results that went stale while it ran.

The crash signal handler no longer calls Qt, reads settings or flushes
the log: the reporter's command line is prepared at startup (and again
when the paste key changes) in fixed buffers and spawned with
CreateProcessW or posix_spawn. The three exit() calls in Application
are spelled QCoreApplication::exit(). Also fixes a latent crash when an
InstanceList with instances is destroyed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
Home comes first in the sidebar and opens at startup, with one job:
get back to what you were doing. A welcome line, Jump back in (the
three most recently played instances with cover art, loader and
version, play time, a calm Crashed last time chip and a secondary Play;
the row scrolls sideways and lets the next card peek in), Recent worlds
across every instance, and a strip of every instance's icon linking to
the Library. It reads only local data, so it opens instantly and
offline; the dock's PLAY stays the one accent control on the page.

Also, defensive against the Windows startup hang: a library section's
fold animation can no longer run during the first layout, and Discover
only searches Modrinth once it is actually the shown page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
…ntrols

The first two waves of the design plan, aimed at the "looks AI-made"
feedback:

- Inter now ships with the app (four static weights, OFL) as resources
  of the Theme module; before this the family was only used when it
  happened to be installed, so almost everyone saw the system font.
- The dark surface ladder of all three palettes is widened so canvas,
  panels, cards and popups read as separate layers, and borders are
  visible (alpha 20 -> 38 dark, 32 light); tests pin both minimums.
- One selection grammar: the sidebar's sliding pill and accent bar is a
  shared component that Settings' section list now uses too; tab
  strips keep the underline and tile pickers the ring.
- Hover changes a single property: combo boxes gain a border hover,
  cards no longer lift and inline Play and View pack buttons fade in
  instead of overshooting.
- Theme.radius.xs, radius literals mapped to tokens, and contract
  comments for type ceilings, motion and selection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
…d accounts

Wave 2 of the design plan, for "the backgrounds have nothing":

- Library shows a faint, softened wash of the most recently played
  instance's own screenshot behind the page, Home a band of it behind
  the welcome and Jump back in rows (PageBackdrop).
- Settings, the New instance dialog header and an empty or loading
  Discover get a quiet static block-grid texture (AmbientPattern)
  instead of a flat canvas; the play bar uses the instance's tint and
  the same texture when it has no art rather than flat black.
- One accent fill per screen: New instance and the Accounts toolbar
  are secondary buttons, the dock's PLAY stays the accent.
- Discover cards show one tag; the detail view lists them all. The
  MeshMC mark signs the Discover empty state and the New instance
  dialog header (BrandMark).
- Accounts and the skin editor drop the ornamental gradient and glow
  for the real skin, or a per-account tinted silhouette
  (SkinSilhouette); empty states can sit in the upper third.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
First batch toward retiring the classic instance editor under QML:

- Version tab: the PackProfile components with state and problems;
  change the Minecraft version, install Forge, NeoForge, Fabric, Quilt
  or LiteLoader from a full-height version list for the instance's game
  version (reusing the new-instance dialog's version proxies), change a
  component's version, enable or disable, move, customise, revert or
  remove it. Work runs as tasks with progress in the tab; destructive
  actions are confirmed. PackProfile gains the roles and invokables for
  this; LoaderInstaller re-enables a switched-off loader before the
  resolve, as the widget dialog does.
- Log tab: Live or Other logs, to read, copy, open or delete the
  instance's logs/ and crash-reports/ files (OtherLogsModel).
- Game options tab: options.txt keys and values with search.
- Under the QML shell, Play on a running instance, or one that cannot
  launch, raises the window on that instance's Log tab instead of
  opening the widget instance window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>
…ader logos

Work in progress from the QML migration, committed as one snapshot:

- Instance page tabs: Servers, Backups, Data packs and Modpack tabs with
  their models/controllers (ServersListModel, BackupController,
  WorldDataPacksController, ManagedPackController) and tests.
- New default palette "grass" (Minecraft green on hue-less graphite), the
  PlayButton and palette picker follow it; original pixel-art PixelArt
  singleton, block textures, landscape fallbacks and empty-state art.
- The Minecraft cat on the play bar: CatRig (own skeleton for the
  Sketchfab model by JanesBT, CC BY 4.0, see Cat/assets/CREDITS.md) and
  CatOverlay with walk/loaf/sleep/stretch/pet animations and five coats.
  Optional Quick3D dependency (MeshMC_ENABLE_CAT), CI setup updated.
- New instance dialog: mod loader cards use the logos the launcher
  already ships (fabricmc, quiltmc, forge, neoforged) instead of generic
  shapes.

Not verified on CI yet. The TabStrip overflow scrolling that was being
written when work stopped is left out of this commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: grxtor <abdullah.huseyin.efe@outlook.com>

@YongDo-Hyun YongDo-Hyun left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are a few general points I’d like to add. Creating a separate folder named qml is a complete waste, and adding a subdirectory just creates unnecessary file clutter. These files should be moved into ui/ and managed via the CMakeLists file in launcher/. I want to reiterate: use the existing icons—do not create new ones. Regarding themes, allow the inclusion of custom fonts. Also, if "CatPacks" are missing, they should be added.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to add Curseforge option

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to fix this issue:

Image

and needs to add overview cape and/or elytra option. With you need to use Shader Scene to move capes with mc skin

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please. Do not to use your custom icons. Please use the existing icons...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to add Native OS Themes like QtWidgets. Because the themes you have added may cause incompatibility issues on some systems.

The clearest example,

Image

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

In my opinion, the main issue here lies with the Wiki, but the buttons are another problem; they don't show the correct state until you restart. This needs to be fixed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs fix this issue:

Image

And needs remove this ground:

Image

And I think to use Shader Scene to the 3D skin exhibition

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

Please refactor and redesign this one. Because this logo is not visible

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We used to be able to configure custom themes for widgets using QCSS. We should be able to use that same theme here as well—with extra options, of course—and it must be 100% compatible with the previous version.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to add attributon to COPYING.md.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants