Skip to content

Commit 215cf7b

Browse files
committed
Add click-through option
1 parent aa7eeec commit 215cf7b

10 files changed

Lines changed: 186 additions & 56 deletions

File tree

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,15 @@ All features can be turned on or off at will using the config screen.
5757
- Skip opening the editor when placing or right-clicking a sign, optionally depending on whether you
5858
are sneaking or not.
5959

60-
#### Auto Fill
61-
62-
- Automatically add text to signs when placing.
63-
6460
#### Click-Through
6561

6662
- [ClickThrough Plus](https://modrinth.com/project/fJi8nm80) is recommended for use with SignTweaks.
63+
- If ClickThrough Plus is not installed, SignTweaks will use a basic click-through implementation to
64+
allow clicking through signs to most block entities (including chests, furnaces etc.)
65+
66+
#### Auto Fill
67+
68+
- Automatically add text to signs when placing.
6769

6870
### Compatibility
6971

changelog.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44

55
- Disabled the enhanced editor toggle button by default.
66
- Disabled the line break indicator by default.
7-
- Added an option to disable saving when exiting using the 'Escape' key.
87
- Added a 'Revert' GUI button.
8+
- Added an option to disable saving when exiting using the 'Escape' key.
99
- Added an option to conditionally prevent opening the sign editor.
1010
- Added an option to block movement key input until key release when opening the sign editor.
1111
- Added an option to automatically add text to placed signs.
12+
- Added an option to click through signs to most block entities.
1213

1314
## 1.2.0
1415

common/src/main/java/dev/terminalmc/signtweaks/SignTweaks.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public class SignTweaks {
4848
public static String[] originalLines;
4949

5050
public static long signPlaceTime = 0;
51+
public static long avoidClickThroughTime = 0;
5152

5253
private SignTweaks() {
5354
throw new UnsupportedOperationException("This class cannot be instantiated.");

common/src/main/java/dev/terminalmc/signtweaks/config/Config.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ public static class Options {
8989
public static final EditCondition editConditionDefault = EditCondition.ALWAYS;
9090
public EditCondition editCondition = editConditionDefault;
9191

92+
public static final boolean clickThroughDefault = true;
93+
public boolean clickThrough = clickThroughDefault;
94+
9295
public static final boolean useAutoFillDefault = false;
9396
public boolean useAutoFill = useAutoFillDefault;
9497

common/src/main/java/dev/terminalmc/signtweaks/gui/screen/ClothScreenProvider.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,15 @@ static Screen getConfigScreen(Screen parent) {
139139
.setSaveConsumer(val -> options.editCondition = val)
140140
.build());
141141

142+
general.addEntry(eb.startBooleanToggle(
143+
localized("option", "general.clickThrough"),
144+
options.clickThrough
145+
)
146+
.setTooltip(localized("option", "general.clickThrough.tooltip"))
147+
.setDefaultValue(Options.clickThroughDefault)
148+
.setSaveConsumer(val -> options.clickThrough = val)
149+
.build());
150+
142151
ConfigCategory autoFill = builder.getOrCreateCategory(localized("option", "autoFill"));
143152

144153
autoFill.addEntry(eb.startBooleanToggle(

common/src/main/java/dev/terminalmc/signtweaks/mixin/MinecraftMixin.java

Lines changed: 0 additions & 47 deletions
This file was deleted.

common/src/main/java/dev/terminalmc/signtweaks/mixin/interact/ClientPacketListenerMixin.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@ private void wrapOpenTextEdit(
5050
boolean isFrontText,
5151
Operation<Void> original
5252
) {
53-
if (System.nanoTime() - SignTweaks.signPlaceTime < 1_000_000_000L) { // 1 sec
53+
long timeNow = System.nanoTime();
54+
long timeSincePlace = timeNow - SignTweaks.signPlaceTime;
55+
long timeSinceClickThrough = timeNow - SignTweaks.avoidClickThroughTime;
56+
System.out.println("time since last clickthrough: " + timeSinceClickThrough);
57+
58+
if (timeSincePlace < 1_000_000_000L) { // 1 sec
5459
SignTweaks.signPlaceTime = 0;
5560
if (options().useAutoFill) {
5661
ClientPacketListener connection = Minecraft.getInstance().getConnection();
@@ -70,7 +75,9 @@ private void wrapOpenTextEdit(
7075

7176
boolean allow = switch (options().editCondition) {
7277
case SNEAKING -> instance.isSteppingCarefully();
73-
case NOT_SNEAKING -> !instance.isSteppingCarefully();
78+
case NOT_SNEAKING -> !instance.isSteppingCarefully()
79+
// override when sneak-clicking to avoid click-through
80+
|| timeSinceClickThrough < 1_000_000_000L; // 1 sec
7481
case ALWAYS -> true;
7582
case NEVER -> false;
7683
};
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright 2026 TerminalMC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package dev.terminalmc.signtweaks.mixin.interact;
18+
19+
import com.llamalad7.mixinextras.sugar.Local;
20+
import dev.terminalmc.signtweaks.SignTweaks;
21+
import dev.terminalmc.signtweaks.platform.services.PlatformServices;
22+
import net.minecraft.client.Minecraft;
23+
import net.minecraft.client.multiplayer.ClientLevel;
24+
import net.minecraft.client.player.LocalPlayer;
25+
import net.minecraft.core.BlockPos;
26+
import net.minecraft.world.InteractionHand;
27+
import net.minecraft.world.item.ItemStack;
28+
import net.minecraft.world.item.SignApplicator;
29+
import net.minecraft.world.item.SignItem;
30+
import net.minecraft.world.level.block.*;
31+
import net.minecraft.world.level.block.entity.BlockEntity;
32+
import net.minecraft.world.level.block.entity.SignBlockEntity;
33+
import net.minecraft.world.level.block.state.BlockState;
34+
import net.minecraft.world.phys.BlockHitResult;
35+
import net.minecraft.world.phys.HitResult;
36+
import org.jspecify.annotations.Nullable;
37+
import org.spongepowered.asm.mixin.Mixin;
38+
import org.spongepowered.asm.mixin.Shadow;
39+
import org.spongepowered.asm.mixin.injection.At;
40+
import org.spongepowered.asm.mixin.injection.Inject;
41+
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
42+
43+
import static dev.terminalmc.signtweaks.config.Config.options;
44+
45+
@Mixin(Minecraft.class)
46+
public abstract class MinecraftMixin {
47+
48+
@Shadow
49+
@Nullable
50+
public HitResult hitResult;
51+
52+
@Shadow
53+
@Nullable
54+
public ClientLevel level;
55+
56+
@Shadow
57+
@Nullable
58+
public LocalPlayer player;
59+
60+
/**
61+
* Rudimentary click-through implementation for signs.
62+
*/
63+
@Inject(
64+
method = "startUseItem",
65+
at = @At(
66+
value = "INVOKE",
67+
target = "Lnet/minecraft/client/player/LocalPlayer;getItemInHand(Lnet/minecraft/world/InteractionHand;)Lnet/minecraft/world/item/ItemStack;"
68+
)
69+
)
70+
public void onGetItemInHand(CallbackInfo ci) {
71+
// must be enabled
72+
if (!options().clickThrough)
73+
return;
74+
75+
// must not be holding dye/ink/honeycomb
76+
// (can't use if sneaking so have to allow usage while standing)
77+
if (player.getItemInHand(InteractionHand.MAIN_HAND).getItem() instanceof SignApplicator)
78+
return;
79+
80+
// must be targeting some form of block
81+
if (!(hitResult instanceof BlockHitResult blockHitResult))
82+
return;
83+
84+
BlockPos hitBlockPos = blockHitResult.getBlockPos();
85+
BlockEntity hitBlockEntity = level.getBlockEntity(hitBlockPos);
86+
87+
// must be targeting a sign
88+
if (!(hitBlockEntity instanceof SignBlockEntity))
89+
return;
90+
91+
BlockState hitBlockState = level.getBlockState(hitBlockPos);
92+
Block hitBlock = hitBlockState.getBlock();
93+
94+
// must be targeting a wall sign
95+
if (!(hitBlock instanceof WallSignBlock))
96+
return;
97+
98+
BlockPos wallBlockPos =
99+
hitBlockPos.offset(hitBlockState.getValue(WallSignBlock.FACING)
100+
.getOpposite()
101+
.getUnitVec3i());
102+
BlockState wallBlockState = level.getBlockState(wallBlockPos);
103+
Block wallBlock = wallBlockState.getBlock();
104+
105+
// sign must be on a block entity
106+
if (!(wallBlock instanceof BaseEntityBlock))
107+
return;
108+
109+
// sign must be on a block entity that it makes sense to click through to
110+
if (wallBlock instanceof AbstractBannerBlock
111+
|| wallBlock instanceof AbstractSkullBlock
112+
|| wallBlock instanceof SignBlock
113+
|| wallBlock instanceof Portal
114+
|| wallBlock instanceof SculkShriekerBlock)
115+
return;
116+
117+
// must not be sneaking
118+
if (player.isSteppingCarefully()) {
119+
// record the time to allow keeping the editor open
120+
SignTweaks.avoidClickThroughTime = System.nanoTime();
121+
return;
122+
}
123+
124+
// must not have clickthrough plus
125+
if (PlatformServices.getInstance().isModLoaded("clickthrough"))
126+
return;
127+
128+
// retarget
129+
hitResult = new BlockHitResult(
130+
blockHitResult.getLocation(),
131+
blockHitResult.getDirection(),
132+
wallBlockPos,
133+
false
134+
);
135+
}
136+
137+
/**
138+
* Records the time when a sign is placed.
139+
*/
140+
@Inject(
141+
method = "startUseItem",
142+
at = @At(
143+
value = "INVOKE",
144+
target = "Lnet/minecraft/client/multiplayer/MultiPlayerGameMode;useItemOn(Lnet/minecraft/client/player/LocalPlayer;Lnet/minecraft/world/InteractionHand;Lnet/minecraft/world/phys/BlockHitResult;)Lnet/minecraft/world/InteractionResult;"
145+
)
146+
)
147+
private void onUseItemOn(CallbackInfo ci, @Local(name = "heldItem") ItemStack heldItem) {
148+
if (heldItem.getItem() instanceof SignItem) {
149+
SignTweaks.signPlaceTime = System.nanoTime();
150+
}
151+
}
152+
}

common/src/main/resources/assets/signtweaks/lang/en_us.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
"option.signtweaks.general.editCondition.SNEAKING": "Only if Sneaking",
3535
"option.signtweaks.general.editCondition.NOT_SNEAKING": "Only if not Sneaking",
3636
"option.signtweaks.general.editCondition.NEVER": "Never Edit",
37+
"option.signtweaks.general.clickThrough": "Click Through Signs",
38+
"option.signtweaks.general.clickThrough.tooltip": "Whether to click through signs placed on block entities when not sneaking.",
3739

3840
"option.signtweaks.autoFill": "AutoFill",
3941
"option.signtweaks.autoFill.useAutoFill": "Use AutoFill",

common/src/main/resources/signtweaks.mixins.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
"mixins": [
1111
],
1212
"client": [
13-
"MinecraftMixin",
1413
"copy.SignBlockMixin",
1514
"edit.AbstractSignEditScreenMixin",
1615
"gui.AbstractSignEditScreenMixin",
1716
"gui.ScreenMixin",
1817
"input.ContainerEventHandlerMixin",
1918
"input.LocalPlayerMixin",
20-
"interact.ClientPacketListenerMixin"
19+
"interact.ClientPacketListenerMixin",
20+
"interact.MinecraftMixin"
2121
],
2222
"server": [
2323
],

0 commit comments

Comments
 (0)