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
118 changes: 67 additions & 51 deletions src/components/widgets/mmu/MmuUnit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
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"
:close-on-content-click="false"
:open-on-click="false"
transition="slide-y-transition"
offset-y
>
Expand Down Expand Up @@ -54,46 +56,21 @@
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%"
class="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 +132,7 @@
<div
v-if="showBypass"
class="gate"
@contextmenu.prevent="openContextMenu(-2, $event)"
@click="selectBypass()"
>
<div :class="clipSpoolClass">
Expand Down Expand Up @@ -278,10 +256,14 @@ 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

@Watch('unit', { immediate: true })
onUnit (value: number) {
Expand Down Expand Up @@ -428,22 +410,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 +421,55 @@ 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'), loading: '$waits.onMmuSelect`' },
{ icon: '$mmuPreload', command: 'MMU_PRELOAD', label: this.$t('app.mmu.btn.preload'), loading: '$waits.onMmuPreload' },
{ icon: '$mmuEject', command: 'MMU_EJECT', label: this.$t('app.mmu.btn.eject'), loading: '$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) {
e.preventDefault()
if (gate < 0 || gate === this.gate || !this.showContextMenu) {
this.closeContextMenu()
return
}
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
}

mounted () {
addEventListener('mmu-close-gate-context-menus', this.closeContextMenu)
}

beforeDestroy () {
removeEventListener('mmu-close-gate-context-menus', this.closeContextMenu)
this.clearCloseTimeout()
}
}
</script>

Expand Down