Add a setting to send messages with Enter, Shift+Enter or Ctrl+Enter

Previously the option was just `invertEnterKey` boolean, which didn't
allow any flexibility, so I replaced it with a three-choice option:
Enter, Shift+Enter and Ctrl+Enter being the send message choices.
Add newline combos are Shift+Enter, Enter and Shift+Enter respectively.

I ended up fixing the emoji/mention pop-up behavior as a side product.
If any of the three combos are pressed, the pop-up is handled and
the event is accepted. This makes it impossible to accidentally send the
message if a pop-up is open.

If an Enter combo didn't match, it's passed to the next event handler.

The old `invertEnterKey` is migrated to the new `sendMessageKey`,
so this change doesn't change the existing preference.
This commit is contained in:
Matti Viljanen 2025-09-27 17:41:43 +03:00
parent 2769642d3c
commit 451e88fe72
3 changed files with 81 additions and 42 deletions

View file

@ -170,15 +170,13 @@ Rectangle {
} else if (event.matches(StandardKey.SelectAll) && popup.opened) { } else if (event.matches(StandardKey.SelectAll) && popup.opened) {
completer.completerName = ""; completer.completerName = "";
popup.close(); popup.close();
} else if (event.matches(StandardKey.InsertLineSeparator)) { } else if (event.key == Qt.Key_Enter || event.key == Qt.Key_Return) {
if (popup.opened) // Handling popup takes priority over newline and sending message.
popup.close(); if (popup.opened &&
if (Settings.invertEnterKey) { (event.modifiers == Qt.NoModifier
room.input.send(); || event.modifiers == Qt.ShiftModifier
event.accepted = true; || event.modifiers == Qt.ControlModifier)
} ) {
} else if (event.matches(StandardKey.InsertParagraphSeparator)) {
if (popup.opened) {
var currentCompletion = completer.currentCompletion(); var currentCompletion = completer.currentCompletion();
let userid = completer.currentUserid(); let userid = completer.currentUserid();
@ -191,14 +189,26 @@ Rectangle {
console.log(userid); console.log(userid);
room.input.addMention(userid, currentCompletion); room.input.addMention(userid, currentCompletion);
} }
event.accepted = true;
return;
} }
event.accepted = true;
} }
if (!Settings.invertEnterKey) { // Send message Enter key combination event.
else if (Settings.sendMessageKey == 0 && event.modifiers == Qt.NoModifier
|| Settings.sendMessageKey == 1 && event.modifiers == Qt.ShiftModifier
|| Settings.sendMessageKey == 2 && event.modifiers == Qt.ControlModifier
) {
room.input.send(); room.input.send();
event.accepted = true; event.accepted = true;
} }
// Add newline Enter key combination event.
else if (Settings.sendMessageKey == 0 && event.modifiers == Qt.ShiftModifier
|| Settings.sendMessageKey == 1 && event.modifiers == Qt.NoModifier
|| Settings.sendMessageKey == 2 && event.modifiers == Qt.ShiftModifier
) {
messageInput.insert(messageInput.cursorPosition, "\n");
event.accepted = true;
}
// Any other Enter key combo is ignored here.
} else if (event.key == Qt.Key_Tab && (event.modifiers == Qt.NoModifier || event.modifiers == Qt.ShiftModifier)) { } else if (event.key == Qt.Key_Tab && (event.modifiers == Qt.NoModifier || event.modifiers == Qt.ShiftModifier)) {
event.accepted = true; event.accepted = true;
if (popup.opened) { if (popup.opened) {

View file

@ -35,6 +35,14 @@ QSharedPointer<UserSettings> UserSettings::instance_;
UserSettings::UserSettings() UserSettings::UserSettings()
{ {
if (settings.contains("user/invert_enter_key")) {
auto oldValue =
(settings.value("user/invert_enter_key", false).toBool() ? SendMessageKey::ShiftEnter
: SendMessageKey::Enter);
settings.setValue("user/send_message_key", static_cast<int>(oldValue));
settings.remove("user/invert_enter_key");
}
connect( connect(
QCoreApplication::instance(), &QCoreApplication::aboutToQuit, []() { instance_.clear(); }); QCoreApplication::instance(), &QCoreApplication::aboutToQuit, []() { instance_.clear(); });
} }
@ -71,8 +79,13 @@ UserSettings::load(std::optional<QString> profile)
settings.value("user/timeline/message_hover_highlight", false).toBool(); settings.value("user/timeline/message_hover_highlight", false).toBool();
enlargeEmojiOnlyMessages_ = enlargeEmojiOnlyMessages_ =
settings.value("user/timeline/enlarge_emoji_only_msg", false).toBool(); settings.value("user/timeline/enlarge_emoji_only_msg", false).toBool();
markdown_ = settings.value("user/markdown_enabled", true).toBool(); markdown_ = settings.value("user/markdown_enabled", true).toBool();
invertEnterKey_ = settings.value("user/invert_enter_key", false).toBool();
auto sendMessageKey = settings.value("user/send_message_key", 0).toInt();
if (sendMessageKey < 0 || sendMessageKey > 2)
sendMessageKey = static_cast<int>(SendMessageKey::Enter);
sendMessageKey_ = static_cast<SendMessageKey>(sendMessageKey);
bubbles_ = settings.value("user/bubbles_enabled", false).toBool(); bubbles_ = settings.value("user/bubbles_enabled", false).toBool();
smallAvatars_ = settings.value("user/small_avatars_enabled", false).toBool(); smallAvatars_ = settings.value("user/small_avatars_enabled", false).toBool();
animateImagesOnHover_ = settings.value("user/animate_images_on_hover", false).toBool(); animateImagesOnHover_ = settings.value("user/animate_images_on_hover", false).toBool();
@ -340,13 +353,12 @@ UserSettings::setMarkdown(bool state)
} }
void void
UserSettings::setInvertEnterKey(bool state) UserSettings::setSendMessageKey(SendMessageKey key)
{ {
if (state == invertEnterKey_) if (key == sendMessageKey_)
return; return;
sendMessageKey_ = key;
invertEnterKey_ = state; emit sendMessageKeyChanged(key);
emit invertEnterKeyChanged(state);
save(); save();
} }
@ -936,7 +948,7 @@ UserSettings::save()
settings.setValue("group_view", groupView_); settings.setValue("group_view", groupView_);
settings.setValue("scrollbars_in_roomlist", scrollbarsInRoomlist_); settings.setValue("scrollbars_in_roomlist", scrollbarsInRoomlist_);
settings.setValue("markdown_enabled", markdown_); settings.setValue("markdown_enabled", markdown_);
settings.setValue("invert_enter_key", invertEnterKey_); settings.setValue("send_message_key", static_cast<int>(sendMessageKey_));
settings.setValue("bubbles_enabled", bubbles_); settings.setValue("bubbles_enabled", bubbles_);
settings.setValue("small_avatars_enabled", smallAvatars_); settings.setValue("small_avatars_enabled", smallAvatars_);
settings.setValue("animate_images_on_hover", animateImagesOnHover_); settings.setValue("animate_images_on_hover", animateImagesOnHover_);
@ -1050,8 +1062,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
return tr("Scrollbars in room list"); return tr("Scrollbars in room list");
case Markdown: case Markdown:
return tr("Send messages as Markdown"); return tr("Send messages as Markdown");
case InvertEnterKey: case SendMessageKey:
return tr("Use shift+enter to send and enter to start a new line"); return tr("Send messages with a shortcut");
case Bubbles: case Bubbles:
return tr("Enable message bubbles"); return tr("Enable message bubbles");
case SmallAvatars: case SmallAvatars:
@ -1205,8 +1217,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
return i->scrollbarsInRoomlist(); return i->scrollbarsInRoomlist();
case Markdown: case Markdown:
return i->markdown(); return i->markdown();
case InvertEnterKey: case SendMessageKey:
return i->invertEnterKey(); return static_cast<int>(i->sendMessageKey());
case Bubbles: case Bubbles:
return i->bubbles(); return i->bubbles();
case SmallAvatars: case SmallAvatars:
@ -1371,10 +1383,11 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
return tr( return tr(
"Allow using markdown in messages.\nWhen disabled, all messages are sent as a plain " "Allow using markdown in messages.\nWhen disabled, all messages are sent as a plain "
"text."); "text.");
case InvertEnterKey: case SendMessageKey:
return tr( return tr(
"Invert the behavior of the enter key in the text input, making it send the message " "Select what Enter key combination sends the message. Shift+Enter adds a new line, "
"when shift+enter is pressed and starting a new line when enter is pressed."); "unless it has been selected, in which case Enter adds a new line instead.\n\n"
"If an emoji picker or a mention picker is open, it is always handled first.");
case Bubbles: case Bubbles:
return tr( return tr(
"Messages get a bubble background. This also triggers some layout changes (WIP)."); "Messages get a bubble background. This also triggers some layout changes (WIP).");
@ -1542,6 +1555,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
case CameraFrameRate: case CameraFrameRate:
case Ringtone: case Ringtone:
case ShowImage: case ShowImage:
case SendMessageKey:
return Options; return Options;
case TimelineMaxWidth: case TimelineMaxWidth:
case PrivacyScreenTimeout: case PrivacyScreenTimeout:
@ -1556,7 +1570,6 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
case GroupView: case GroupView:
case ScrollbarsInRoomlist: case ScrollbarsInRoomlist:
case Markdown: case Markdown:
case InvertEnterKey:
case Bubbles: case Bubbles:
case SmallAvatars: case SmallAvatars:
case AnimateImagesOnHover: case AnimateImagesOnHover:
@ -1675,6 +1688,12 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
tr("Only in private rooms"), tr("Only in private rooms"),
tr("Never"), tr("Never"),
}; };
case SendMessageKey:
return QStringList{
tr("Enter"),
tr("Shift+Enter"),
tr("Ctrl+Enter"),
};
case Microphone: case Microphone:
return vecToList(CallDevices::instance().names(false, i->microphone().toStdString())); return vecToList(CallDevices::instance().names(false, i->microphone().toStdString()));
case Camera: case Camera:
@ -1816,12 +1835,14 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int
} else } else
return false; return false;
} }
case InvertEnterKey: { case SendMessageKey: {
if (value.userType() == QMetaType::Bool) { auto newKey = value.toInt();
i->setInvertEnterKey(value.toBool()); if (newKey < 0 ||
return true; QMetaEnum::fromType<UserSettings::SendMessageKey>().keyCount() <= newKey)
} else
return false; return false;
i->setSendMessageKey(static_cast<UserSettings::SendMessageKey>(newKey));
return true;
} }
case Bubbles: { case Bubbles: {
if (value.userType() == QMetaType::Bool) { if (value.userType() == QMetaType::Bool) {
@ -2306,8 +2327,8 @@ UserSettingsModel::UserSettingsModel(QObject *p)
connect(s.get(), &UserSettings::markdownChanged, this, [this]() { connect(s.get(), &UserSettings::markdownChanged, this, [this]() {
emit dataChanged(index(Markdown), index(Markdown), {Value}); emit dataChanged(index(Markdown), index(Markdown), {Value});
}); });
connect(s.get(), &UserSettings::invertEnterKeyChanged, this, [this]() { connect(s.get(), &UserSettings::sendMessageKeyChanged, this, [this]() {
emit dataChanged(index(InvertEnterKey), index(InvertEnterKey), {Value}); emit dataChanged(index(SendMessageKey), index(SendMessageKey), {Value});
}); });
connect(s.get(), &UserSettings::bubblesChanged, this, [this]() { connect(s.get(), &UserSettings::bubblesChanged, this, [this]() {
emit dataChanged(index(Bubbles), index(Bubbles), {Value}); emit dataChanged(index(Bubbles), index(Bubbles), {Value});

View file

@ -29,8 +29,8 @@ class UserSettings final : public QObject
Q_PROPERTY(bool scrollbarsInRoomlist READ scrollbarsInRoomlist WRITE setScrollbarsInRoomlist Q_PROPERTY(bool scrollbarsInRoomlist READ scrollbarsInRoomlist WRITE setScrollbarsInRoomlist
NOTIFY scrollbarsInRoomlistChanged) NOTIFY scrollbarsInRoomlistChanged)
Q_PROPERTY(bool markdown READ markdown WRITE setMarkdown NOTIFY markdownChanged) Q_PROPERTY(bool markdown READ markdown WRITE setMarkdown NOTIFY markdownChanged)
Q_PROPERTY( Q_PROPERTY(SendMessageKey sendMessageKey READ sendMessageKey WRITE setSendMessageKey NOTIFY
bool invertEnterKey READ invertEnterKey WRITE setInvertEnterKey NOTIFY invertEnterKeyChanged) sendMessageKeyChanged)
Q_PROPERTY(bool bubbles READ bubbles WRITE setBubbles NOTIFY bubblesChanged) Q_PROPERTY(bool bubbles READ bubbles WRITE setBubbles NOTIFY bubblesChanged)
Q_PROPERTY(bool smallAvatars READ smallAvatars WRITE setSmallAvatars NOTIFY smallAvatarsChanged) Q_PROPERTY(bool smallAvatars READ smallAvatars WRITE setSmallAvatars NOTIFY smallAvatarsChanged)
Q_PROPERTY(bool animateImagesOnHover READ animateImagesOnHover WRITE setAnimateImagesOnHover Q_PROPERTY(bool animateImagesOnHover READ animateImagesOnHover WRITE setAnimateImagesOnHover
@ -166,6 +166,14 @@ public:
}; };
Q_ENUM(ShowImage) Q_ENUM(ShowImage)
enum class SendMessageKey
{
Enter,
ShiftEnter,
CtrlEnter,
};
Q_ENUM(SendMessageKey)
void save(); void save();
void load(std::optional<QString> profile); void load(std::optional<QString> profile);
void applyTheme(); void applyTheme();
@ -182,7 +190,7 @@ public:
void setGroupView(bool state); void setGroupView(bool state);
void setScrollbarsInRoomlist(bool state); void setScrollbarsInRoomlist(bool state);
void setMarkdown(bool state); void setMarkdown(bool state);
void setInvertEnterKey(bool state); void setSendMessageKey(SendMessageKey key);
void setBubbles(bool state); void setBubbles(bool state);
void setSmallAvatars(bool state); void setSmallAvatars(bool state);
void setAnimateImagesOnHover(bool state); void setAnimateImagesOnHover(bool state);
@ -255,7 +263,7 @@ public:
bool privacyScreen() const { return privacyScreen_; } bool privacyScreen() const { return privacyScreen_; }
int privacyScreenTimeout() const { return privacyScreenTimeout_; } int privacyScreenTimeout() const { return privacyScreenTimeout_; }
bool markdown() const { return markdown_; } bool markdown() const { return markdown_; }
bool invertEnterKey() const { return invertEnterKey_; } SendMessageKey sendMessageKey() const { return sendMessageKey_; }
bool bubbles() const { return bubbles_; } bool bubbles() const { return bubbles_; }
bool smallAvatars() const { return smallAvatars_; } bool smallAvatars() const { return smallAvatars_; }
bool animateImagesOnHover() const { return animateImagesOnHover_; } bool animateImagesOnHover() const { return animateImagesOnHover_; }
@ -328,7 +336,7 @@ signals:
void trayChanged(bool state); void trayChanged(bool state);
void startInTrayChanged(bool state); void startInTrayChanged(bool state);
void markdownChanged(bool state); void markdownChanged(bool state);
void invertEnterKeyChanged(bool state); void sendMessageKeyChanged(SendMessageKey key);
void bubblesChanged(bool state); void bubblesChanged(bool state);
void smallAvatarsChanged(bool state); void smallAvatarsChanged(bool state);
void animateImagesOnHoverChanged(bool state); void animateImagesOnHoverChanged(bool state);
@ -399,7 +407,7 @@ private:
bool groupView_; bool groupView_;
bool scrollbarsInRoomlist_; bool scrollbarsInRoomlist_;
bool markdown_; bool markdown_;
bool invertEnterKey_; SendMessageKey sendMessageKey_;
bool bubbles_; bool bubbles_;
bool smallAvatars_; bool smallAvatars_;
bool animateImagesOnHover_; bool animateImagesOnHover_;
@ -510,7 +518,7 @@ class UserSettingsModel : public QAbstractListModel
TypingNotifications, TypingNotifications,
ReadReceipts, ReadReceipts,
Markdown, Markdown,
InvertEnterKey, SendMessageKey,
Bubbles, Bubbles,
SmallAvatars, SmallAvatars,