Skip to content

fix Mac teardown crashes by correcting QObject ownership hierarchy - #526

Merged
nschimme merged 1 commit into
MUME:masterfrom
nschimme:fix-mac-crash
Apr 20, 2026
Merged

fix Mac teardown crashes by correcting QObject ownership hierarchy#526
nschimme merged 1 commit into
MUME:masterfrom
nschimme:fix-mac-crash

Conversation

@nschimme

@nschimme nschimme commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

The application was crashing on macOS during shutdown due to improper parenting of UI components. When objects like delegates or models are parented to the main Widget instead of the View, they can be destroyed out of order, leading to use-after-free errors when the View attempts to access them during its own destruction.

  • Parent GroupModel and GroupProxyModel to m_table instead of 'this'.
  • Parent GroupDelegate and TimerDelegate to their respective views.
  • Ensure the lifecycle of the delegate matches the lifecycle of the view to prevent segmentation faults during signal disconnection.
  • Restore heap allocation for GroupModel to ensure Qt's ownership system manages its cleanup correctly.

Summary by Sourcery

Fix macOS shutdown crashes by aligning Qt object ownership with their views to ensure correct destruction order of models, views, and delegates.

Bug Fixes:

  • Parent group table models and delegates to the table view so they are destroyed in the correct order during widget teardown, preventing use-after-free crashes.
  • Parent timer model and delegate to the timer table view to avoid segmentation faults during view and delegate destruction on shutdown.

Enhancements:

  • Allocate the group model on the heap and rely on Qt parent-child ownership instead of manual deletion for safer lifetime management across the group widget.

@sourcery-ai

sourcery-ai Bot commented Apr 20, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adjusts QObject ownership for group and timer views on macOS by reparenting models and delegates to their views, reinstating heap allocation for GroupModel, and updating usage sites to work with pointer-based models and Qt-managed lifetimes to avoid shutdown crashes.

Sequence diagram for Qt teardown with corrected ownership hierarchy

sequenceDiagram
    participant App
    participant GroupWidget
    participant QTableView as m_table
    participant GroupModel as m_model
    participant GroupProxyModel as m_proxyModel
    participant GroupDelegate as m_delegate

    App->>GroupWidget: destroy GroupWidget
    activate GroupWidget
    GroupWidget->>GroupWidget: ~GroupWidget()
    GroupWidget->>QTimer: stop()
    deactivate GroupWidget
    note over GroupWidget,QTableView: Qt QObject destruction via parent-child
    GroupWidget->>QTableView: ~QTableView() (child deleted by parent)
    activate QTableView
    QTableView->>GroupProxyModel: ~GroupProxyModel()
    QTableView->>GroupModel: ~GroupModel()
    QTableView->>GroupDelegate: ~GroupDelegate()
    deactivate QTableView
    note over QTableView,GroupModel: View and its models/delegate are destroyed together
Loading

Class diagram for updated Qt ownership in GroupWidget

classDiagram
    class GroupWidget {
        +Mmapper2Group* m_group
        +MapData* m_map
        +GroupProxyModel* m_proxyModel
        +GroupModel* m_model
        +QTimer* m_pulseTimer
        +QTableView* m_table
        +void slot_onCharacterAdded(SharedGroupChar character)
        +void slot_onCharacterRemoved(GroupId characterId)
        +void slot_onCharacterUpdated(SharedGroupChar character)
        +void slot_onGroupReset(GroupVector newCharacterList)
        +void slot_mapLoaded()
        +void slot_mapUnloaded()
        -void updateColumnVisibility()
        -void updatePulseTimer()
    }

    class GroupModel {
        +void setCharacters(GroupVector characters)
        +void insertCharacter(SharedGroupChar character)
        +void removeCharacterById(GroupId characterId)
        +void updateCharacter(SharedGroupChar character)
        +GroupVector getCharacters()
        +SharedGroupChar getCharacter(int row)
        +void setMapLoaded(bool loaded)
    }

    class GroupProxyModel {
        +void setSourceModel(GroupModel* model)
    }

    class GroupDelegate {
    }

    class QTableView {
        +void setModel(GroupProxyModel* model)
        +void setItemDelegate(GroupDelegate* delegate)
    }

    class QTimer {
    }

    GroupWidget *-- QTableView : owns m_table
    GroupWidget *-- QTimer : owns m_pulseTimer
    QTableView *-- GroupModel : parent of m_model
    QTableView *-- GroupProxyModel : parent of m_proxyModel
    QTableView *-- GroupDelegate : item delegate parent
    GroupProxyModel o-- GroupModel : wraps
Loading

Class diagram for updated Qt ownership in TimerWidget

classDiagram
    class TimerWidget {
        +CTimers& m_timers
        +TimerModel* m_model
        +QTableView* m_view
    }

    class TimerModel {
    }

    class TimerDelegate {
    }

    class QTableView {
        +void setModel(TimerModel* model)
        +void setItemDelegateForColumn(int column, TimerDelegate* delegate)
    }

    TimerWidget *-- QTableView : owns m_view
    QTableView *-- TimerModel : parent of m_model
    QTableView *-- TimerDelegate : shared delegate parent
Loading

File-Level Changes

Change Details Files
Move GroupModel from value member to heap-allocated QObject parented to the table view, and update all call sites accordingly.
  • Replace value member GroupModel with a GroupModel* in GroupWidget, initialized with m_table as its parent
  • Initialize GroupModel after constructing m_table and set characters based on m_group
  • Update all accesses to m_model methods to dereference the pointer via deref(m_model)
  • Adjust mapLoaded/mapUnloaded slots to call setMapLoaded on the pointer-backed model
src/group/groupwidget.cpp
src/group/groupwidget.h
Ensure proxy model and delegates are parented to their views or tables instead of the widget, aligning lifetimes with the view hierarchy.
  • Construct GroupProxyModel with m_table as parent and set its source model to the heap-allocated GroupModel
  • Set the table's item delegate using a GroupDelegate parented to m_table instead of the widget
  • Remove manual deletion of m_table and m_recolor from GroupWidget destructor to rely on Qt parent-child cleanup
  • In TimerWidget, parent TimerModel to m_view instead of the widget
  • In TimerWidget, parent TimerDelegate to m_view instead of the widget
src/group/groupwidget.cpp
src/timers/TimerWidget.cpp

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • All call sites now using deref(m_model) assume m_model is non-null; it would be safer either to assert/check for null inside deref or to guard the slots (e.g. slot_mapLoaded/Unloaded) in case they can fire before the view/model is fully initialized or after it has been torn down.
  • Since GroupModel and GroupProxyModel lifetimes are now owned by m_table, consider documenting or encapsulating that ownership (e.g. via a small helper or comment near their creation) to make it clear to future maintainers that GroupWidget must not manually delete them and that the raw pointers are non-owning.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- All call sites now using `deref(m_model)` assume `m_model` is non-null; it would be safer either to assert/check for null inside `deref` or to guard the slots (e.g. `slot_mapLoaded/Unloaded`) in case they can fire before the view/model is fully initialized or after it has been torn down.
- Since `GroupModel` and `GroupProxyModel` lifetimes are now owned by `m_table`, consider documenting or encapsulating that ownership (e.g. via a small helper or comment near their creation) to make it clear to future maintainers that `GroupWidget` must not manually delete them and that the raw pointers are non-owning.

## Individual Comments

### Comment 1
<location path="src/group/groupwidget.h" line_range="154" />
<code_context>
     MapData *m_map = nullptr;
     GroupProxyModel *m_proxyModel = nullptr;
-    GroupModel m_model;
+    GroupModel *m_model = nullptr;
     QTimer *m_pulseTimer = nullptr;

</code_context>
<issue_to_address>
**suggestion:** Centralize access to m_model instead of using deref(m_model) in multiple places.

Since m_model is now a pointer, several `deref(m_model)` calls are scattered across slots and helpers. Please add a small private accessor like `GroupModel &model() { return deref(m_model); }` and use it instead. That centralizes the invariant/non-null assertion for m_model and improves readability.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/group/groupwidget.h
MapData *m_map = nullptr;
GroupProxyModel *m_proxyModel = nullptr;
GroupModel m_model;
GroupModel *m_model = nullptr;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Centralize access to m_model instead of using deref(m_model) in multiple places.

Since m_model is now a pointer, several deref(m_model) calls are scattered across slots and helpers. Please add a small private accessor like GroupModel &model() { return deref(m_model); } and use it instead. That centralizes the invariant/non-null assertion for m_model and improves readability.

The application was crashing on macOS during shutdown due to improper
parenting of UI components. When objects like delegates or models are
parented to the main Widget instead of the View, they can be destroyed
out of order, leading to use-after-free errors when the View attempts to
access them during its own destruction.

- Parent GroupModel and GroupProxyModel to m_table instead of 'this'.
- Parent GroupDelegate and TimerDelegate to their respective views.
- Ensure the lifecycle of the delegate matches the lifecycle of the view
  to prevent segmentation faults during signal disconnection.
- Restore heap allocation for GroupModel to ensure Qt's ownership system
  manages its cleanup correctly.
@nschimme
nschimme merged commit 765550d into MUME:master Apr 20, 2026
18 checks passed
@nschimme
nschimme deleted the fix-mac-crash branch April 20, 2026 16:20
@codecov

codecov Bot commented Apr 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 25.40%. Comparing base (dd0c268) to head (fc66eca).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
src/group/groupwidget.cpp 0.00% 14 Missing ⚠️
src/group/groupwidget.h 0.00% 2 Missing ⚠️
src/timers/TimerWidget.cpp 0.00% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master     #526   +/-   ##
=======================================
  Coverage   25.40%   25.40%           
=======================================
  Files         519      519           
  Lines       43109    43107    -2     
  Branches     4699     4699           
=======================================
  Hits        10952    10952           
+ Misses      32157    32155    -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant