1
0
mirror of https://github.com/vcmi/vcmi.git synced 2026-05-22 09:55:17 +02:00

code review

This commit is contained in:
Laserlicht
2026-04-19 17:36:50 +02:00
parent 0981de1dda
commit cd79b0932e
12 changed files with 67 additions and 79 deletions
@@ -1096,18 +1096,12 @@
"vcmi.tutorialWindow.decription.RadialWheel": "Swiping opens radial wheel for various actions, such as creature/hero management and town ordering.",
"vcmi.tutorialWindow.decription.RightClick": "Touch and hold the element on which you want to right-click. Touch the free area to close.",
"vcmi.tutorialWindow.title": "Touchscreen Introduction",
"vcmi.wiki.button.back": "Back",
"vcmi.wiki.button.close": "Close",
"vcmi.wiki.content.placeholder": "Select a category and an entry to view its description.",
"vcmi.wiki.header.category": "Category",
"vcmi.wiki.header.entry": "Entry",
"vcmi.wiki.header.information": "Information",
"vcmi.wiki.search.hint": "Search...",
"vcmi.wiki.stub.body": "Detailed information about \"%s\" will be added here in a future update.",
"vcmi.wiki.stub.category": "[Category: %s]",
"vcmi.wiki.stub.hint": "You can describe stats, background lore, tactical notes, and other relevant information for this entry.",
"vcmi.wiki.stub.intro": "This entry is a stub.",
"vcmi.wiki.title": "Wiki",
"vcmi.wiki.title": "Heroes-o-pedia",
"vcmi.wiki.category.glossary": "Glossary",
"vcmi.wiki.category.town": "Town",
"vcmi.wiki.category.hero": "Hero",
@@ -1125,22 +1119,15 @@
"vcmi.wiki.creature.cost": "Cost",
"vcmi.wiki.creature.stat.level": "Level",
"vcmi.wiki.creature.stat.level.short": "Lv",
"vcmi.wiki.creature.stat.attack": "Attack",
"vcmi.wiki.creature.stat.attack.short": "Atk",
"vcmi.wiki.creature.stat.defense": "Defense",
"vcmi.wiki.creature.stat.defense.short": "Def",
"vcmi.wiki.creature.stat.damage": "Damage",
"vcmi.wiki.creature.stat.damage.short": "Dmg",
"vcmi.wiki.creature.stat.speed": "Speed",
"vcmi.wiki.creature.stat.speed.short": "Spd",
"vcmi.wiki.creature.stat.hitpoints": "Hit Points",
"vcmi.wiki.creature.stat.hitpoints.short": "HP",
"vcmi.wiki.creature.stat.growth": "Growth",
"vcmi.wiki.creature.stat.growth.short": "Gr",
"vcmi.wiki.creature.stat.aivalue": "AI Value",
"vcmi.wiki.creature.stat.aivalue.short": "AI",
"vcmi.wiki.creature.stat.shots": "Shots",
"vcmi.wiki.creature.stat.spellpoints": "Spell Points",
"vcmi.wiki.creature.stat.fightvalue": "Fight Value",
"vcmi.wiki.creature.stat.hordegrowth": "Horde Growth",
"vcmi.wiki.creature.stat.doublewide": "Double Wide",
@@ -1153,7 +1140,6 @@
"vcmi.wiki.hero.specialty": "Specialty",
"vcmi.wiki.hero.gender.male": "Male",
"vcmi.wiki.hero.gender.female": "Female",
"vcmi.wiki.hero.column.creature": "Creature",
"vcmi.wiki.hero.column.amount": "Amount",
"vcmi.wiki.hero.column.skill": "Skill",
"vcmi.wiki.hero.column.level": "Level",
+2 -2
View File
@@ -245,11 +245,11 @@ ColorRGBA Canvas::getPixel(const Point & position) const
return ColorRGBA(color.r, color.g, color.b, color.a);
}
CanvasClipRectGuard::CanvasClipRectGuard(Canvas & canvas, const Rect & rect, bool intersect): surf(canvas.surface)
CanvasClipRectGuard::CanvasClipRectGuard(Canvas & canvas, const Rect & rect): surf(canvas.surface)
{
CSDL_Ext::getClipRect(surf, oldRect);
const Rect scaled = rect * ENGINE->screenHandler().getScalingFactor();
CSDL_Ext::setClipRect(surf, intersect ? oldRect.intersect(scaled) : scaled);
CSDL_Ext::setClipRect(surf, oldRect.intersect(scaled));
}
CanvasClipRectGuard::~CanvasClipRectGuard()
+1 -3
View File
@@ -133,8 +133,6 @@ class CanvasClipRectGuard : boost::noncopyable
Rect oldRect;
public:
/// @param intersect if true, clips to the intersection of the current clip rect and @p rect
/// instead of replacing it (prevents child widgets from overdrawing a parent clip)
CanvasClipRectGuard(Canvas & canvas, const Rect & rect, bool intersect = false);
CanvasClipRectGuard(Canvas & canvas, const Rect & rect);
~CanvasClipRectGuard();
};
+1 -1
View File
@@ -309,7 +309,7 @@ void CMultiLineLabel::showAll(Canvas & to)
Point lineStart = getTextLocation().topLeft() - visibleSize + Point(0, beginLine * fontPtr->getLineHeight());
Point lineSize = Point(getTextLocation().w, fontPtr->getLineHeight());
CanvasClipRectGuard guard(to, getTextLocation(), true); // intersect with outer (viewport) clip
CanvasClipRectGuard guard(to, getTextLocation());
for(int i = beginLine; i < std::min(totalLines, endLine); i++)
{
+1 -1
View File
@@ -1686,7 +1686,7 @@ void CCastleInterface::keyPressed(EShortcut key)
case EShortcut::ADVENTURE_OPEN_WIKI:
ENGINE->windows().createAndPushWindow<WikiWindow>(
WikiWindow::Style::BROWN,
WikiEntryKey{WikiCategory::TOWN, town->getTown()->faction->getNameTranslated()});
WikiEntryKey{WikiCategory::TOWN, town->getTown()->faction->getJsonKey()});
break;
default:
break;
+1 -1
View File
@@ -874,7 +874,7 @@ void CStackWindow::keyPressed(EShortcut key)
if(key == EShortcut::ADVENTURE_OPEN_WIKI && info->creature)
ENGINE->windows().createAndPushWindow<WikiWindow>(
WikiWindow::Style::BROWN,
WikiEntryKey{WikiCategory::CREATURE, info->creature->getNameSingularTranslated()});
WikiEntryKey{WikiCategory::CREATURE, info->creature->getJsonKey()});
}
void CStackWindow::initBonusesList()
+1 -1
View File
@@ -192,7 +192,7 @@ void CHeroWindow::keyPressed(EShortcut key)
if(key == EShortcut::ADVENTURE_OPEN_WIKI)
ENGINE->windows().createAndPushWindow<WikiWindow>(
WikiWindow::Style::BROWN,
WikiEntryKey{WikiCategory::HERO, curHero->getNameTranslated()});
WikiEntryKey{WikiCategory::HERO, curHero->getHeroType()->getJsonKey()});
}
void CHeroWindow::updateArtifacts()
+10 -10
View File
@@ -227,24 +227,24 @@ std::vector<std::shared_ptr<CIntObject>> buildCreatureContent(
std::vector<StatRow> stats;
stats.push_back({tr("vcmi.wiki.creature.stat.level"), std::to_string(creature->getLevel())});
stats.push_back({tr("vcmi.wiki.creature.stat.attack"), std::to_string(creature->getBaseAttack())});
stats.push_back({tr("vcmi.wiki.creature.stat.defense"), std::to_string(creature->getBaseDefense())});
stats.push_back({tr("core.genrltxt.190"), std::to_string(creature->getBaseAttack())});
stats.push_back({tr("core.genrltxt.191"), std::to_string(creature->getBaseDefense())});
const int dmgMin = creature->getBaseDamageMin();
const int dmgMax = creature->getBaseDamageMax();
if(dmgMin == dmgMax)
stats.push_back({tr("vcmi.wiki.creature.stat.damage"), std::to_string(dmgMin)});
stats.push_back({tr("core.genrltxt.199"), std::to_string(dmgMin)});
else
stats.push_back({tr("vcmi.wiki.creature.stat.damage"), std::to_string(dmgMin) + " \xe2\x80\x93 " + std::to_string(dmgMax)});
stats.push_back({tr("core.genrltxt.199"), std::to_string(dmgMin) + " - " + std::to_string(dmgMax)});
stats.push_back({tr("vcmi.wiki.creature.stat.speed"), std::to_string(creature->getBaseSpeed())});
stats.push_back({tr("vcmi.wiki.creature.stat.hitpoints"), std::to_string(creature->getBaseHitPoints())});
stats.push_back({tr("vcmi.wiki.creature.stat.growth"), std::to_string(creature->getGrowth())});
stats.push_back({tr("core.genrltxt.193"), std::to_string(creature->getBaseSpeed())});
stats.push_back({tr("core.help.439.help"), std::to_string(creature->getBaseHitPoints())});
stats.push_back({tr("core.genrltxt.194"), std::to_string(creature->getGrowth())});
if(creature->getBaseShots() > 0)
stats.push_back({tr("vcmi.wiki.creature.stat.shots"), std::to_string(creature->getBaseShots())});
if(creature->getBaseSpellPoints() > 0)
stats.push_back({tr("vcmi.wiki.creature.stat.spellpoints"), std::to_string(creature->getBaseSpellPoints())});
stats.push_back({tr("core.genrltxt.387"), std::to_string(creature->getBaseSpellPoints())});
if(creature->getHorde() > 0)
stats.push_back({tr("vcmi.wiki.creature.stat.hordegrowth"), std::to_string(creature->getHorde())});
if(creature->isDoubleWide())
@@ -367,10 +367,10 @@ std::vector<std::shared_ptr<CIntObject>> buildCreatureContent(
rel->getNameSingularTranslated()));
// Clickable overlay: left-click → navigate, right-click → CStackWindow
const std::string relName = rel->getNameSingularTranslated();
const std::string relJsonKey = rel->getJsonKey();
std::function<void()> lclick;
if(navigateCallback)
lclick = [navigateCallback, relName](){ navigateCallback(relName); };
lclick = [navigateCallback, relJsonKey](){ navigateCallback(relJsonKey); };
const CreatureID relId(rel->getIndex());
widgets.push_back(std::make_shared<ClickablePortrait>(
Point(MARGIN, curY), tableW, rowH,
+3 -3
View File
@@ -343,7 +343,7 @@ std::vector<std::shared_ptr<CIntObject>> buildHeroContent(
widgets.push_back(std::make_shared<CLabel>(
MARGIN + iconW + CELL_L, curY + CELL_T,
FONT_TINY, ETextAlignment::TOPLEFT, Colors::YELLOW,
LIBRARY->generaltexth->translate("vcmi.wiki.hero.column.creature")));
LIBRARY->generaltexth->translate("core.genrltxt.42")));
widgets.push_back(std::make_shared<CLabel>(
MARGIN + iconW + nameW + CELL_L, curY + CELL_T,
FONT_TINY, ETextAlignment::TOPLEFT, Colors::YELLOW,
@@ -508,8 +508,8 @@ std::vector<std::shared_ptr<CIntObject>> buildHeroContent(
std::function<void()> lclick;
if(navigateCallback)
{
const std::string spName = sp->getNameTranslated();
lclick = [navigateCallback, spName](){ navigateCallback(WikiCategory::SPELL, spName); };
const std::string spId = sp->getJsonKey();
lclick = [navigateCallback, spId](){ navigateCallback(WikiCategory::SPELL, spId); };
}
const CSpell * spPtr = sp;
widgets.push_back(std::make_shared<HeroWikiClickable>(
+6 -6
View File
@@ -677,10 +677,10 @@ std::vector<std::shared_ptr<CIntObject>> buildTownContent(
std::function<void()> lclick;
if(navigateCallback)
{
const std::string crName = row.creature->getNameSingularTranslated();
lclick = [navigateCallback, crName]()
const std::string crId = row.creature->getJsonKey();
lclick = [navigateCallback, crId]()
{
navigateCallback(WikiCategory::CREATURE, crName);
navigateCallback(WikiCategory::CREATURE, crId);
};
}
const CCreature * crPtr = row.creature;
@@ -798,10 +798,10 @@ curY += 8; // extra padding above section title
std::function<void()> lclick;
if(navigateCallback)
{
const std::string hname = h->getNameTranslated();
lclick = [navigateCallback, hname]()
const std::string heroJsonKey = h->getJsonKey();
lclick = [navigateCallback, heroJsonKey]()
{
navigateCallback(WikiCategory::HERO, hname);
navigateCallback(WikiCategory::HERO, heroJsonKey);
};
}
const HeroTypeID hId = h->getId();
+37 -34
View File
@@ -321,7 +321,7 @@ WikiWindow::WikiWindow(WikiWindow::Style style_, std::optional<WikiEntryKey> ini
{
const std::string name = LIBRARY->generaltexth->translate(e["name"].String());
const std::string desc = LIBRARY->generaltexth->translate(e["description"].String());
categoryEntries[iGlossary].push_back({ name, desc, std::nullopt });
categoryEntries[iGlossary].push_back({ name, name, desc, std::nullopt });
}
}
catch(const std::exception &) {} // file absent → empty glossary
@@ -337,7 +337,7 @@ WikiWindow::WikiWindow(WikiWindow::Style style_, std::optional<WikiEntryKey> ini
for(const auto & faction : LIBRARY->townh->objects)
if(faction && faction->hasTown() && !faction->special)
categoryEntries[iTown].push_back({
faction->getNameTranslated(), "",
faction->getJsonKey(), faction->getNameTranslated(), "",
WikiIconInfo{ AnimationPath::builtin("ITPA"), (size_t)(faction->town->clientInfo.icons[1][0] + 2), 0, std::nullopt }
});
std::sort(categoryEntries[iTown].begin(), categoryEntries[iTown].end(),
@@ -350,7 +350,7 @@ WikiWindow::WikiWindow(WikiWindow::Style style_, std::optional<WikiEntryKey> ini
for(const auto & hero : LIBRARY->heroh->objects)
if(hero && !hero->special)
categoryEntries[iHero].push_back({
hero->getNameTranslated(), "",
hero->getJsonKey(), hero->getNameTranslated(), "",
WikiIconInfo{ AnimationPath::builtin("PortraitsSmall"), (size_t)hero->getIconIndex(), 0, std::nullopt }
});
std::sort(categoryEntries[iHero].begin(), categoryEntries[iHero].end(),
@@ -372,7 +372,7 @@ WikiWindow::WikiWindow(WikiWindow::Style style_, std::optional<WikiEntryKey> ini
const bool isWM = warMachineCreatures.count(CreatureID(creature->getIndex())) > 0;
if(!creature->special || isWM)
categoryEntries[iCreature].push_back({
creature->getNameSingularTranslated(), "",
creature->getJsonKey(), creature->getNameSingularTranslated(), "",
WikiIconInfo{ AnimationPath::builtin("CPRSMALL"), (size_t)creature->getIconIndex(), 0, std::nullopt }
});
}
@@ -386,6 +386,7 @@ WikiWindow::WikiWindow(WikiWindow::Style style_, std::optional<WikiEntryKey> ini
for(const auto & artifact : LIBRARY->arth->objects)
if(artifact && artifact->aClass != EArtifactClass::ART_SPECIAL)
categoryEntries[iArtifact].push_back({
artifact->getJsonKey(),
artifact->getNameTranslated(),
artifact->getDescriptionTranslated(),
WikiIconInfo{ AnimationPath::builtin("Artifact"), (size_t)artifact->getIconIndex(), 0, std::nullopt }
@@ -412,6 +413,7 @@ WikiWindow::WikiWindow(WikiWindow::Style style_, std::optional<WikiEntryKey> ini
}
}
categoryEntries[iSpell].push_back({
spell->getJsonKey(),
spell->getNameTranslated(), desc,
WikiIconInfo{ AnimationPath::builtin("SpellInt"), (size_t)spell->getIndex() + 1, 0, std::nullopt }
});
@@ -438,6 +440,7 @@ WikiWindow::WikiWindow(WikiWindow::Style style_, std::optional<WikiEntryKey> ini
}
}
categoryEntries[iSkill].push_back({
skill->getJsonKey(),
skill->getNameTranslated(), desc,
WikiIconInfo{ AnimationPath::builtin("SECSK32"), (size_t)(skill->getIndex() * 3 + 3), 0, std::nullopt }
});
@@ -469,7 +472,7 @@ WikiWindow::WikiWindow(WikiWindow::Style style_, std::optional<WikiEntryKey> ini
std::string desc;
if(!nativeTowns.empty())
desc = "{" + LIBRARY->generaltexth->translate("vcmi.wiki.terrain.nativeTowns") + "}\n\n" + nativeTowns;
categoryEntries[iTerrain].push_back({ terrain->getNameTranslated(), desc, colorIcon });
categoryEntries[iTerrain].push_back({ terrain->getJsonKey(), terrain->getNameTranslated(), desc, colorIcon });
}
std::sort(categoryEntries[iTerrain].begin(), categoryEntries[iTerrain].end(),
[](const WikiEntry & a, const WikiEntry & b){ return a.name < b.name; });
@@ -495,7 +498,7 @@ WikiWindow::WikiWindow(WikiWindow::Style style_, std::optional<WikiEntryKey> ini
searchBoxHint = std::make_shared<CLabel>(
sbRect.center().x, sbRect.center().y,
FONT_SMALL, ETextAlignment::CENTER, sbHintColor,
LIBRARY->generaltexth->translate("vcmi.wiki.search.hint"));
LIBRARY->generaltexth->translate("vcmi.spellBook.search"));
searchBox = std::make_shared<CTextInput>(
sbRect, FONT_SMALL, ETextAlignment::CENTER, false);
searchBox->setCallback([this](const std::string &) { onSearchInput(); });
@@ -546,10 +549,10 @@ WikiWindow::WikiWindow(WikiWindow::Style style_, std::optional<WikiEntryKey> ini
backButton = std::make_shared<CButton>(
Point(COL1_X, CLOSE_Y),
AnimationPath::builtin(style == Style::BLUE ? "buttonBlue80" : "settingsWindow/button80"),
CButton::tooltip("", LIBRARY->generaltexth->translate("vcmi.wiki.button.back")),
CButton::tooltip("", LIBRARY->generaltexth->translate("core.help.561.hover")),
std::bind(&WikiWindow::navigateBack, this));
backButton->setOverlay(std::make_shared<CLabel>(0, 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW,
LIBRARY->generaltexth->translate("vcmi.wiki.button.back")));
LIBRARY->generaltexth->translate("core.help.561.hover")));
backButton->disable(); // hidden until there is history to go back to
// Apply scroll-wheel bounds after center() so pos is finalised
@@ -768,21 +771,21 @@ void WikiWindow::updateContent()
{
if(useTownViewport)
{
const std::string & townName = currentDisplayedEntries[activeElementIndex].name;
if(townName != currentTownName)
rebuildTownViewport(townName);
const std::string & townIdentifier = currentDisplayedEntries[activeElementIndex].identifier;
if(townIdentifier != currentTownName)
rebuildTownViewport(townIdentifier);
}
else if(useCreatureViewport)
{
const std::string & creName = currentDisplayedEntries[activeElementIndex].name;
if(creName != currentCreatureName)
rebuildCreatureViewport(creName);
const std::string & creatureIdentifier = currentDisplayedEntries[activeElementIndex].identifier;
if(creatureIdentifier != currentCreatureName)
rebuildCreatureViewport(creatureIdentifier);
}
else if(useHeroViewport)
{
const std::string & heroName = currentDisplayedEntries[activeElementIndex].name;
if(heroName != currentHeroName)
rebuildHeroViewport(heroName);
const std::string & heroIdentifier = currentDisplayedEntries[activeElementIndex].identifier;
if(heroIdentifier != currentHeroName)
rebuildHeroViewport(heroIdentifier);
}
redraw();
@@ -823,16 +826,16 @@ void WikiWindow::updateContent()
contentBox->setText(text);
}
void WikiWindow::rebuildTownViewport(const std::string & factionName)
void WikiWindow::rebuildTownViewport(const std::string & factionIdentifier)
{
currentTownName = factionName;
currentTownName = factionIdentifier;
townContentWidgets.clear();
// Look up the faction by translated name
// Look up the faction by JSON key
const CFaction * faction = nullptr;
for(const auto & f : LIBRARY->townh->objects)
{
if(f && f->hasTown() && !f->special && f->getNameTranslated() == factionName)
if(f && f->hasTown() && !f->special && f->getJsonKey() == factionIdentifier)
{
faction = f.get();
break;
@@ -869,16 +872,16 @@ void WikiWindow::rebuildTownViewport(const std::string & factionName)
applyScrollBounds();
}
void WikiWindow::rebuildCreatureViewport(const std::string & creatureName)
void WikiWindow::rebuildCreatureViewport(const std::string & creatureIdentifier)
{
currentCreatureName = creatureName;
currentCreatureName = creatureIdentifier;
creatureContentWidgets.clear();
// Look up the creature by translated name
// Look up the creature by JSON key
const CCreature * creature = nullptr;
for(const auto & c : LIBRARY->creh->objects)
{
if(c && c->getNameSingularTranslated() == creatureName)
if(c && c->getJsonKey() == creatureIdentifier)
{
creature = c.get();
break;
@@ -917,16 +920,16 @@ void WikiWindow::rebuildCreatureViewport(const std::string & creatureName)
ENGINE->windows().totalRedraw();
}
void WikiWindow::rebuildHeroViewport(const std::string & heroName)
void WikiWindow::rebuildHeroViewport(const std::string & heroIdentifier)
{
currentHeroName = heroName;
currentHeroName = heroIdentifier;
heroContentWidgets.clear();
// Look up the hero by translated name
// Look up the hero by JSON key
const CHero * hero = nullptr;
for(const auto & h : LIBRARY->heroh->objects)
{
if(h && h->getNameTranslated() == heroName)
if(h && h->getJsonKey() == heroIdentifier)
{
hero = h.get();
break;
@@ -985,7 +988,7 @@ void WikiWindow::onElementClicked(int index)
&& activeElementIndex < (int)currentDisplayedEntries.size())
{
WikiCategory curCat = static_cast<WikiCategory>(activeCategoryIndex);
navHistory.push_back(WikiEntryKey{curCat, currentDisplayedEntries[activeElementIndex].name});
navHistory.push_back(WikiEntryKey{curCat, currentDisplayedEntries[activeElementIndex].identifier});
}
if(backButton)
backButton->setEnabled(!navHistory.empty());
@@ -1016,7 +1019,7 @@ void WikiWindow::navigateTo(const WikiEntryKey & key)
&& activeElementIndex < (int)currentDisplayedEntries.size())
{
WikiCategory curCat = static_cast<WikiCategory>(activeCategoryIndex);
navHistory.push_back(WikiEntryKey{curCat, currentDisplayedEntries[activeElementIndex].name});
navHistory.push_back(WikiEntryKey{curCat, currentDisplayedEntries[activeElementIndex].identifier});
}
if(backButton)
backButton->setEnabled(!navHistory.empty());
@@ -1033,10 +1036,10 @@ void WikiWindow::navigateTo(const WikiEntryKey & key)
categoryList->scrollTo(activeCategoryIndex);
}
// Find the entry by name and select it
// Find the entry by identifier and select it
for(int i = 0; i < (int)currentDisplayedEntries.size(); ++i)
{
if(currentDisplayedEntries[i].name == key.entryName)
if(currentDisplayedEntries[i].identifier == key.entryName)
{
activeElementIndex = i;
if(elementList)
@@ -1093,7 +1096,7 @@ void WikiWindow::navigateBack()
for(int i = 0; i < (int)currentDisplayedEntries.size(); ++i)
{
if(currentDisplayedEntries[i].name == prev.entryName)
if(currentDisplayedEntries[i].identifier == prev.entryName)
{
activeElementIndex = i;
if(elementList)
+3 -2
View File
@@ -47,9 +47,10 @@ struct WikiIconInfo
std::optional<ColorRGBA> colorFill; ///< drawn as solid square when set (no CAnimImage)
};
/// A single wiki entry – name (translated), optional description and optional icon
/// A single wiki entry – identifier (JSON key), name (translated), optional description and optional icon
struct WikiEntry
{
std::string identifier; ///< unique entity identifier / JSON key (used for lookup)
std::string name; ///< translated display name (shown in list)
std::string description; ///< full description; empty = show auto-stub text
std::optional<WikiIconInfo> icon;
@@ -96,7 +97,7 @@ public:
struct WikiEntryKey
{
WikiCategory category; ///< Which category tab to open
std::string entryName; ///< Translated display name of the entry (used for lookup)
std::string entryName; ///< Entity identifier / JSON key (used for lookup, not a translated string)
};
/// In-game Glossary / Wiki - 800x600 stub window