Skip to content

tip of the day #6841

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 37 commits into from
May 23, 2025
Merged
Changes from 6 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7a82bb0
i18n
exeea Apr 7, 2025
732ff20
corrected initial comment
exeea Apr 7, 2025
77e75b4
ident
exeea Apr 7, 2025
5139ef6
variable name
exeea Apr 7, 2025
7631968
more verbose variable names
exeea Apr 7, 2025
addf18c
enum
exeea Apr 7, 2025
0f9ccdf
dpi scale
exeea Apr 7, 2025
b47171a
size
exeea Apr 7, 2025
c9f3f04
dpi for monitor
exeea Apr 7, 2025
1144da9
constructor
exeea Apr 7, 2025
02e4325
dpi change method
exeea Apr 8, 2025
77776fe
fixed per monitor dpi calculation
exeea Apr 8, 2025
74a0ec8
reverted autoformatting
exeea Apr 8, 2025
daf0da6
scaling
exeea Apr 8, 2025
dafa339
font
exeea Apr 8, 2025
81ca888
resolution based scaling
exeea Apr 8, 2025
162292e
license and updated resolution scale factor
exeea Apr 9, 2025
235dc9e
Merge branch 'master' into tipoftheday
exeea Apr 9, 2025
1f762c0
I18n
exeea Apr 9, 2025
1ab265e
Merge branch 'master' into tipoftheday
exeea Apr 12, 2025
b8466ac
Merge branch 'master' into tipoftheday
exeea Apr 22, 2025
34b7d7c
Merge branch 'master' into tipoftheday
exeea Apr 24, 2025
235bb4e
Merge branch 'MegaMek:master' into tipoftheday
exeea Apr 26, 2025
d4738b9
corrected loop logic
exeea Apr 26, 2025
9d8b57c
Update megamek/src/megamek/common/util/TipOfTheDay.java
exeea Apr 26, 2025
668cc13
Merge branch 'master' into tipoftheday
exeea Apr 28, 2025
213cfe1
added logged warning
exeea Apr 28, 2025
27be84b
Merge branch 'tipoftheday' of https://github.com/exeea/megamek into t…
exeea Apr 28, 2025
ff29264
history.txt
exeea Apr 29, 2025
1512ef8
Merge branch 'master' into tipoftheday
exeea Apr 29, 2025
3ab76a9
Merge branch 'tipoftheday' of https://github.com/exeea/megamek into t…
exeea Apr 29, 2025
b1c6262
new approach for selecting the tip
exeea Apr 29, 2025
c9c38fe
tips in MM
exeea Apr 29, 2025
f164e0c
Merge branch 'master' into tipoftheday
exeea Apr 29, 2025
35d2469
Merge remote-tracking branch 'upstream/master' into tipoftheday
exeea May 19, 2025
8ee110a
de
exeea May 19, 2025
087edef
Merge branch 'master' into tipoftheday
HammerGS May 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 227 additions & 0 deletions megamek/src/megamek/common/util/TipOfTheDay.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/*
* Copyright (c) 2025 - The MegaMek Team. All Rights Reserved.
*
* This file is part of MegaMek.
*
* MegaMek is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* MegaMek is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MegaMek. If not, see <http://www.gnu.org/licenses/>.
*/
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still isn't the correct new copyright notice.

package megamek.common.util;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.text.AttributedString;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import megamek.client.ui.swing.widget.SkinSpecification;
import megamek.client.ui.swing.widget.SkinSpecification.UIComponents;
import megamek.client.ui.swing.widget.SkinXMLHandler;
import megamek.common.internationalization.Internationalization;

/**
* @author Drake
*
* Provides a daily/random tip functionality
*/
public class TipOfTheDay {
// Enum for positioning the tip text
public enum Position {
TOP_BORDER,
BOTTOM_BORDER
}

private static final String TIP_BUNDLE_KEY = "TipOfTheDay.tip.";
private static final String TIP_BUNDLE_TITLE_KEY = "TipOfTheDay.title.text";
private static final int TIP_BORDER_MARGIN = 60;
private static final int TIP_SIDE_PADDING = 20;
private static final float TIP_TITLE_FONT_SIZE = 24f;
private static final float TIP_FONT_SIZE = 32f;
private static final float STROKE_WIDTH = 4.0f;
private static final Color TIP_STROKE_COLOR = Color.BLACK;
private static final Color TIP_TITLE_FONT_COLOR = Color.WHITE;
private static final Color TIP_FONT_COLOR = Color.WHITE;
private final String bundleName;
private final int countTips;
private final String tipOfTheDay;
private final String tipLabel;
private Font tipFont;
private Font tipLabelFont;

// Prevent instantiation
public TipOfTheDay(String bundleName) {
this.bundleName = bundleName;
countTips = countTips();
tipLabel = Internationalization.getTextAt(bundleName, TIP_BUNDLE_TITLE_KEY);
tipOfTheDay = getRandomTip();

SkinSpecification skinSpec = SkinXMLHandler.getSkin(UIComponents.MainMenuBorder.getComp(), true);
Font baseFont = new Font(skinSpec.fontName, Font.PLAIN, skinSpec.fontSize);
tipLabelFont = baseFont.deriveFont(Font.BOLD, TIP_TITLE_FONT_SIZE); // Tip title font
tipFont = baseFont.deriveFont(Font.BOLD, TIP_FONT_SIZE); // Tip font
}

/**
* Count the number of tips in the resource bundle
*/
private int countTips() {
int count = 0;
try {
while (true) {
count++;
String tip = Internationalization.getTextAt(bundleName, TIP_BUNDLE_KEY + count);
if (tip.startsWith("!") && tip.endsWith("!")) {
return count - 1;
}
}
} catch (Exception e) {
// When we get an exception, we've found all tips
return count - 1;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has to be a better way to do this to get the number of tips.

Copy link
Collaborator Author

@exeea exeea Apr 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see many other options:
a) or we store, in the file, also the number of tips in a dedicated key=value pair (making the i18n file store also numeric constants, that has to be kept updated while adding/removing tips)
b) or we ditch the key=value file format, and we use a dedicated file for specifically only the tips, one per line, and we use the lines count to know how many tips we have

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you get the ResourceBundle you're using, you would be able to get the list of its keys. From there you could filter it to keys that contain TipOfTheDay.tip (maybe change it to be longer/more specific so there's no false positives?). This would also give the list of tips, and wouldn't require them to be numbered sequentially. Something like:

ResourceBundle bundle = I18n.getInstance().getResourceBundle(bundleName);
Enumeration<String> keys = bundle.getKeys();

?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking making a resource file JUST for the tips for easier translation and just get the keys that way.

The ResourceBundle will handle I18N for us anyways and I plan to update the I18N class itself to be more dynamic in nature with ability to have defaults for the class as well as add in additional bundles as needed. A way to keep the resource files smaller and easier to manage for translations.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok then, let's do a dedicated file without Key=Value pairs structure

}

/**
* Gets the tip for today based on the current date
*
* @return A tip string for today
*/
public String getTodaysTip() {
LocalDate today = LocalDate.now();
int dayOfYear = today.getDayOfYear();
int tipIndex = (dayOfYear % countTips) + 1;
return Internationalization.getTextAt(bundleName, TIP_BUNDLE_KEY + tipIndex);
}

/**
* Gets a random tip from the list
*
* @return A random tip string
*/
public String getRandomTip() {
int randomIndex = (int) (Math.random() * countTips) + 1;
return Internationalization.getTextAt(bundleName, TIP_BUNDLE_KEY + randomIndex);
}

/**
* Draws the Tip of the Day text with word wrap and styling.
*/
public void drawTipOfTheDay(Graphics2D graphics2D, Rectangle referenceBounds, Position position) {
if (tipOfTheDay == null || tipOfTheDay.isEmpty() || tipLabelFont == null || tipFont == null) {
return;
}
if (referenceBounds == null || referenceBounds.width <= 0 || referenceBounds.height <= 0) {
return; // Cannot draw if referenceBounds is invalid
}
float availableWidth = referenceBounds.width - (TIP_SIDE_PADDING * 2);
if (availableWidth <= 0)
return; // Not enough space to draw

Graphics2D tipGraphics = (Graphics2D) graphics2D.create();

try {
tipGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
tipGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
tipGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
tipGraphics.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);

FontRenderContext frc = tipGraphics.getFontRenderContext();

// "Tip of the Day:" label
AttributedString labelAS = new AttributedString(tipLabel);
labelAS.addAttribute(TextAttribute.FONT, tipLabelFont);
TextLayout labelLayout = new TextLayout(labelAS.getIterator(), frc);
float labelHeight = labelLayout.getAscent() + labelLayout.getDescent() + labelLayout.getLeading();
float labelWidth = (float) labelLayout.getBounds().getWidth();

// Actual tip text with word wrapping
AttributedString tipAS = new AttributedString(tipOfTheDay);
tipAS.addAttribute(TextAttribute.FONT, tipFont);
LineBreakMeasurer measurer = new LineBreakMeasurer(tipAS.getIterator(), frc);
List<TextLayout> tipLayouts = new ArrayList<>();
float totalTipHeight = 0;
measurer.setPosition(0);
while (measurer.getPosition() < tipAS.getIterator().getEndIndex()) {
TextLayout layout = measurer.nextLayout(availableWidth);
if (layout != null) {
tipLayouts.add(layout);
totalTipHeight += layout.getAscent() + layout.getDescent() + layout.getLeading();
} else {
break; // Should not happen with LineBreakMeasurer unless width is tiny
}
if (measurer.getPosition() == layout.getCharacterCount() + measurer.getPosition()
&& measurer.getPosition() < tipAS.getIterator().getEndIndex()) {
break;
}
}

// Positioning// Positioning
float totalBlockHeight = labelHeight + totalTipHeight;
float startY;
if (position == Position.BOTTOM_BORDER) {
startY = referenceBounds.y + referenceBounds.height - TIP_BORDER_MARGIN - totalBlockHeight;
} else {
startY = referenceBounds.y + TIP_BORDER_MARGIN;
}
float startX = referenceBounds.x + TIP_SIDE_PADDING;

// Draw the text (outline then fill)
BasicStroke outlineStroke = new BasicStroke(STROKE_WIDTH, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
tipGraphics.setStroke(outlineStroke);

// Draw Label
float labelDrawX = startX + (availableWidth - labelWidth) / 2; // Center label
float labelDrawY = startY + labelLayout.getAscent();
Shape labelShape = labelLayout.getOutline(null);

tipGraphics.translate(labelDrawX, labelDrawY);
tipGraphics.setColor(TIP_STROKE_COLOR);
tipGraphics.draw(labelShape); // Draw outline
tipGraphics.setColor(TIP_TITLE_FONT_COLOR); // Fill color
tipGraphics.fill(labelShape); // Draw fill
tipGraphics.translate(-labelDrawX, -labelDrawY); // Translate back

// Draw Tip Lines
float currentY = startY + labelHeight; // Start drawing tips below the label
for (TextLayout tipLayout : tipLayouts) {
float lineAscent = tipLayout.getAscent();
float lineHeight = lineAscent + tipLayout.getDescent() + tipLayout.getLeading();
float lineDrawY = currentY + lineAscent; // Baseline for this line
float lineWidth = (float) tipLayout.getBounds().getWidth();

float lineDrawX = startX + (availableWidth - lineWidth) / 2f; // Center line
lineDrawX = Math.max(startX, lineDrawX); // Ensure it doesn't go out of bounds
Shape tipShape = tipLayout.getOutline(AffineTransform.getTranslateInstance(lineDrawX, lineDrawY));
tipGraphics.setColor(TIP_STROKE_COLOR); // Outline color
tipGraphics.draw(tipShape); // Draw outline
tipGraphics.setColor(TIP_FONT_COLOR); // Fill color
tipGraphics.fill(tipShape); // Draw fill

currentY += lineHeight;
}

} finally {
tipGraphics.dispose();
}
}

}