Skip to content
1 change: 1 addition & 0 deletions src/components/widgets/mmu/MmuEditGateMapDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
<v-row align="start">
<v-col class="d-flex justify-start align-center no-padding">
<mmu-machine
:show-context-menu="false"
:edit-gate-map="editGateMap"
:edit-gate-selected="editGateSelected"
@select-gate="selectGate"
Expand Down
4 changes: 4 additions & 0 deletions src/components/widgets/mmu/MmuMachine.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
:unit-index="index"
:edit-gate-map="editGateMap"
:edit-gate-selected="editGateSelected"
:show-context-menu="showContextMenu"
@select-gate="selectGate"
/>
</div>
Expand Down Expand Up @@ -38,6 +39,9 @@ export default class MmuMachine extends Mixins(StateMixin, MmuMixin) {
@Prop({ required: false, default: -1 })
readonly editGateSelected!: number

@Prop({ required: false, default: true })
readonly showContextMenu!: boolean

get unitArray (): number[] {
return Array.from({ length: this.numUnits }, (_, k) => k)
}
Expand Down
138 changes: 86 additions & 52 deletions src/components/widgets/mmu/MmuUnit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@
v-for="(g, index) in unitGateRange"
:key="`gate_${g}`"
class="gate"
@contextmenu.prevent="openContextMenu(g, $event)"
@click="selectGate(g)"
>
<div :class="clipSpoolClass">
<v-menu
v-model="gateMenuVisible[g]"
:disabled="g === gate || !unitDetails(unitIndex).multiGear"
:disabled="g === gate"
:position-x="menuX"
:position-y="menuY"
:close-on-content-click="false"
:open-on-click="false"
transition="slide-y-transition"
absolute
offset-y
>
<template #activator="{ attrs: menuAttrs }">
Expand Down Expand Up @@ -49,51 +54,29 @@
</v-tooltip>
</template>

<v-list dense>
<v-list
dense
@mouseleave="closeContextMenu"
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

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

The mouseleave event on the v-list will close the menu when the user's cursor briefly leaves the menu bounds, which can be frustrating. This conflicts with the 6-second timeout mechanism. Consider removing the mouseleave handler or add a small delay before closing to prevent accidental closure when the user's cursor momentarily leaves the menu area.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

Assume intended behavior, so ignore.

>
<v-subheader class="compact-subheader">
Gate {{ g }}
</v-subheader>
<v-divider />
<v-list-item>
<v-btn
small
style="width: 100%"
:disabled="!klippyReady || !canSend"
:loading="hasWait($waits.onMmuSelect)"
@click="sendGcode(`MMU_SELECT GATE=${g}`, $waits.onMmuSelect)"
>
<v-icon left>
$mmuSelectGate
</v-icon>
{{ $t('app.mmu.btn.select') }}
</v-btn>
</v-list-item>
<v-list-item>
<v-btn
small
style="width: 100%"
:disabled="!klippyReady || !canSend || ![GATE_UNKNOWN, GATE_EMPTY].includes(gateDetails(g).status)"
:loading="hasWait($waits.onMmuPreload)"
@click="sendGcode(`MMU_PRELOAD GATE=${g}`, $waits.onMmuPreload)"
>
<v-icon left>
$mmuPreload
</v-icon>
{{ $t('app.mmu.btn.preload') }}
</v-btn>
</v-list-item>
<v-list-item>
<v-list-item
v-for="(item, i) in contextMenuItems"
:key="i"
>
<v-btn
small
style="width: 100%"
:disabled="!klippyReady || !canSend"
:loading="hasWait($waits.onMmuEject)"
@click="sendGcode(`MMU_EJECT GATE=${g}`, $waits.onMmuEject)"
:loading="hasWait(item.loading)"
@click="contextMenuCommand(item.command, item.loading, g)"
>
<v-icon left>
$mmuEject
{{ item.icon }}
</v-icon>
{{ $t('app.mmu.btn.eject') }}
{{ item.label }}
</v-btn>
</v-list-item>
</v-list>
Expand Down Expand Up @@ -155,6 +138,7 @@
<div
v-if="showBypass"
class="gate"
@contextmenu.prevent="openContextMenu(-2, $event)"
@click="selectBypass()"
>
<div :class="clipSpoolClass">
Expand Down Expand Up @@ -278,10 +262,15 @@ export default class MmuUnit extends Mixins(BrowserMixin, StateMixin, MmuMixin)
@Prop({ required: false, default: -1 })
readonly editGateSelected!: number

@Prop({ required: false, default: true })
readonly showContextMenu!: boolean

gateMenuVisible: Record<number, boolean> = {}
gateMenuTimer: ReturnType<typeof setTimeout> | null = null

vendorLogo = ''
closeTimeout: number | null = null
menuX = 0
menuY = 0

@Watch('unit', { immediate: true })
onUnit (value: number) {
Expand Down Expand Up @@ -428,22 +417,7 @@ export default class MmuUnit extends Mixins(BrowserMixin, StateMixin, MmuMixin)
if (this.editGateMap) {
this.$emit('select-gate', gate)
} else if (!this.isPrinting) {
if (
this.unitDetails(this.unitIndex).multiGear &&
gate !== this.gate &&
![this.FILAMENT_POS_UNLOADED, this.FILAMENT_POS_UNKNOWN].includes(this.filamentPos)
) {
if (this.gateMenuTimer) clearTimeout(this.gateMenuTimer)
this.gateMenuTimer = setTimeout(() => {
Object.keys(this.gateMenuVisible).forEach(key => {
this.$set(this.gateMenuVisible, Number(key), false)
})
}, 3000)
this.$set(this.gateMenuVisible, gate, true)
} else {
if (this.gateMenuTimer) clearTimeout(this.gateMenuTimer)
this.sendGcode('MMU_SELECT GATE=' + gate)
}
this.sendGcode('MMU_SELECT GATE=' + gate)
}
}

Expand All @@ -454,6 +428,66 @@ export default class MmuUnit extends Mixins(BrowserMixin, StateMixin, MmuMixin)
this.sendGcode('MMU_SELECT BYPASS=1')
}
}

// Gate context menu handling...

get contextMenuItems () {
return [
{
icon: '$mmuSelectGate',
command: 'MMU_SELECT',
label: this.$t('app.mmu.btn.select').toString(),
loading: this.$waits.onMmuSelect
},
{
icon: '$mmuPreload',
command: 'MMU_PRELOAD',
label: this.$t('app.mmu.btn.preload').toString(),
loading: this.$waits.onMmuPreload
},
{
icon: '$mmuEject',
command: 'MMU_EJECT',
label: this.$t('app.mmu.btn.eject').toString(),
loading: this.$waits.onMmuEject
}
]
}

contextMenuCommand (command: string, loading: string, gate: number) {
this.sendGcode(`${command} GATE=${gate}`, loading)
}
Comment on lines +457 to +459
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

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

After executing a command, the context menu should close but there's no call to closeContextMenu in contextMenuCommand. This means the menu stays open after clicking a button, which is counterintuitive. Add a call to this.closeContextMenu() at the end of contextMenuCommand to close the menu after executing the command.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

This seems to be the expected behavior, so ignore.


openContextMenu (gate: number, e: MouseEvent) {
if (gate < 0 || gate === this.gate || !this.showContextMenu) {
this.closeContextMenu()
return
}
this.menuX = e.clientX - 20
this.menuY = e.clientY - 20
Comment on lines +466 to +467
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

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

The magic numbers -20 for menu positioning offset are unexplained. Extract these to named constants with descriptive names to improve code maintainability and make the offset purpose clear.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

Ignore.

this.closeContextMenu()
this.$set(this.gateMenuVisible, gate, true)
this.closeTimeout = window.setTimeout(() => {
this.closeContextMenu()
}, 6000)
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

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

The magic number 6000 (6 seconds timeout) is unexplained. Extract this to a named constant with a descriptive name to improve code maintainability and make the timeout duration clear and easily configurable.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

Ignore.

}

closeContextMenu () {
this.clearCloseTimeout()
Object.keys(this.gateMenuVisible).forEach(key => {
this.$set(this.gateMenuVisible, Number(key), false)
})
}

clearCloseTimeout () {
if (this.closeTimeout === null) return
clearTimeout(this.closeTimeout)
this.closeTimeout = null
}

beforeDestroy () {
this.clearCloseTimeout()
}
}
</script>

Expand Down