[phase-02] Reach launcher services through LauncherContext - #190
YongDo-Hyun wants to merge 64 commits into
Conversation
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>
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>
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>
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
left a comment
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Needs to add Curseforge option
There was a problem hiding this comment.
Please. Do not to use your custom icons. Please use the existing icons...
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Needs to add attributon to COPYING.md.






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:
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.