diff --git a/share/icons/hicolor/16x16/apps/iptux-icon-reverse.png b/share/icons/hicolor/16x16/apps/iptux-icon-reverse.png new file mode 100644 index 000000000..00d1ea7d0 Binary files /dev/null and b/share/icons/hicolor/16x16/apps/iptux-icon-reverse.png differ diff --git a/share/icons/hicolor/22x22/apps/iptux-icon-reverse.png b/share/icons/hicolor/22x22/apps/iptux-icon-reverse.png new file mode 100644 index 000000000..0a0d7ac64 Binary files /dev/null and b/share/icons/hicolor/22x22/apps/iptux-icon-reverse.png differ diff --git a/share/icons/hicolor/24x24/apps/iptux-icon-reverse.png b/share/icons/hicolor/24x24/apps/iptux-icon-reverse.png new file mode 100644 index 000000000..0bc11b957 Binary files /dev/null and b/share/icons/hicolor/24x24/apps/iptux-icon-reverse.png differ diff --git a/share/icons/hicolor/32x32/apps/iptux-icon-reverse.png b/share/icons/hicolor/32x32/apps/iptux-icon-reverse.png new file mode 100644 index 000000000..ac52f41ce Binary files /dev/null and b/share/icons/hicolor/32x32/apps/iptux-icon-reverse.png differ diff --git a/share/icons/hicolor/48x48/apps/iptux-icon-reverse.png b/share/icons/hicolor/48x48/apps/iptux-icon-reverse.png new file mode 100644 index 000000000..10966a105 Binary files /dev/null and b/share/icons/hicolor/48x48/apps/iptux-icon-reverse.png differ diff --git a/share/icons/hicolor/64x64/apps/iptux-icon-reverse.png b/share/icons/hicolor/64x64/apps/iptux-icon-reverse.png new file mode 100644 index 000000000..985df507c Binary files /dev/null and b/share/icons/hicolor/64x64/apps/iptux-icon-reverse.png differ diff --git a/share/iptux/pixmaps/icon/iptux-icon-reverse.png b/share/iptux/pixmaps/icon/iptux-icon-reverse.png new file mode 100644 index 000000000..985df507c Binary files /dev/null and b/share/iptux/pixmaps/icon/iptux-icon-reverse.png differ diff --git a/src/config.h.in b/src/config.h.in index 10bba306f..f2882649a 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -11,6 +11,7 @@ #define __LOCALE_PATH "@SHARE_DIR@/locale" #define __SOUND_PATH "@SHARE_IPTUX_DIR@/sound" #define __UI_PATH "@SHARE_IPTUX_DIR@/ui" +#define __ICON_PATH "@SHARE_DIR@/icons/hicolor" #define IPTUX_PATH "/iptux" #define LOG_PATH "/iptux/log" diff --git a/src/iptux/AppIndicator.cpp b/src/iptux/AppIndicator.cpp index edf31e54f..c46ca8f09 100644 --- a/src/iptux/AppIndicator.cpp +++ b/src/iptux/AppIndicator.cpp @@ -4,12 +4,17 @@ #include #include +#include "iptux-utils/output.h" + namespace iptux { class IptuxAppIndicatorPrivate { public: IptuxAppIndicatorPrivate(IptuxAppIndicator* owner) : owner(owner) {} ~IptuxAppIndicatorPrivate() { + if (blinkTimerId) { + g_source_remove(blinkTimerId); + } if (indicator) { g_object_unref(indicator); } @@ -22,12 +27,49 @@ class IptuxAppIndicatorPrivate { GtkBuilder* menuBuilder; StatusIconMode mode = STATUS_ICON_MODE_NORMAL; int unreadCount = 0; + guint blinkTimerId = 0; + bool blinkState = false; static void onScrollEvent(IptuxAppIndicatorPrivate* self) { self->owner->sigActivateMainWindow.emit(); } }; +static gboolean blinkTimerCallback(gpointer data) { + auto priv = static_cast(data); + priv->blinkState = !priv->blinkState; + if (priv->blinkState) { + LOG_DEBUG("blinkTimerCallback: switching to reverse icon"); + app_indicator_set_icon_full(priv->indicator, "iptux-icon-reverse", + "iptux-icon-reverse"); + } else { + LOG_DEBUG("blinkTimerCallback: switching to normal icon"); + app_indicator_set_icon_full(priv->indicator, "iptux-icon", "iptux-icon"); + } + return G_SOURCE_CONTINUE; +} + +static void startBlinkTimer(IptuxAppIndicatorPrivate* priv) { + if (priv->blinkTimerId) { + LOG_DEBUG("startBlinkTimer: timer already running (id=%u)", priv->blinkTimerId); + return; + } + priv->blinkState = false; + priv->blinkTimerId = g_timeout_add(500, blinkTimerCallback, priv); + LOG_DEBUG("startBlinkTimer: blinking started (timerId=%u)", priv->blinkTimerId); +} + +static void stopBlinkTimer(IptuxAppIndicatorPrivate* priv) { + if (priv->blinkTimerId) { + LOG_DEBUG("stopBlinkTimer: blinking stopped (timerId=%u)", priv->blinkTimerId); + g_source_remove(priv->blinkTimerId); + priv->blinkTimerId = 0; + } else { + LOG_DEBUG("stopBlinkTimer: no timer was running"); + } + priv->blinkState = false; +} + IptuxAppIndicator::IptuxAppIndicator(GActionGroup* action_group) { this->priv = std::make_shared(this); @@ -42,7 +84,7 @@ IptuxAppIndicator::IptuxAppIndicator(GActionGroup* action_group) { app_indicator_set_status(priv->indicator, APP_INDICATOR_STATUS_ACTIVE); app_indicator_set_attention_icon_full(priv->indicator, "iptux-attention", "iptux-attention"); - + app_indicator_set_icon_theme_path(priv->indicator, __ICON_PATH); app_indicator_set_title(priv->indicator, _("Iptux")); priv->menuBuilder = @@ -59,8 +101,24 @@ IptuxAppIndicator::IptuxAppIndicator(GActionGroup* action_group) { } void IptuxAppIndicator::SetUnreadCount(int i) { + LOG_DEBUG("SetUnreadCount: count=%d, mode=%d", i, priv->mode); priv->unreadCount = i; - if (priv->mode == STATUS_ICON_MODE_NONE) return; + if (priv->mode == STATUS_ICON_MODE_NONE) { + LOG_DEBUG("SetUnreadCount: early return (mode=NONE)"); + return; + } + + if (priv->mode == STATUS_ICON_MODE_BLINKING) { + if (i > 0) { + startBlinkTimer(priv.get()); + } else { + stopBlinkTimer(priv.get()); + app_indicator_set_icon_full(priv->indicator, "iptux-icon", "iptux-icon"); + app_indicator_set_status(priv->indicator, APP_INDICATOR_STATUS_ACTIVE); + } + return; + } + if (i > 0) { app_indicator_set_status(priv->indicator, APP_INDICATOR_STATUS_ATTENTION); } else { @@ -69,7 +127,15 @@ void IptuxAppIndicator::SetUnreadCount(int i) { } void IptuxAppIndicator::SetMode(StatusIconMode mode) { + LOG_DEBUG("SetMode: mode=%d (old=%d)", mode, priv->mode); + StatusIconMode oldMode = priv->mode; priv->mode = mode; + + if (oldMode == STATUS_ICON_MODE_BLINKING) { + stopBlinkTimer(priv.get()); + app_indicator_set_icon_full(priv->indicator, "iptux-icon", "iptux-icon"); + } + if (mode == STATUS_ICON_MODE_NONE) { app_indicator_set_status(priv->indicator, APP_INDICATOR_STATUS_PASSIVE); } else { @@ -77,4 +143,12 @@ void IptuxAppIndicator::SetMode(StatusIconMode mode) { } } +void IptuxAppIndicator::StopBlinking() { + LOG_DEBUG("StopBlinking called"); + stopBlinkTimer(priv.get()); + app_indicator_set_icon_full(priv->indicator, "iptux-icon", "iptux-icon"); + if (priv->mode == STATUS_ICON_MODE_NONE) return; + app_indicator_set_status(priv->indicator, APP_INDICATOR_STATUS_ACTIVE); +} + } // namespace iptux diff --git a/src/iptux/AppIndicator.h b/src/iptux/AppIndicator.h index 56c5a4c96..f136a11b6 100644 --- a/src/iptux/AppIndicator.h +++ b/src/iptux/AppIndicator.h @@ -14,6 +14,7 @@ class IptuxAppIndicator { IptuxAppIndicator(GActionGroup* action_group); void SetUnreadCount(int count); void SetMode(StatusIconMode mode); + void StopBlinking(); sigc::signal sigActivateMainWindow; diff --git a/src/iptux/AppIndicatorDummy.cpp b/src/iptux/AppIndicatorDummy.cpp index 0d421f95a..fac4d442f 100644 --- a/src/iptux/AppIndicatorDummy.cpp +++ b/src/iptux/AppIndicatorDummy.cpp @@ -12,4 +12,8 @@ void IptuxAppIndicator::SetUnreadCount(int) { void IptuxAppIndicator::SetMode(StatusIconMode) { // Dummy implementation } + +void IptuxAppIndicator::StopBlinking() { + // Dummy implementation +} } // namespace iptux diff --git a/src/iptux/AppIndicatorMac.mm b/src/iptux/AppIndicatorMac.mm index cfa286df2..109657989 100644 --- a/src/iptux/AppIndicatorMac.mm +++ b/src/iptux/AppIndicatorMac.mm @@ -6,6 +6,8 @@ #include #include +#include "iptux-utils/output.h" + #import // Objective-C helper class for NSStatusItem callbacks. @@ -111,6 +113,9 @@ - (void)statusItemClicked:(id)sender { public: IptuxAppIndicatorPrivate() {} ~IptuxAppIndicatorPrivate() { + if (blinkTimerId) { + g_source_remove(blinkTimerId); + } if (statusItem) { [[NSStatusBar systemStatusBar] removeStatusItem:statusItem]; [statusItem release]; @@ -124,14 +129,20 @@ - (void)statusItemClicked:(id)sender { if (attentionIcon) { [attentionIcon release]; } + if (reverseIcon) { + [reverseIcon release]; + } } NSStatusItem* statusItem = nil; IptuxStatusItemHelper* helper = nil; NSImage* normalIcon = nil; NSImage* attentionIcon = nil; + NSImage* reverseIcon = nil; StatusIconMode mode = STATUS_ICON_MODE_NORMAL; int unreadCount = 0; + guint blinkTimerId = 0; + bool blinkState = false; }; IptuxAppIndicator::IptuxAppIndicator(GActionGroup* action_group) { @@ -143,6 +154,7 @@ - (void)statusItemClicked:(id)sender { // Load icons priv->normalIcon = loadIcon("iptux-icon", 64); priv->attentionIcon = loadIcon("iptux-attention", 64); + priv->reverseIcon = loadIcon("iptux-icon-reverse", 64); // Create status item priv->statusItem = @@ -186,9 +198,68 @@ - (void)statusItemClicked:(id)sender { [menu release]; } +static gboolean blinkTimerCallback(gpointer data) { + auto priv = static_cast(data); + if (!priv->statusItem) { + LOG_DEBUG("blinkTimerCallback: statusItem is nil, removing timer"); + return G_SOURCE_REMOVE; + } + priv->blinkState = !priv->blinkState; + if (priv->blinkState && priv->reverseIcon) { + LOG_DEBUG("blinkTimerCallback: switching to reverse icon"); + priv->statusItem.button.image = priv->reverseIcon; + } else if (priv->normalIcon) { + LOG_DEBUG("blinkTimerCallback: switching to normal icon"); + priv->statusItem.button.image = priv->normalIcon; + } else { + LOG_DEBUG("blinkTimerCallback: no icon available (blinkState=%d, reverseIcon=%p, normalIcon=%p)", + priv->blinkState, priv->reverseIcon, priv->normalIcon); + } + return G_SOURCE_CONTINUE; +} + +static void startBlinkTimer(IptuxAppIndicatorPrivate* priv) { + if (priv->blinkTimerId) { + LOG_DEBUG("startBlinkTimer: timer already running (id=%u)", priv->blinkTimerId); + return; + } + priv->blinkState = false; + priv->blinkTimerId = g_timeout_add(500, blinkTimerCallback, priv); + LOG_DEBUG("startBlinkTimer: blinking started (timerId=%u)", priv->blinkTimerId); +} + +static void stopBlinkTimer(IptuxAppIndicatorPrivate* priv) { + if (priv->blinkTimerId) { + LOG_DEBUG("stopBlinkTimer: blinking stopped (timerId=%u)", priv->blinkTimerId); + g_source_remove(priv->blinkTimerId); + priv->blinkTimerId = 0; + } else { + LOG_DEBUG("stopBlinkTimer: no timer was running"); + } + priv->blinkState = false; +} + void IptuxAppIndicator::SetUnreadCount(int count) { + LOG_DEBUG("SetUnreadCount: count=%d, mode=%d, statusItem=%p", count, priv->mode, + priv->statusItem); priv->unreadCount = count; - if (!priv->statusItem || priv->mode == STATUS_ICON_MODE_NONE) return; + if (!priv->statusItem || priv->mode == STATUS_ICON_MODE_NONE) { + LOG_DEBUG("SetUnreadCount: early return (statusItem=%p, mode=%d)", + priv->statusItem, priv->mode); + return; + } + + if (priv->mode == STATUS_ICON_MODE_BLINKING) { + if (count > 0) { + startBlinkTimer(priv.get()); + } else { + stopBlinkTimer(priv.get()); + if (priv->normalIcon) { + priv->statusItem.button.image = priv->normalIcon; + } + } + return; + } if (count > 0 && priv->attentionIcon) { priv->statusItem.button.image = priv->attentionIcon; @@ -198,12 +269,31 @@ - (void)statusItemClicked:(id)sender { } void IptuxAppIndicator::SetMode(StatusIconMode mode) { + LOG_DEBUG("SetMode: mode=%d (old=%d)", mode, priv->mode); + StatusIconMode oldMode = priv->mode; priv->mode = mode; if (!priv->statusItem) return; priv->statusItem.visible = (mode != STATUS_ICON_MODE_NONE); + + if (oldMode == STATUS_ICON_MODE_BLINKING) { + stopBlinkTimer(priv.get()); + if (priv->normalIcon) { + priv->statusItem.button.image = priv->normalIcon; + } + } + if (mode != STATUS_ICON_MODE_NONE) { SetUnreadCount(priv->unreadCount); } } +void IptuxAppIndicator::StopBlinking() { + LOG_DEBUG("StopBlinking called"); + stopBlinkTimer(priv.get()); + if (!priv->statusItem || priv->mode == STATUS_ICON_MODE_NONE) return; + if (priv->normalIcon) { + priv->statusItem.button.image = priv->normalIcon; + } +} + } // namespace iptux diff --git a/src/iptux/Application.cpp b/src/iptux/Application.cpp index cc9b2062a..ff21a4594 100644 --- a/src/iptux/Application.cpp +++ b/src/iptux/Application.cpp @@ -135,6 +135,10 @@ void Application::onStartup(Application& self) { }); self.cthrd->sigUnreadMsgCountUpdated.connect( sigc::mem_fun(*self.app_indicator, &IptuxAppIndicator::SetUnreadCount)); + + // Removed notify::is-active StopBlinking handler. + // Blinking now stops only when the user interacts with the dialog + // (button-press-event or key-press-event via ClearNotify). } bool use_app_menu = true; diff --git a/src/iptux/DialogBase.cpp b/src/iptux/DialogBase.cpp index b885f4ad2..dc15c8444 100644 --- a/src/iptux/DialogBase.cpp +++ b/src/iptux/DialogBase.cpp @@ -225,7 +225,7 @@ void DialogBase::MainWindowSignalSetup(GtkWindow* window) { G_CALLBACK(DragDataReceived), this); g_signal_connect(window, "configure-event", G_CALLBACK(WindowConfigureEvent), &dtset); - g_signal_connect(window, "focus-in-event", G_CALLBACK(ClearNotify), NULL); + g_signal_connect(window, "event-after", G_CALLBACK(ClearNotify), NULL); } /** @@ -470,13 +470,20 @@ void DialogBase::DialogDestory(DialogBase* dialog) { /** * 清除提示,这个提示只是窗口闪动的提示 */ -gboolean DialogBase::ClearNotify(GtkWidget* window, GdkEventConfigure*) { +void DialogBase::ClearNotify(GtkWidget* window, GdkEvent* event) { + if (event) { + GdkEventType type = gdk_event_get_event_type(event); + if (type != GDK_BUTTON_PRESS && type != GDK_KEY_PRESS) + return; + LOG_DEBUG("ClearNotify: user interaction on dialog window (type=%d)", type); + } else { + LOG_DEBUG("ClearNotify: called directly (window is active)"); + } if (gtk_window_get_urgency_hint(GTK_WINDOW(window))) gtk_window_set_urgency_hint(GTK_WINDOW(window), FALSE); DialogBase* self = (DialogBase*)g_object_get_data(G_OBJECT(window), "session-class"); self->grpinf->readAllMsg(); - return FALSE; } /** diff --git a/src/iptux/DialogBase.h b/src/iptux/DialogBase.h index 356c1d9fc..e6a45142b 100644 --- a/src/iptux/DialogBase.h +++ b/src/iptux/DialogBase.h @@ -64,7 +64,7 @@ class DialogBase : public SessionAbstract, public sigc::trackable { // 回调部分 static void DialogDestory(DialogBase*); - static gboolean ClearNotify(GtkWidget* window, GdkEventConfigure* event); + static void ClearNotify(GtkWidget* window, GdkEvent* event); static void DragDataReceived(DialogBase* dlgpr, GdkDragContext* context, gint x, diff --git a/src/iptux/DialogPeer.cpp b/src/iptux/DialogPeer.cpp index c19a450d8..95ff247af 100644 --- a/src/iptux/DialogPeer.cpp +++ b/src/iptux/DialogPeer.cpp @@ -1068,9 +1068,9 @@ void DialogPeer::onNewFileReceived(GroupInfo*) { void DialogPeer::onGroupInfoUpdated(GroupInfo* groupInfo) { if (groupInfo != this->grpinf) return; - if (gtk_window_is_active(GTK_WINDOW(this->window))) { - ClearNotify(GTK_WIDGET(this->window), nullptr); - } + // Don't auto-read messages just because the window is active. + // Messages are marked as read only on user interaction + // (button-press or key-press via ClearNotify). } void DialogPeer::refreshSendAction() { diff --git a/src/iptux/UiCoreThread.cpp b/src/iptux/UiCoreThread.cpp index 757059f04..d2eabb7c7 100644 --- a/src/iptux/UiCoreThread.cpp +++ b/src/iptux/UiCoreThread.cpp @@ -494,7 +494,9 @@ void UiCoreThread::PopItemFromEnclosureList(FileInfo* file) { delete file; } -void UiCoreThread::onGroupInfoMsgCountUpdate(GroupInfo* grpinf, int, int) { +void UiCoreThread::onGroupInfoMsgCountUpdate(GroupInfo* grpinf, int oldCount, int newCount) { + LOG_DEBUG("onGroupInfoMsgCountUpdate: oldCount=%d, newCount=%d, totalUnread=%d", + oldCount, newCount, unread_msg_count()); sigGroupInfoUpdated.emit(grpinf); sigUnreadMsgCountUpdated.emit(unread_msg_count()); } diff --git a/src/iptux/UiModels.cpp b/src/iptux/UiModels.cpp index ec243be59..a75d1de84 100644 --- a/src/iptux/UiModels.cpp +++ b/src/iptux/UiModels.cpp @@ -532,11 +532,14 @@ void GroupInfo::clearInputBuffer() { void GroupInfo::addMsgCount(int i) { int oldCount = getUnreadMsgCount(); allMsgCount += i; + LOG_DEBUG("addMsgCount: i=%d, oldCount=%d, newCount=%d", i, oldCount, + getUnreadMsgCount()); signalUnreadMsgCountUpdated.emit(this, oldCount, getUnreadMsgCount()); } void GroupInfo::readAllMsg() { int oldCount = getUnreadMsgCount(); + LOG_DEBUG("readAllMsg: oldCount=%d", oldCount); if (oldCount != 0) { readMsgCount = allMsgCount; signalUnreadMsgCountUpdated.emit(this, oldCount, getUnreadMsgCount()); @@ -806,7 +809,9 @@ void GroupInfo::_addMsgPara(const MsgPara& para, time_t now) { break; } } - addMsgCount(1); + if (para.stype == MessageSourceType::PAL) { + addMsgCount(1); + } } bool transModelIsFinished(TransModel* model) {