Skip to content

implement Qt6 views#1446

Merged
dxqb merged 90 commits into
Nerogar:mergefrom
dxqb:pyside
Jul 1, 2026
Merged

implement Qt6 views#1446
dxqb merged 90 commits into
Nerogar:mergefrom
dxqb:pyside

Conversation

@dxqb

@dxqb dxqb commented May 10, 2026

Copy link
Copy Markdown
Collaborator

Builds on the controller-view abstraction introduced in the previous PR. That PR made all business logic toolkit-independent and defined UI content in toolkit-neutral base views. This PR adds a PySide6 frontend without touching any of that shared code.

Three pieces are needed to make build_content run under Qt:

pyside6_components — mirrors the ctk_components API with Qt widgets. self.components.label(...), self.components.options(...) etc. resolve to Qt implementations when a PySide6 view passes this namespace at construction.

QtVar / PySide6UIState — Qt equivalent of tkinter variables and UIState. QtVar wraps a value and fires Qt signals on change; PySide6UIState exposes the same interface controllers and base views expect.

PySide6*View classes — inherit from the Qt window/widget base and the shared Base*View. For TrainUI, PySide6TrainUIView(QMainWindow, BaseTrainUIView) reuses TrainUIController and BaseTrainUIView unchanged.

The PySide6 UI is launched via ./start-ui.sh. The CTK UI is unaffected and is still started using ./run-cmd.sh train_ui_ctk.

  • untested on windows
  • the dataset tools were not ported because it contains lots of outdated ctk-specific code that doesn't use ctk_components.py. If we want to keep those it needs a refactor itself before porting

dxqb and others added 4 commits May 10, 2026 14:11
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@dxqb

dxqb commented May 10, 2026

Copy link
Copy Markdown
Collaborator Author
grafik

Comment thread .gitignore
Comment thread modules/util/ui/pyside6_components.py
dxqb and others added 6 commits May 11, 2026 20:09
… VideoToolUI

Removes abstract _create_browse_dir_button/_create_browse_file_button from
BaseVideoToolUIView and uses the combined path_entry component instead, fixing
widget alignment issues. Adds allow_video_files flag to path_entry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…IView

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…UIView

Now handled by path_entry in the base class.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…files)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@dxqb dxqb requested a review from O-J1 May 11, 2026 21:04
@dxqb dxqb linked an issue May 15, 2026 that may be closed by this pull request
dxqb and others added 14 commits May 15, 2026 11:40
Base view concrete methods were accessing self.controller, self.ui_state,
and toolkit-specific action methods that were only set by CTK subclass
__init__ after the base __init__ call, with no enforcement in the base.

- BaseTrainUIView: add controller/ui_state as constructor params; fix
  sync_cloud_secrets to use controller.train_config; add @AbstractMethod
  for export_training, generate_debug_package, open_profiling_tool
- CtkTrainUIView: reorder __init__ to create deps before base init call;
  replace self.train_config with self.controller.train_config
- BaseCloudTabView: add controller as constructor param
- CtkCloudTabView: pass controller to base __init__, drop redundant assignment
- BaseCaptionUIView: add ABC + @AbstractMethod for 6 action callbacks
- BaseConceptTabView: remove concrete _update_filters() (accessed CTK vars);
  add ConceptConfig import; add concept: ConceptConfig param to
  BaseConceptWidgetView.__init__
- CtkConceptTabView: implement _update_filters(); pass concept to base init
- BaseConceptWindowView: initialize bucket_ax/text_color/canvas to None
- BaseTrainingTabView: replace callbacks dict with 6 @AbstractMethod
  declarations; restore_optimizer_config(variable: str) matches controller
- CtkTrainingTabView: implement all 6 abstract methods directly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The ctypes DPI awareness call is toolkit-specific (fixes CTK transparency
on Windows monitor changes). It already exists in CtkTrainUIView.py and
has no place in the toolkit-agnostic controller.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PySide6TrainUIView: create train_config/ui_state/controller before base
  init so they can be passed as constructor params; replace self.train_config
  with self.controller.train_config throughout
- PySide6CloudTabView: pass controller to BaseCloudTabView.__init__;
  remove now-redundant self.controller assignment
- PySide6ConceptTabView: implement _update_filters() (removed from base);
  pass concept to BaseConceptWidgetView.__init__
- PySide6TrainingTabView: remove callbacks dict; call build() without it;
  implement 6 abstract methods with logic inlined from private helpers;
  remove now-unused private helper methods

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
search_var/filter_var/show_disabled_var were stored in the base but never
used there after _update_filters() was removed. Subclasses manage them.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…abView

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
UIState's enum trace looks up var_type[string], so the var must hold the
string repr of the value, not the value itself.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirrors the change from upstream commit e928dda (Remove COFT Nerogar#1447),
which removed COFT from LoraTab.py. The merge didn't carry it across
the rename to CtkLoraTabView.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Brings in upstream changes (remove COFT Nerogar#1447, remove cautious_mask Nerogar#1451,
fix aux optimizer defaults Nerogar#1444). Conflicts resolved by keeping the
ctk_abstraction refactored versions (self.components, controller delegation).
COFT removal applied manually to BaseLoraTabView.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dxqb and others added 13 commits June 27, 2026 12:08
BaseConfigListView and BaseAdditionalEmbeddingsTabView imported customtkinter
solely for a `-> ctk.CTkToplevel` return annotation on the abstract
open_element_window, the lone toolkit reference in the Base layer (the sibling
abstract methods return toolkit objects unannotated). Drop the annotation and
the import so the Base layer is genuinely toolkit-free; the concrete Ctk
subclasses keep their own CTkToplevel annotation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tk view split

The ctk view split copied OptimizerParamsWindow into a controller from a base
that predated Nerogar#1444, reverting `if not current_state:` back to
`if current_state is None:`. Since muon_adam_config defaults to {} (not None),
the empty initial state fell through to from_dict({}), so the Muon/aux-Adam
window opened with bare default_values() instead of MUON_AUX_ADAM_DEFAULTS /
ADAMW_ADV. Restore the upstream one-line guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…izer params

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…onvert_model)

video_tool_ui.py and convert_model_ui.py now use QApplication/exec() with their
PySide6 views instead of the CTk hidden-root pattern. caption_ui.py stays on CTk
until the caption tool is ported to Qt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…onents updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
entry() and switch() registered a var trace via add_var_trace() but never
removed it on widget destruction, so traces accumulated on long-lived
ui_state objects (e.g. the optimizer window's optimizer_ui_state) across
rebuilds, keeping destroyed windows alive. Wire destroyed.connect to
remove_var_trace, mirroring the existing _unbind_widget cleanup pattern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
refresh_ui() stored only the inner content QWidget as self.scroll_frame
and recreated the QScrollArea as a local, so each model-type/training-method
switch left the old empty QScrollArea parented to self and stacked in the
grid cell. Track the QScrollArea instead (matching PySide6ModelTabView) so
deleteLater() frees the whole subtree on refresh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
__create_configs_dropdown called QWidget.destroy() directly, which on
PySide6 only tears down the native handle and leaves the old combo box in
the grid cell. Adding a config (which recreates the dropdown) stacked
duplicate combos. Use the _destroy_widget abstraction, which hides and
deleteLater()s on PySide6 and stays equivalent to destroy() on Ctk.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PySide6ConceptWindowView had no closeEvent override, so closing via the OS
X button skipped _ok(): the matplotlib figure was leaked and the background
scan thread kept running. Add a closeEvent that runs a shared _cleanup()
(also called from _ok), which sets the controller's cancel_scan_flag to stop
the scan and plt.close()s the figure. The scan's call_after updates are
already auto-cancelled by Qt via the context-object singleShot overload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The search box, type combo and show-disabled checkbox were wired one-way
(widget -> var), so _reset_filters() reset the vars but left the widgets
displaying their old state. Register var -> widget _bind_widget callbacks
so resetting the vars (and any other programmatic var change) pushes back
to the widgets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
QInputDialog.getText always closes on OK, so a name starting with '#'
(reserved for built-in presets) passed the ok check but failed the prefix
guard, silently skipping the save with no feedback. Show a warning so the
user knows the name was rejected and can retry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The sample window's button spawned a daemon thread on every click with no
guard. For the standalone sampling tool (use_external_model=False) do_sample
runs the model synchronously, so rapid clicks raced the lazy model load and
started concurrent sampling runs; run it in a thread and disable the button
while a sample is in flight, re-enabling in finally on the main thread. For
the manual-sample-during-training path (use_external_model=True) do_sample
only enqueues a command for the training thread, so call it directly without
a thread and keep the button clickable so multiple samples can be queued.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Refuse to close the main window while the training thread is alive, and
restore the OS default SIGINT handler so Ctrl+C terminates the process
even while blocked inside Qt's event loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dxqb and others added 9 commits June 29, 2026 23:49
…gc to close

connect_window_closed bound <Destroy> unconditionally, so the callback
fired once per descendant widget as the toplevel tore down. Guard on the
event widget being the window itself so it fires exactly once.

With that in place, open_sampling_tool defers torch_gc to window close via
connect_window_closed instead of running it inline at open time, restoring
the original 'free GPU memory after the sample window closes' behaviour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	modules/ui/PySide6TrainUIView.py
…reate_window

PySide6TrainUIView constructed PySide6ProfilingWindowView directly instead of
via create_window(), so controller.view was never set. Any profiling button
(dump stack, start/end profiling) then crashed with AttributeError on
self.view = None. Mirrors the existing CtkTrainUIView pattern and the other
five window controllers (CaptionUIController, VideoToolUIController, etc.)
that all rely on create_window() for the same two-phase init.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_forget_dropdown only called grid_forget(), not destroy(), so the old
CTkOptionMenu and its var trace stayed alive every time the training-method
dropdown was rebuilt on a model-type change. winfo_exists() stayed True for
the stale widgets, so their var.trace_add callback kept firing on every
later training-method change, multiplying calls to
change_training_method_callback (and the resulting full model-tab rebuild)
by the number of prior model-type switches. The PySide6 sibling already used
deleteLater() correctly; this regression was CTk-only, introduced when
__create_training_method was switched from a direct .destroy() call to the
shared _forget_dropdown abstraction. destroy() restores winfo_exists() ==
False for stale dropdowns, which the existing update_var() guard already
relies on.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… review

- PySide6OptimizerParamsWindowView: use hide()+deleteLater() instead of
  setParent(None) when rebuilding dynamic optimizer fields, so var-trace
  cleanup (destroyed signal) actually fires instead of leaking widgets/traces.
- PySide6PathValidator.revalidate(): guard against calling into an
  already-destroyed QLineEdit via a dependent-var trace during teardown.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…app setup

- pyside6_components.py/ctk_components.py: replace button()'s silent
  **kwargs passthrough with an explicit width param; use setMinimumWidth on
  Qt to match CTkButton's floor-not-cap width behavior (CTkButton never
  disables grid propagation, so it grows past `width` to fit its label).
- pyside6_validation.py: drop the dead max_undo/DEFAULT_MAX_UNDO plumbing
  from PySide6FieldValidator/PySide6PathValidator; QLineEdit has native
  undo/redo, unlike tk.Entry which needed the custom UndoHistory.
- QtVar.py: switch docstring to a # comment per project convention.
- New modules/util/ui/pyside6_util.py: shared create_application() (SIGINT
  restore, light palette, base stylesheet) and QtABCMeta (merged from the
  deleted pyside6_abc.py), applied to all three Qt entry scripts
  (train_ui_qt.py, convert_model_ui.py, video_tool_ui.py) so they're
  visually and behaviorally consistent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@dxqb dxqb mentioned this pull request Jul 1, 2026
@dxqb dxqb changed the base branch from pyside_copy to merge July 1, 2026 04:26
dxqb added 2 commits July 1, 2026 06:27
…erogar#1445) into pyside

Nerogar/merge's tip is a squash-merge of ctk_abstraction, tree-identical
to our ctk_abstraction/pyside_copy ancestors already in pyside's history.
-X ours plus manual conflict resolution below keeps pyside's tree unchanged.
@dxqb dxqb merged commit 4a660d4 into Nerogar:merge Jul 1, 2026
1 check passed
@Mixomo

Mixomo commented Jul 7, 2026

Copy link
Copy Markdown

Hey, so did you switch from custom Tkinter to Qt6? I just updated to the latest version and it opened with a light theme, which is burning my eyes :)

Is there a way to switch back to a dark theme?

Thanks in advance!

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

Labels

preview merged in the preview branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat]: Replace CustomTkInter with PyQt6

4 participants