#priority init 100 import elements.BaseGuiElement; import elements.GuiSprite; import elements.GuiMarkupText; import elements.MarkupTooltip; import elements.Gui3DObject; import notifications; import influence; import planet_loyalty; import util.icon_view; import util.formatting; from util.draw_model import drawLitModel; from obj_selection import selectObject; from overlays.AnomalyOverlay import AnomalyOverlay; from gui import animate_speed, animate_time; import hooks; import tabs.tabbar; import void showInfluenceVote(uint voteId) from "tabs.DiplomacyTab"; import Tab@ createInfluenceVoteTab(int voteId) from "tabs.InfluenceVoteTab"; import Tab@ createDiplomacyTab() from "tabs.DiplomacyTab"; import void zoomTabTo(Object@ obj) from "tabs.GalaxyTab"; import Tab@ createGalaxyTab(Object@ obj) from "tabs.GalaxyTab"; const double ANIM_SPEED = 1000.0; const double CLASS_SCROLL = 30.0; const double EVENT_SCROLL = 50.0; const double FLASH_TIME = 1.0; const double ICON_PULSE_TIME = 0.4; void showNotifications() { overlay.visible = true; } void hideNotifications() { overlay.visible = false; } void gotoNotification(Notification@ n, bool bg = false) { if(n.type == NT_TreatyEvent) { Tab@ tb = findTab(TC_Diplomacy); if(tb is null) @tb = newTab(createDiplomacyTab()); switchToTab(tb); } else if(n.type == NT_Card && cast(n).voteId >= 0) { if(bg) newTab(createInfluenceVoteTab(cast(n).voteId)); else showInfluenceVote(cast(n).voteId); } else { Object@ obj = n.relatedObject; if(obj !is null) { zoomTabTo(obj); if(n.type == NT_FlagshipBuilt) selectObject(obj); } } } class NotifyOverlay : BaseGuiElement { array classes; NotifyOverlay() { super(null, Alignment().fill()); updateAbsolutePosition(); } //Don't consider this element part of the tree IGuiElement@ elementFromPosition(const vec2i& pos) { IGuiElement@ elem = BaseGuiElement::elementFromPosition(pos); if(elem is this) return null; return elem; } }; class ClassDisplay { Notification@ base; ClassDisplay(Notification@ n) { @base = n; } string formatDescription() { return ""; } bool update(NotifyClass@ cls, double time) { return true; } bool goto(Notification@ evt, bool bg = false) { return false; } void draw(NotifyClass@ cls, const recti& iconPos) { } void add(NotifyClass@ cls, Notification@ n) { } } class NotifyClass : BaseGuiElement { recti animPos; bool shown = false; int textWidth = 0; double scroll = 0.0; bool animating = false; bool closing = false; bool hovered = false; Notification@ base; Sprite icon; array active; double flashTime = 0; int flashes = 0; ClassDisplay@ clsDisp; const Model@ iconModel; const Material@ iconMaterial; Object@ related; Draw3D@ drawObject; MarkupRenderer render; NotifyClass(Notification@ n) { super(overlay, recti()); @base = n; @clsDisp = createClassDisplay(base); string clsText = n.formatClass(); if(clsText.length == 0) clsText = n.formatEvent(); else if(n.formatEvent().length != 0) add(n); icon = n.icon; if(!icon.valid) { @iconModel = n.model; @iconMaterial = n.material; if(iconModel is null) { @related = n.relatedObject; if(related !is null) @drawObject = makeDrawMode(related); } } render.defaultFont = FT_Medium; render.expandWidth = true; render.parse(skin, clsText, recti(0, 0, 300, 34)); render.update(skin, recti(0, 0, 300, 34)); textWidth = clamp(render.width+16, 300, 800); updateTooltip(); } void add(Notification@ n) { if(active.length != 0) { NotifyEvent@ prev = active[active.length - 1]; prev.visible = false; } NotifyEvent evt(this, n); animate_speed(evt, getEventArea(rect), ANIM_SPEED); evt.animating = true; active.insertLast(evt); flashes += n.flashCount; updateTooltip(); if(n.pulseIcon) PulseIcon(this, n.icon); clsDisp.add(this, n); } void animate(const recti& pos) { if(closing) return; if(!shown) { rect = pos + vec2i(pos.width+60, 0); shown = true; } animating = true; animPos = pos; animate_speed(this, animPos, ANIM_SPEED); if(active.length != 0) { NotifyEvent@ prev = active[active.length - 1]; prev.animating = true; animate_speed(prev, getEventArea(animPos), ANIM_SPEED); } updateAbsolutePosition(); } bool onGuiEvent(const GuiEvent& evt) override { if(evt.type == GUI_Animation_Complete) { animating = false; if(closing) remove(); return true; } else if(evt.type == GUI_Mouse_Entered) { hovered = true; } else if(evt.type == GUI_Mouse_Left) { hovered = false; } return BaseGuiElement::onGuiEvent(evt); } recti getTextArea(const recti& atPos) { return recti_area( vec2i(8+max(atPos.size.width - textWidth - 80 - 8, 0), 4)+atPos.topLeft, vec2i(min(atPos.size.width-16, textWidth + 80), 26)); } recti getEventArea(const recti& atPos) { recti textArea = getTextArea(atPos); int offset = max(30, atPos.size.width - textArea.width+30); return recti_area(vec2i(offset, 33), vec2i(atPos.size.width - offset, 26)); } recti getIconArea(const recti& atPos) { recti iconPos = recti_area(atPos.botRight - vec2i(80, 60), vec2i(80, 60)); return iconPos; } void updateAbsolutePosition() override { render.update(skin, getTextArea(rect)); BaseGuiElement::updateAbsolutePosition(); } bool update(double time) { //Update tooltip state if(settings::bIconifyNotifications) { if(tooltipObject is null) updateTooltip(); } else { if(tooltipObject !is null) @tooltipObject = null; } //Update icon if(!icon.valid) { if(iconModel is null) { if(related !is null && drawObject is null) @drawObject = makeDrawMode(related); } } //Update class display if(!clsDisp.update(this, time)) return false; return true; } void dismiss() { closing = true; animating = true; active.length = 0; animate_speed(this, rect+vec2i(size.width, 0), ANIM_SPEED); overlay.classes.remove(this); updatePositions(); } void dismissEvents() { for(uint i = 0, cnt = active.length; i < cnt; ++i) active[i].remove(); active.length = 0; } void dismissEvent(NotifyEvent@ evt) { active.remove(evt); evt.remove(); if(active.length != 0) { NotifyEvent@ prev = active[active.length - 1]; prev.animating = true; prev.visible = true; animate_speed(prev, getEventArea(animPos), ANIM_SPEED); } } void popEvent(bool bg = false) { NotifyEvent@ evt; Notification@ n; if(active.length == 0) { @n = base; } else { @evt = active[active.length - 1]; @n = evt.notification; } if(!clsDisp.goto(n, bg)) gotoNotification(n, bg); switch(n.triggerMode) { case NTM_Ignore: break; case NTM_KillClass: dismiss(); break; case NTM_KillEvents: dismissEvents(); break; case NTM_KillTop: if(evt !is null) dismissEvent(evt); break; } flashes = 0; updateTooltip(); } void updateTooltip() { if(!settings::bIconifyNotifications) { @tooltipObject = null; return; } string tt = base.formatClass(); if(tt.length == 0) tt = base.formatEvent(); tt = "[font=Medium]"+tt+"[/font]"; if(clsDisp !is null) tt += clsDisp.formatDescription(); for(int i = active.length - 1; i >= 0; --i) { Notification@ n = active[i].notification; tt += "\n\n"; tt += formatTimeStamp(n.time); tt += " [offset=80]"; string evt = n.formatEvent(); if(evt.length == 0) evt = n.formatClass(); tt += evt; tt += "[/offset]"; } setMarkupTooltip(this, tt, true, textWidth+30); } bool onMouseEvent(const MouseEvent& mevt, IGuiElement@ caller) override { if(mevt.type == MET_Button_Down) { return true; } else if(mevt.type == MET_Button_Up) { if(mevt.button == 0) { popEvent(); } else if(mevt.button == 1) { if(caller is this) dismiss(); else if(active.length != 0) dismissEvent(active[active.length - 1]); } else if(mevt.button == 2) { popEvent(true); } return true; } return BaseGuiElement::onMouseEvent(mevt, caller); } //Don't consider this element part of the tree IGuiElement@ elementFromPosition(const vec2i& pos) { IGuiElement@ elem = BaseGuiElement::elementFromPosition(pos); if(settings::bIconifyNotifications) { if(getIconArea(rect).isWithin(pos)) return this; else return null; } else { if(elem is this) { if(getTextArea(rect).isWithin(pos)) return this; if(getIconArea(rect).isWithin(pos)) return this; return null; } } return elem; } void draw() override { recti textBox = getTextArea(rect); recti iconPos = getIconArea(rect); //Show the class text if(!settings::bIconifyNotifications) { skin.draw(SS_PlainOverlay, SF_Normal, textBox.padded(-8, -4)); } BaseGuiElement::draw(); if(!settings::bIconifyNotifications) { setClip(textBox); int tbWidth = textBox.width - iconPos.width; if(textWidth > tbWidth && !animating) { int area = textWidth + max(tbWidth / 3, 50); scroll = (scroll + frameLength * CLASS_SCROLL) % double(area); int startPos = area - int(scroll); if(startPos < textBox.width) { render.draw(skin, recti_area(textBox.topLeft+vec2i(startPos, 1), vec2i(size.width*2, textBox.height))); } } render.draw(skin, recti_area(textBox.topLeft-vec2i(int(scroll), -1), vec2i(size.width*2, textBox.height))); } //Show the icon clearClip(); if(icon.sheet is spritesheet::Notifications) { icon.draw(iconPos); } else { spritesheet::Notifications.draw(0, iconPos); if(icon.valid) { vec2i iSize = icon.size; recti pos = iconPos.padded(30,0,0,0).aspectAligned(double(iSize.width) / double(iSize.height), 1.0, 0.5); icon.draw(pos); } else if(iconModel !is null) { recti pos = iconPos.padded(30,0,0,0).aspectAligned(1.0, 1.0, 0.5); drawLitModel(iconModel, iconMaterial, pos, quaterniond_fromAxisAngle(vec3d_front(), 0.5*pi)); } else if(drawObject !is null) { recti pos = iconPos.padded(30,0,0,0).aspectAligned(1.0, 1.0, 0.5); if(related.owner !is null) NODE_COLOR = related.owner.color; else NODE_COLOR = colors::White; drawObject.preRender(related); drawObject.draw(pos, quaterniond_fromAxisAngle(vec3d_front(), 0.5*pi)); } } //Show class-specifics clsDisp.draw(this, iconPos); //Show flashes if(flashTime > 0.0) { float alpha = 0.f; if(flashTime < FLASH_TIME * 0.5) alpha = 1.f - (FLASH_TIME * 0.5 - flashTime) / (FLASH_TIME * 0.5); else alpha = 1.f - (flashTime - FLASH_TIME * 0.5) / (FLASH_TIME * 0.5); Color col(0xffffffff); col.a = alpha * 255; spritesheet::Notifications.draw(7, iconPos, col); flashTime -= frameLength; } else { if(flashes > 0) { --flashes; flashTime = FLASH_TIME; } } } }; class PulseIcon : BaseGuiElement { Sprite sprite; Color color; PulseIcon(NotifyClass@ cls, const Sprite& icon) { noClip = true; sprite = icon; super(cls, recti_area(cls.size, vec2i(1, 1))); recti target(cls.size * -2, cls.size); vec2i iSize = icon.size; if(iSize.x != 0 && iSize.y != 0) target = target.aspectAligned(double(iSize.x) / double(iSize.y), 1.0, 1.0); animate_time(this, target, ICON_PULSE_TIME); rect = target; updateAbsolutePosition(); } bool onGuiEvent(const GuiEvent& evt) override { if(evt.type == GUI_Animation_Complete) { remove(); return true; } return BaseGuiElement::onGuiEvent(evt); } void draw() override { clearClip(); sprite.draw(AbsolutePosition, color); BaseGuiElement::draw(); } }; class PulseCard : PulseIcon { Empire@ from; PulseCard(NotifyClass@ cls, const Sprite& icon, Empire@ emp) { super(cls, icon); @from = emp; color = Color(0xffffff80); } void draw() override { PulseIcon::draw(); if(from !is null) { Color color = from.color; color.a = 0x80; material::CardBorder.draw(AbsolutePosition, color); } } }; class NotifyEvent : BaseGuiElement { Notification@ notification; MarkupRenderer render; int textWidth; double scroll = 0.0; bool animating = false; NotifyEvent(NotifyClass@ cls, Notification@ n) { @notification = n; super(cls, recti_area(vec2i(cls.size.width+20, 33), vec2i(20, 26))); string evtText = n.formatEvent(); render.expandWidth = true; render.parse(skin, evtText, recti(0, 0, 300, 26)); render.update(skin, recti(0, 0, 300, 26)); textWidth = render.width+16; } bool onGuiEvent(const GuiEvent& evt) override { if(evt.type == GUI_Animation_Complete) { animating = false; return true; } return BaseGuiElement::onGuiEvent(evt); } void draw() override { if(settings::bIconifyNotifications) return; recti textBox = AbsolutePosition; skin.draw(SS_PlainOverlay, SF_Normal, textBox); //Show the event text if(textWidth > textBox.width - 80 && !animating) { int area = textWidth + max((textBox.width - 80) / 3, 50); scroll = (scroll + frameLength * EVENT_SCROLL) % double(area); int startPos = area - int(scroll); if(startPos < textBox.width) { render.draw(skin, recti_area(textBox.topLeft+vec2i(startPos, 5), vec2i(size.width*2, textBox.height))); } } render.draw(skin, recti_area(textBox.topLeft-vec2i(int(scroll), -5), vec2i(size.width*2, textBox.height))); BaseGuiElement::draw(); } }; void handle(Notification@ n) { if(n.time <= minGameTime) return; const SoundSource@ sound = n.sound; if(sound !is null) sound.play(priority=true); //Check if this notification can be added to any classes for(uint i = 0, cnt = overlay.classes.length; i < cnt; ++i) { NotifyClass@ cls = overlay.classes[i]; if(cls.base.sharesClass(n)) { cls.add(n); if(i != 0) { overlay.classes.removeAt(i); overlay.classes.insertLast(cls); } return; } } //Add a new class if none is there overlay.classes.insertLast(NotifyClass(n)); while(overlay.classes.length > uint(settings::dMaxNotifications)) overlay.classes[0].dismiss(); } void updatePositions() { vec2i osize = overlay.size; int w = osize.width * 0.25, h = 60, o = 12; for(uint i = 0, cnt = overlay.classes.length; i < cnt; ++i) { NotifyClass@ cls = overlay.classes[i]; recti target = recti_area(osize - vec2i(w, h+h*i+o*i+8), vec2i(w, h)); if(target != cls.animPos) cls.animate(target); } } uint nextNotify = 0; array unhandled; void tick(double time) { //Update notifications uint latest = playerEmpire.notificationCount; if(latest != nextNotify) { receiveNotifications(unhandled, playerEmpire.getNotifications(100, nextNotify, false)); nextNotify = latest; } //Handle all unhandled if(unhandled.length > 0) { for(uint i = 0, cnt = unhandled.length; i < cnt; ++i) handle(unhandled[i]); unhandled.length = 0; } //Update positioning of classes updatePositions(); //Update clasess for(uint i = 0, cnt = overlay.classes.length; i < cnt; ++i) { NotifyClass@ cls = overlay.classes[i]; if(!cls.update(time)) { cls.dismiss(); --i; --cnt; } } } class VoteDisplay : ClassDisplay { double timer = 0.0; InfluenceVote@ vote; VoteDisplay(Notification@ n) { super(n); @vote = InfluenceVote(); VoteNotification@ vn = cast(n); receive(getInfluenceVoteByID(vn.vote.id), vote); } bool update(NotifyClass@ cls, double time) override { timer += time; if(timer >= 1.0) { timer = 0.0; VoteNotification@ vn = cast(base); receive(getInfluenceVoteByID(vn.vote.id), vote); if(!vote.active && base.time + 10.0 < gameTime && (cls.active.length == 0 || settings::bIconifyNotifications)) return false; } return true; } string formatDescription() { if(vote !is null && vote.id != uint(-1)) { if(vote.startedBy is defaultEmpire) return "\n[font=Small]"+vote.type.description+"[/font]"; } return ""; } void draw(NotifyClass@ cls, const recti& iconPos) override { const Font@ ft = cls.skin.getFont(FT_Bold); if(vote !is null && vote.id != uint(-1)) { //Show type of vote icon vote.type.icon.draw(iconPos.padded(30, 0, -10, 0).aspectAligned(vote.type.icon.aspect), Color(0xffffff70)); if(vote.type.targets.targets.length > 0 && vote.type.targets.targets[0].type == TT_Object) { drawObjectIcon(vote.targets.targets[0].obj, iconPos.padded(30, 10, 10, 10)); } //Show starter of proposition as glow color Color col = vote.startedBy.color; col.a = 0x80; spritesheet::Notifications.draw(8, iconPos, col); //Timer Color timerColor; string timeText; if(vote.totalFor > vote.totalAgainst) { timerColor = Color(0x00ff00ff); timeText = formatShortTime(vote.remainingTime); } else { timerColor = Color(0xff0000ff); timeText = formatShortTime(vote.remainingTime); } ft.draw(iconPos, timeText, locale::SHORT_ELLIPSIS, timerColor, 0.85, 0.9); //Status Color voteColor(0x00ff00ff); if(vote.totalAgainst >= vote.totalFor) voteColor = Color(0xff0000ff); int net = vote.totalFor - vote.totalAgainst; string netStr = toString(net, 0); if(net > 0) netStr = "+"+netStr; ft.draw(iconPos, netStr, locale::SHORT_ELLIPSIS, voteColor, 0.75, 0.1); } } void add(NotifyClass@ cls, Notification@ n) override { VoteNotification@ vn = cast(n); if(vn !is null) { if(vn.event.type == IVET_Card) PulseCard(cls, vn.event.cardEvent.card.type.icon, vn.event.emp); } } bool goto(Notification@ evt, bool bg = false) override { if(bg) { if(!ctrlKey && vote.type.targets.targets.length > 0 && vote.type.targets.targets[0].type == TT_Object) zoomTabTo(vote.targets.targets[0].obj); else newTab(createInfluenceVoteTab(vote.id)); } else showInfluenceVote(vote.id); return true; } }; class ContestDisplay : ClassDisplay { Region@ region; uint loyaltyState = CM_None; ContestDisplay(Notification@ n) { super(n); Object@ obj = base.relatedObject; @region = cast(obj); if(region is null) @region = obj.region; } bool update(NotifyClass@ cls, double time) override { //Dismiss empty contested notifications if the //system is no longer contested. if(base.time + 10.0 < gameTime) { //Check if we have any substantial actual notifications //left, and don't kill it if we do bool hasSubstantial = false; if(!settings::bIconifyNotifications) { for(uint i = 0, cnt = cls.active.length; i < cnt; ++i) { WarEventNotification@ wn = cast(cls.active[i].notification); if(wn is null || wn.eventType != WET_ContestedSystem) { hasSubstantial = true; break; } } } if(region !is null && !hasSubstantial) { if(region.ContestedMask & playerEmpire.mask == 0) return false; } } if(region !is null) loyaltyState = region.getContestedState(playerEmpire); return true; } void draw(NotifyClass@ cls, const recti& iconPos) override { if(loyaltyState != CM_None) { Color col = ContestedColors[loyaltyState]; col.a = 0x80; spritesheet::Notifications.draw(8, iconPos, col); } } }; class AnomalyDisplay : ClassDisplay { Draw3D@ mode; AnomalyDisplay(Notification@ n) { super(n); AnomalyNotification@ dn = cast(n); @mode = makeDrawMode(dn.obj); } bool update(NotifyClass@ cls, double time) override { AnomalyNotification@ n = cast(cls.base); if(!n.obj.valid) return false; return true; } void draw(NotifyClass@ cls, const recti& iconPos) override { if(mode !is null) mode.draw(iconPos, quaterniond()); } bool goto(Notification@ evt, bool bg = false) override { AnomalyNotification@ n = cast(evt); if(bg) zoomTabTo(n.obj); else AnomalyOverlay(ActiveTab, cast(n.obj)); return true; } }; ClassDisplay@ createClassDisplay(Notification@ n) { ClassDisplay@ cls; switch(n.type) { case NT_Vote: @cls = VoteDisplay(n); break; case NT_WarEvent: @cls = ContestDisplay(n); break; case NT_Anomaly: @cls = AnomalyDisplay(n); break; } if(cls is null) @cls = ClassDisplay(n); return cls; } NotifyOverlay@ overlay; double minGameTime = 0.0; void init() { @overlay = NotifyOverlay(); minGameTime = gameTime; } void save(SaveFile& file) { uint cnt = overlay.classes.length; file << cnt; for(uint i = 0; i < cnt; ++i) { NotifyClass@ cls = overlay.classes[i]; file << cls.base; uint evcnt = cls.active.length; file << evcnt; for(uint j = 0; j < evcnt; ++j) { NotifyEvent@ evt = cls.active[j]; file << evt.notification; } } } void load(SaveFile& file) { uint cnt = 0; file >> cnt; for(uint i = 0; i < cnt; ++i) { Notification@ n = loadNotification(file); NotifyClass cls(n); overlay.classes.insertLast(cls); uint evcnt = 0; file >> evcnt; for(uint j = 0; j < evcnt; ++j) { Notification@ n = loadNotification(file); cls.add(n); } } updatePositions(); }