diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/clock/ClockPatternPreviewRenderer.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/clock/ClockPatternPreviewRenderer.java new file mode 100644 index 0000000..a6cad8d --- /dev/null +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/clock/ClockPatternPreviewRenderer.java @@ -0,0 +1,45 @@ +package se.ajpanton.statusbartweak.runtime.features.clock; + +import android.content.Context; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; + +import java.util.ArrayList; + +public final class ClockPatternPreviewRenderer { + public static final int ROW_GAP_AUTO = ClockPatternParser.ROW_GAP_AUTO; + private static final ClockPatternParser PATTERN_PARSER = new ClockPatternParser(); + private static final ClockTextRenderer TEXT_RENDERER = new ClockTextRenderer(); + + private ClockPatternPreviewRenderer() { + } + + public static Result render(Context context, String pattern, int referenceColor) { + if (context == null || TextUtils.isEmpty(pattern)) { + return new Result(new ArrayList<>(), "", false); + } + ClockParsedPattern parsedPattern = PATTERN_PARSER.parse(pattern); + ArrayList renderedRows = + TEXT_RENDERER.renderLayoutRows(context, parsedPattern, referenceColor); + ArrayList rows = new ArrayList<>(); + StringBuilder description = new StringBuilder(); + for (int i = 0; i < renderedRows.size(); i++) { + ClockTextRenderer.LayoutRowResult row = renderedRows.get(i); + if (i > 0) { + description.append('\n'); + } + description.append(row.contentDescription); + rows.add(new Row(row.text, row.leftText, row.rightText, row.pipeCount, row.gapBeforePx)); + } + if (rows.isEmpty()) { + return new Result(new ArrayList<>(), "", parsedPattern.containsSeconds); + } + return new Result(rows, description.toString(), parsedPattern.containsSeconds); + } + + public record Result(ArrayList rows, String contentDescription, boolean containsSeconds) { + } + + public record Row(CharSequence text, CharSequence leftText, CharSequence rightText, int pipeCount, int gapBeforePx) { + } +} diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/clock/DrawerClockTextController.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/clock/DrawerClockTextController.java index b234fb9..4472a27 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/clock/DrawerClockTextController.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/clock/DrawerClockTextController.java @@ -28,6 +28,7 @@ import se.ajpanton.statusbartweak.runtime.mode.SceneKey; import se.ajpanton.statusbartweak.runtime.mode.SurfaceMode; import se.ajpanton.statusbartweak.runtime.settings.BroadcastReceiverSupport; import se.ajpanton.statusbartweak.runtime.settings.RuntimeSettingsCache; +import se.ajpanton.statusbartweak.R; import se.ajpanton.statusbartweak.shell.settings.SbtSettings; final class DrawerClockTextController { @@ -204,6 +205,7 @@ final class DrawerClockTextController { return; } Role previous = trackedRoles.put(textView, role); + textView.setTag(R.id.sbt_debug_drawer_clock_container, role.name()); if (previous == null || previous != role || !stockSnapshots.containsKey(textView)) { captureSnapshot(textView); } @@ -214,6 +216,9 @@ final class DrawerClockTextController { stockSnapshots.remove(textView); renderCache.remove(textView); ownedTexts.remove(textView); + if (textView != null) { + textView.setTag(R.id.sbt_debug_drawer_clock_container, null); + } stopTicker(textView); } diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/DebugBoundsOverlayController.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/DebugBoundsOverlayController.java index df0b3d9..2fce871 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/DebugBoundsOverlayController.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/DebugBoundsOverlayController.java @@ -94,6 +94,12 @@ public final class DebugBoundsOverlayController { OwnedIconHostManager.TAG_UNLOCKED_CLOCK_HOST, COLOR_CLOCK, specs); + addTaggedDescendantSpecs( + root, + parent, + R.id.sbt_debug_drawer_clock_container, + COLOR_CLOCK, + specs); } if (layoutEnabled && settings.debugVisualizeChipContainer) { addHostContainerSpecs( @@ -148,6 +154,9 @@ public final class DebugBoundsOverlayController { result = 31 * result + hostSignature(root, OwnedIconHostManager.TAG_UNLOCKED_NOTIFICATION_HOST); result = 31 * result + hostSignature(root, OwnedIconHostManager.TAG_UNLOCKED_STATUS_HOST); result = 31 * result + hostSignature(root, OwnedIconHostManager.TAG_UNLOCKED_CLOCK_HOST); + if (settings.debugVisualizeClockContainer) { + result = 31 * result + taggedDescendantSignature(root, root, R.id.sbt_debug_drawer_clock_container); + } result = 31 * result + hostSignature(root, OwnedIconHostManager.TAG_UNLOCKED_CHIP_HOST); return result; } @@ -403,6 +412,30 @@ public final class DebugBoundsOverlayController { } } + private void addTaggedDescendantSpecs( + View root, + View view, + int tagKey, + int color, + ArrayList specs + ) { + if (root == null || view == null || specs == null) { + return; + } + if (view.getTag(tagKey) != null && view.getVisibility() != View.GONE) { + Bounds bounds = boundsRelativeTo(root, view); + if (bounds != null && bounds.width > 0 && bounds.height > 0) { + specs.add(new OverlaySpec(bounds, color)); + } + } + if (!(view instanceof ViewGroup group)) { + return; + } + for (int i = 0; i < group.getChildCount(); i++) { + addTaggedDescendantSpecs(root, group.getChildAt(i), tagKey, color, specs); + } + } + private OverlayView ensureOverlay(ViewGroup parent) { View existing = findDirectTaggedChild(parent, TAG_OVERLAY_HOST); if (existing instanceof OverlayView overlayView) { @@ -493,6 +526,24 @@ public final class DebugBoundsOverlayController { return result; } + private int taggedDescendantSignature(View root, View view, int tagKey) { + if (root == null || view == null) { + return 0; + } + int result = 17; + Object tag = view.getTag(tagKey); + if (tag != null && view.getVisibility() != View.GONE) { + result = 31 * result + signature(boundsRelativeTo(root, view)); + result = 31 * result + tag.hashCode(); + } + if (view instanceof ViewGroup group) { + for (int i = 0; i < group.getChildCount(); i++) { + result = 31 * result + taggedDescendantSignature(root, group.getChildAt(i), tagKey); + } + } + return result; + } + private Bounds boundsRelativeTo(View root, View view) { if (root == null || view == null) { return null; diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/ClockFragment.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/ClockFragment.java index d7532e5..7bf1d17 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/ClockFragment.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/ClockFragment.java @@ -1,6 +1,7 @@ package se.ajpanton.statusbartweak.shell.ui; import se.ajpanton.statusbartweak.R; +import se.ajpanton.statusbartweak.runtime.features.clock.ClockPatternPreviewRenderer; import se.ajpanton.statusbartweak.shell.settings.SbtDefaults; import se.ajpanton.statusbartweak.shell.settings.SbtSettings; @@ -42,6 +43,19 @@ import java.util.List; import java.util.Locale; public final class ClockFragment extends Fragment { + private final ArrayList previewBindings = new ArrayList<>(); + private final Runnable previewTicker = new Runnable() { + @Override + public void run() { + boolean needsNextTick = false; + for (PreviewBinding binding : previewBindings) { + needsNextTick |= updatePreview(binding); + } + if (needsNextTick && getView() != null) { + getView().postDelayed(this, nextSecondDelayMillis()); + } + } + }; @Nullable @Override @@ -55,18 +69,24 @@ public final class ClockFragment extends Fragment { CheckBox customFormatEnabled = root.findViewById(R.id.clock_custom_format_enabled); View customFormatContainer = root.findViewById(R.id.clock_custom_format_container); EditText customFormatInput = root.findViewById(R.id.clock_custom_format_input); + ClockPatternPreviewView customFormatPreview = + root.findViewById(R.id.clock_custom_format_preview); CheckBox drawerClockCustomFormatEnabled = root.findViewById(R.id.drawer_clock_custom_format_enabled); View drawerClockCustomFormatContainer = root.findViewById(R.id.drawer_clock_custom_format_container); EditText drawerClockCustomFormatInput = root.findViewById(R.id.drawer_clock_custom_format_input); + ClockPatternPreviewView drawerClockCustomFormatPreview = + root.findViewById(R.id.drawer_clock_custom_format_preview); CheckBox drawerDateCustomFormatEnabled = root.findViewById(R.id.drawer_date_custom_format_enabled); View drawerDateCustomFormatContainer = root.findViewById(R.id.drawer_date_custom_format_container); EditText drawerDateCustomFormatInput = root.findViewById(R.id.drawer_date_custom_format_input); + ClockPatternPreviewView drawerDateCustomFormatPreview = + root.findViewById(R.id.drawer_date_custom_format_preview); MaterialButton fontPickerButton = root.findViewById(R.id.clock_font_picker_button); TextView customFormatHelpToggle = root.findViewById(R.id.clock_custom_format_help_toggle); View customFormatHelpContainer = root.findViewById(R.id.clock_custom_format_help_container); @@ -94,27 +114,40 @@ public final class ClockFragment extends Fragment { SbtSettings.KEY_DRAWER_DATE_CUSTOM_FORMAT, SbtDefaults.DRAWER_DATE_CUSTOM_FORMAT_DEFAULT); - customFormatEnabled.setChecked(customFormatEnabledValue); - customFormatInput.setText(customFormatValue); - updateCustomFormatVisibility(customFormatContainer, customFormatEnabledValue); + previewBindings.clear(); + bindCustomFormat( + prefs, + customFormatEnabled, + customFormatContainer, + customFormatInput, + customFormatPreview, + SbtSettings.KEY_CLOCK_CUSTOM_FORMAT_ENABLED, + SbtSettings.KEY_CLOCK_CUSTOM_FORMAT, + customFormatEnabledValue, + customFormatValue, + 18f); bindCustomFormat( prefs, drawerClockCustomFormatEnabled, drawerClockCustomFormatContainer, drawerClockCustomFormatInput, + drawerClockCustomFormatPreview, SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED, SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT, drawerClockCustomFormatEnabledValue, - drawerClockCustomFormatValue); + drawerClockCustomFormatValue, + 24f); bindCustomFormat( prefs, drawerDateCustomFormatEnabled, drawerDateCustomFormatContainer, drawerDateCustomFormatInput, + drawerDateCustomFormatPreview, SbtSettings.KEY_DRAWER_DATE_CUSTOM_FORMAT_ENABLED, SbtSettings.KEY_DRAWER_DATE_CUSTOM_FORMAT, drawerDateCustomFormatEnabledValue, - drawerDateCustomFormatValue); + drawerDateCustomFormatValue, + 16f); updateCustomFormatHelpVisibility(customFormatHelpContainer, customFormatHelpToggle, false); applyBulletText(customFormatHelpCommon); applyBulletText(customFormatHelpStyling); @@ -123,12 +156,6 @@ public final class ClockFragment extends Fragment { customFormatHelpLink.setMovementMethod(LinkMovementMethod.getInstance()); } - customFormatEnabled.setOnCheckedChangeListener((buttonView, isChecked) -> { - updateCustomFormatVisibility(customFormatContainer, isChecked); - prefs.edit().putBoolean(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT_ENABLED, isChecked).apply(); - SbtSettings.ensureReadable(requireContext()); - }); - if (customFormatHelpToggle != null) { customFormatHelpToggle.setOnClickListener(v -> updateCustomFormatHelpVisibility( customFormatHelpContainer, @@ -136,24 +163,6 @@ public final class ClockFragment extends Fragment { customFormatHelpContainer == null || customFormatHelpContainer.getVisibility() != View.VISIBLE)); } - customFormatInput.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - - @Override - public void afterTextChanged(Editable s) { - prefs.edit() - .putString(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT, s != null ? s.toString() : "") - .apply(); - SbtSettings.ensureReadable(requireContext()); - } - }); - if (fontPickerButton != null) { fontPickerButton.setOnClickListener(v -> showFontPickerDialog()); } @@ -172,20 +181,32 @@ public final class ClockFragment extends Fragment { CheckBox enabledView, View container, EditText input, + ClockPatternPreviewView preview, String enabledKey, String formatKey, boolean enabled, - String format) { + String format, + float previewTextSizeSp) { if (enabledView == null || input == null) { return; } enabledView.setChecked(enabled); input.setText(format != null ? format : ""); + if (preview != null) { + preview.setPreviewTextAppearance( + requireContext().getColor(R.color.sbt_on_surface), + previewTextSizeSp); + } updateCustomFormatVisibility(container, enabled); + PreviewBinding binding = new PreviewBinding(enabledView, input, preview); + previewBindings.add(binding); + updatePreview(binding); enabledView.setOnCheckedChangeListener((buttonView, isChecked) -> { updateCustomFormatVisibility(container, isChecked); prefs.edit().putBoolean(enabledKey, isChecked).apply(); SbtSettings.ensureReadable(requireContext()); + updatePreview(binding); + schedulePreviewTickerIfNeeded(); }); input.addTextChangedListener(new TextWatcher() { @Override @@ -202,10 +223,81 @@ public final class ClockFragment extends Fragment { .putString(formatKey, s != null ? s.toString() : "") .apply(); SbtSettings.ensureReadable(requireContext()); + updatePreview(binding); + schedulePreviewTickerIfNeeded(); } }); } + private boolean updatePreview(PreviewBinding binding) { + if (binding == null || binding.preview == null) { + return false; + } + boolean enabled = binding.enabled != null && binding.enabled.isChecked(); + String pattern = binding.input != null && binding.input.getText() != null + ? binding.input.getText().toString() + : ""; + if (!enabled || pattern.isEmpty()) { + binding.preview.setVisibility(View.GONE); + binding.preview.setPreview(null); + binding.preview.setContentDescription(""); + return false; + } + ClockPatternPreviewRenderer.Result result = ClockPatternPreviewRenderer.render( + binding.preview.getContext(), + pattern, + binding.preview.getPreviewTextColor()); + binding.preview.setPreview(result); + binding.preview.setVisibility(result.rows().isEmpty() ? View.GONE : View.VISIBLE); + binding.preview.setContentDescription(result.contentDescription()); + return result.containsSeconds(); + } + + private void schedulePreviewTickerIfNeeded() { + View view = getView(); + if (view == null) { + return; + } + view.removeCallbacks(previewTicker); + for (PreviewBinding binding : previewBindings) { + if (updatePreview(binding)) { + view.postDelayed(previewTicker, nextSecondDelayMillis()); + return; + } + } + } + + private long nextSecondDelayMillis() { + long now = System.currentTimeMillis(); + long next = now - (now % 1000L) + 1000L; + return Math.max(1L, next - now); + } + + @Override + public void onResume() { + super.onResume(); + schedulePreviewTickerIfNeeded(); + } + + @Override + public void onPause() { + View view = getView(); + if (view != null) { + view.removeCallbacks(previewTicker); + } + super.onPause(); + } + + @Override + public void onDestroyView() { + View view = getView(); + if (view != null) { + view.removeCallbacks(previewTicker); + } + previewBindings.clear(); + super.onDestroyView(); + } + private void updateCustomFormatHelpVisibility(View helpContainer, TextView helpToggle, boolean visible) { @@ -354,6 +446,9 @@ public final class ClockFragment extends Fragment { view.setText(builder); } + private record PreviewBinding(CheckBox enabled, EditText input, ClockPatternPreviewView preview) { + } + private static final class FontFamilyAdapter extends BaseAdapter { private static final String SAMPLE_PATTERN = "EEEE HH:mm:ss"; diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/ClockPatternPreviewView.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/ClockPatternPreviewView.java new file mode 100644 index 0000000..8323cd1 --- /dev/null +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/ClockPatternPreviewView.java @@ -0,0 +1,228 @@ +package se.ajpanton.statusbartweak.shell.ui; + +import android.content.Context; +import android.graphics.Canvas; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; + +import java.util.ArrayList; + +import se.ajpanton.statusbartweak.runtime.features.clock.ClockPatternPreviewRenderer; +import se.ajpanton.statusbartweak.shell.settings.SbtDefaults; + +public final class ClockPatternPreviewView extends View { + private final TextPaint textPaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); + private final ArrayList measuredRows = new ArrayList<>(); + private ClockPatternPreviewRenderer.Result result; + private int textColor; + private float textSizePx; + private int contentWidth; + private int contentHeight; + + public ClockPatternPreviewView(Context context) { + super(context); + } + + public ClockPatternPreviewView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setPreviewTextAppearance(int color, float sizeSp) { + textColor = color; + textSizePx = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, + sizeSp, + getResources().getDisplayMetrics()); + textPaint.setColor(textColor); + textPaint.setTextSize(textSizePx); + rebuildMeasurements(); + } + + public int getPreviewTextColor() { + return textColor; + } + + public void setPreview(ClockPatternPreviewRenderer.Result nextResult) { + result = nextResult; + rebuildMeasurements(); + } + + private void rebuildMeasurements() { + measuredRows.clear(); + contentWidth = 0; + contentHeight = 0; + if (result == null || result.rows() == null || result.rows().isEmpty()) { + requestLayout(); + invalidate(); + return; + } + int leftColumnWidth = 0; + int rightColumnWidth = 0; + int baseRowHeight = measureText("0").height; + for (ClockPatternPreviewRenderer.Row row : result.rows()) { + MeasuredRow measured = measureRow(row); + measuredRows.add(measured); + if (measured.piped) { + leftColumnWidth = Math.max(leftColumnWidth, measured.left.width); + rightColumnWidth = Math.max(rightColumnWidth, measured.right.width); + } else { + contentWidth = Math.max(contentWidth, measured.full.width); + } + } + if (leftColumnWidth > 0 || rightColumnWidth > 0) { + contentWidth = Math.max(contentWidth, leftColumnWidth + rightColumnWidth); + for (MeasuredRow measured : measuredRows) { + measured.leftColumnWidth = leftColumnWidth; + } + } + positionRows(Math.max(1, baseRowHeight)); + requestLayout(); + invalidate(); + } + + private MeasuredRow measureRow(ClockPatternPreviewRenderer.Row row) { + if (row != null && row.pipeCount() > 0) { + MeasuredText left = measureText(row.leftText()); + MeasuredText right = measureText(row.rightText()); + return MeasuredRow.piped(left, right, row.gapBeforePx()); + } + return MeasuredRow.full(measureText(row != null ? row.text() : ""), row != null ? row.gapBeforePx() : 0); + } + + private MeasuredText measureText(CharSequence text) { + CharSequence safeText = text != null ? text : ""; + if (TextUtils.isEmpty(safeText)) { + safeText = " "; + } + int width = Math.max(1, (int) Math.ceil(Layout.getDesiredWidth(safeText, textPaint))); + StaticLayout layout = StaticLayout.Builder + .obtain(safeText, 0, safeText.length(), textPaint, width) + .setAlignment(Layout.Alignment.ALIGN_NORMAL) + .setIncludePad(false) + .setLineSpacing(0f, 1f) + .build(); + if (layout.getLineCount() <= 0) { + TextPaint.FontMetricsInt metrics = textPaint.getFontMetricsInt(); + int ascent = -metrics.ascent; + int descent = metrics.descent; + return new MeasuredText(layout, width, Math.max(1, ascent + descent), ascent, descent); + } + int baseline = layout.getLineBaseline(0); + int ascent = Math.max(0, baseline - layout.getLineTop(0)); + int descent = Math.max(0, layout.getLineBottom(0) - baseline); + return new MeasuredText(layout, width, Math.max(1, ascent + descent), ascent, descent); + } + + private void positionRows(int baseRowHeight) { + if (measuredRows.isEmpty()) { + return; + } + int minTop = Integer.MAX_VALUE; + int maxBottom = Integer.MIN_VALUE; + int bottom = 0; + for (int i = 0; i < measuredRows.size(); i++) { + MeasuredRow row = measuredRows.get(i); + if (i == 0) { + bottom = row.height; + } else { + bottom += resolveGapPx(row.gapBeforePx, baseRowHeight); + } + int top = bottom - row.height; + row.drawTop = top; + minTop = Math.min(minTop, top); + maxBottom = Math.max(maxBottom, bottom); + } + for (MeasuredRow row : measuredRows) { + row.drawTop -= minTop; + } + contentHeight = Math.max(1, maxBottom - minTop); + } + + private int resolveGapPx(int gapBefore, int baseRowHeight) { + if (gapBefore == ClockPatternPreviewRenderer.ROW_GAP_AUTO) { + return Math.max(1, baseRowHeight); + } + if (gapBefore == 0) { + return 0; + } + int defaultSteps = Math.max(1, SbtDefaults.CLOCK_TEXT_SIZE_STEPS_DEFAULT); + return Math.max(1, Math.round(gapBefore * Math.max(1, baseRowHeight) / (float) defaultSteps)); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int desiredWidth = getPaddingLeft() + contentWidth + getPaddingRight(); + int desiredHeight = getPaddingTop() + contentHeight + getPaddingBottom(); + setMeasuredDimension( + resolveSize(desiredWidth, widthMeasureSpec), + resolveSize(desiredHeight, heightMeasureSpec)); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + for (MeasuredRow row : measuredRows) { + int y = getPaddingTop() + row.drawTop; + if (row.piped) { + drawText(canvas, row.left, getPaddingLeft() + row.leftColumnWidth - row.left.width, y + row.ascent - row.left.ascent); + drawText(canvas, row.right, getPaddingLeft() + row.leftColumnWidth, y + row.ascent - row.right.ascent); + } else { + drawText(canvas, row.full, getPaddingLeft(), y); + } + } + } + + private void drawText(Canvas canvas, MeasuredText text, int left, int top) { + if (text == null || text.layout == null) { + return; + } + int save = canvas.save(); + canvas.translate(left, top); + text.layout.draw(canvas); + canvas.restoreToCount(save); + } + + private static final class MeasuredRow { + final boolean piped; + final MeasuredText full; + final MeasuredText left; + final MeasuredText right; + final int height; + final int ascent; + final int descent; + final int gapBeforePx; + int leftColumnWidth; + int drawTop; + + private MeasuredRow(boolean piped, MeasuredText full, MeasuredText left, MeasuredText right, int gapBeforePx) { + this.piped = piped; + this.full = full; + this.left = left; + this.right = right; + this.gapBeforePx = gapBeforePx; + this.ascent = piped + ? Math.max(left != null ? left.ascent : 0, right != null ? right.ascent : 0) + : full != null ? full.ascent : 0; + this.descent = piped + ? Math.max(left != null ? left.descent : 0, right != null ? right.descent : 0) + : full != null ? full.descent : 0; + this.height = Math.max(1, ascent + descent); + } + + static MeasuredRow full(MeasuredText text, int gapBeforePx) { + return new MeasuredRow(false, text, null, null, gapBeforePx); + } + + static MeasuredRow piped(MeasuredText left, MeasuredText right, int gapBeforePx) { + return new MeasuredRow(true, null, left, right, gapBeforePx); + } + } + + private record MeasuredText(StaticLayout layout, int width, int height, int ascent, int descent) { + } +} diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/MainActivity.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/MainActivity.java index 0d9c70d..111a77b 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/MainActivity.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/MainActivity.java @@ -6,6 +6,8 @@ import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; @@ -24,6 +26,7 @@ import se.ajpanton.statusbartweak.R; public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener { private static final String EXTRA_OPEN_NAV_ITEM = "open_nav_item"; + private static final String STATE_CURRENT_ITEM_ID = "current_item_id"; private static final float PERMANENT_SIDEBAR_FOLDED_WIDTH_MULTIPLIER = 1.1f; private SafeInsetDrawerLayout drawerLayout; @@ -34,6 +37,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On private Integer drawerNavigationBasePaddingLeft; private Integer permanentNavigationBasePaddingLeft; private boolean permanentSidebar; + private int currentItemId = R.id.nav_layout; public static Intent newOpenDebugIntent(Context context) { Intent intent = new Intent(context, MainActivity.class); @@ -56,7 +60,6 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On setupNavigationView(drawerNavigationView); setupNavigationView(permanentNavigationView); applyNavigationDrawerInsets(); - drawerToggle = new ActionBarDrawerToggle( this, drawerLayout, @@ -84,15 +87,27 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On } }); + if (savedInstanceState != null) { + currentItemId = savedInstanceState.getInt(STATE_CURRENT_ITEM_ID, currentItemId); + } if (savedInstanceState == null) { int initialItemId = resolveInitialItemId(getIntent()); navigateTo(initialItemId); consumeOpenNavItemExtra(getIntent()); } else { + syncDrawerSelection(currentItemId); + setTitle(resolveTitle(currentItemId)); + drawerLayout.post(this::applyPageTitleVisibility); handleOpenNavIntent(getIntent()); } } + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + outState.putInt(STATE_CURRENT_ITEM_ID, currentItemId); + super.onSaveInstanceState(outState); + } + @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); @@ -110,6 +125,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On } private void navigateTo(int itemId) { + currentItemId = itemId; syncDrawerSelection(itemId); Fragment fragment; if (itemId == R.id.nav_system_icons) { @@ -138,6 +154,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On getSupportFragmentManager() .beginTransaction() .replace(R.id.content_frame, fragment) + .runOnCommit(this::applyPageTitleVisibility) .commit(); setTitle(resolveTitle(itemId)); } @@ -296,6 +313,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On permanentSidebar = shouldUsePermanentSidebar; drawerToggle.setDrawerIndicatorEnabled(!permanentSidebar); toolbar.setVisibility(permanentSidebar ? View.GONE : View.VISIBLE); + setTitle(resolveTitle(currentItemId)); if (permanentSidebar) { permanentNavigationView.setVisibility(View.VISIBLE); drawerLayout.closeDrawer(GravityCompat.START, false); @@ -309,6 +327,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On } updateNavigationHeaderHeight(drawerNavigationView); updateNavigationHeaderHeight(permanentNavigationView); + applyPageTitleVisibility(); } private void updateNavigationHeaderHeight(NavigationView navigationView) { @@ -348,4 +367,26 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On } return count; } + + private void applyPageTitleVisibility() { + View content = findViewById(R.id.content_frame); + setTaggedPageTitlesVisible(content, permanentSidebar); + } + + private void setTaggedPageTitlesVisible(View view, boolean visible) { + if (view == null) { + return; + } + Object tag = view.getTag(); + if (tag instanceof String string && "statusbartweak.page.title".equals(string) + && view instanceof TextView) { + view.setVisibility(visible ? View.VISIBLE : View.GONE); + } + if (!(view instanceof ViewGroup group)) { + return; + } + for (int i = 0; i < group.getChildCount(); i++) { + setTaggedPageTitlesVisible(group.getChildAt(i), visible); + } + } } diff --git a/app/src/main/res/drawable/sbt_clock_input_background.xml b/app/src/main/res/drawable/sbt_clock_input_background.xml new file mode 100644 index 0000000..f943c91 --- /dev/null +++ b/app/src/main/res/drawable/sbt_clock_input_background.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/app/src/main/res/drawable/sbt_clock_preview_background.xml b/app/src/main/res/drawable/sbt_clock_preview_background.xml new file mode 100644 index 0000000..209bcab --- /dev/null +++ b/app/src/main/res/drawable/sbt_clock_preview_background.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/app/src/main/res/layout/fragment_battery_bar.xml b/app/src/main/res/layout/fragment_battery_bar.xml index c86d110..897b8ae 100644 --- a/app/src/main/res/layout/fragment_battery_bar.xml +++ b/app/src/main/res/layout/fragment_battery_bar.xml @@ -13,6 +13,7 @@ diff --git a/app/src/main/res/layout/fragment_block_app_notification_icons.xml b/app/src/main/res/layout/fragment_block_app_notification_icons.xml index ac6951a..4c0516b 100644 --- a/app/src/main/res/layout/fragment_block_app_notification_icons.xml +++ b/app/src/main/res/layout/fragment_block_app_notification_icons.xml @@ -9,6 +9,7 @@ android:id="@+id/hidden_apps_title" android:layout_width="match_parent" android:layout_height="wrap_content" + android:tag="statusbartweak.page.title" android:text="@string/hidden_apps_page_title" android:textAppearance="?attr/textAppearanceHeadline6" android:textStyle="bold" /> diff --git a/app/src/main/res/layout/fragment_clock.xml b/app/src/main/res/layout/fragment_clock.xml index e1b5204..25fd0f7 100644 --- a/app/src/main/res/layout/fragment_clock.xml +++ b/app/src/main/res/layout/fragment_clock.xml @@ -13,6 +13,7 @@ @@ -53,17 +54,24 @@ android:orientation="vertical" android:visibility="gone"> - + + @@ -107,17 +115,24 @@ android:orientation="vertical" android:visibility="gone"> - + + @@ -161,17 +176,24 @@ android:orientation="vertical" android:visibility="gone"> - + + diff --git a/app/src/main/res/layout/fragment_icons_debug.xml b/app/src/main/res/layout/fragment_icons_debug.xml index e7e5805..05d6a42 100644 --- a/app/src/main/res/layout/fragment_icons_debug.xml +++ b/app/src/main/res/layout/fragment_icons_debug.xml @@ -13,6 +13,7 @@ diff --git a/app/src/main/res/layout/fragment_layout.xml b/app/src/main/res/layout/fragment_layout.xml index 66ceea6..e2ea519 100644 --- a/app/src/main/res/layout/fragment_layout.xml +++ b/app/src/main/res/layout/fragment_layout.xml @@ -13,6 +13,7 @@ diff --git a/app/src/main/res/layout/fragment_misc.xml b/app/src/main/res/layout/fragment_misc.xml index 8dd2b81..842b7b9 100644 --- a/app/src/main/res/layout/fragment_misc.xml +++ b/app/src/main/res/layout/fragment_misc.xml @@ -13,6 +13,7 @@ diff --git a/app/src/main/res/layout/fragment_status_chips.xml b/app/src/main/res/layout/fragment_status_chips.xml index 718c423..ea1500a 100644 --- a/app/src/main/res/layout/fragment_status_chips.xml +++ b/app/src/main/res/layout/fragment_status_chips.xml @@ -13,6 +13,7 @@ diff --git a/app/src/main/res/layout/fragment_system_icons.xml b/app/src/main/res/layout/fragment_system_icons.xml index e038aa7..9102db6 100644 --- a/app/src/main/res/layout/fragment_system_icons.xml +++ b/app/src/main/res/layout/fragment_system_icons.xml @@ -13,6 +13,7 @@ diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index 87b6388..77c742d 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -1,3 +1,4 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index edbec59..785f928 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -243,14 +243,13 @@ Enable custom date/time pattern Enable custom date/time pattern Enable custom date/time pattern - Examples: HH:mm, EEE d MMM HH:mm:ss, {color(#batterybar 1.2 * ; invRGB 0.5 *RGB) big @sans-serif-condensed}HH:mm Date/time patterns:\n\u2022 Uses Java SimpleDateFormat-style pattern letters\n\u2022 Common symbols: yyyy yy MMMM MMM MM EEEE EEE dd d HH hh mm ss a\n\nTags:\n\u2022 Supported tags: <i>, <u>, <small>, <big>, <font color=\'...\'>, <font face=\'...\'>\n\u2022 <b> is not supported\n\u2022 Custom font tags: <sans>, <serif>, <mono>, <condensed>\n\u2022 {/font} closes any open <font ...> tag\n\u2022 Opening a new colour or font face automatically closes the previous one Shorthand blocks:\n\u2022 {#FA0 big @sans-serif-condensed} is shorthand for opening multiple tags in order\n\u2022 {#FA0} is shorthand for {color(#FA0)}\n\u2022 In shorthand: / closes a tag, color(...) sets font colour, @ sets font face\n\u2022 Each whitespace-separated token inside {} becomes its own tag, except color(...) which may contain spaces\n\nLine breaks:\n\u2022 Use \\n, {\\n} or {\\n15}; the number is the line-height delta in statusbar units\n\u2022 Omit the number to use the default row spacing\n\u2022 { /\\n } style line breaks are written without spaces: {/\\n} or {/\\n15}\n\u2022 Normal line breaks propagate open tags onto the next row\n\u2022 A / line break ends all currently open tags before the next row starts\n\nPipes and grouping:\n\u2022 Maximum two pipes per row; more than two is a syntax error for that row only\n\u2022 Pipes inside single quotes or {} do not split the row\n\u2022 One pipe splits into left and right segments\n\u2022 Two pipes remove the middle section, then split using the outer pipes\n\u2022 Tags are logically continued across pipe splits, even if the middle section is ignored\n\u2022 Rows without pipes are independent; rows with pipes share one common pipe group\n\u2022 A syntax-error row becomes the text Syntax error, with no whole-clock fallback\n\nColours:\n\u2022 Supported operands: #RGB, #ARGB, #RRGGBB, #AARRGGBB, #batterybar, [R,G,B], [A,R,G,B], and numbers\n\u2022 Colour expressions use reverse Polish notation: operands first, operator last\n\u2022 Use ; to separate bright/dark variants; the dark side starts with the raw result from the bright side on the stack\n\u2022 Operators: + - * / ^ inv min max sqrt clip dup\n\u2022 Operators can be channel-filtered, for example +A, *RGB, invRGB, maxRGB, clipA\n\u2022 Final values are clipped to 0..255; a final number becomes opaque grayscale Examples:\n\u2022 <small>EEE d MMM</small> HH:mm\n\u2022 HH<small>:mm</small>\n\u2022 <font color=\'#FA0\'>ss</font>\n\u2022 {color(#FA0 ; invRGB 0.8 *RGB)}ss\n\u2022 {color(#batterybar 1.2 * ; invRGB 0.5 *RGB) big u @sans-serif-condensed}HH:mm\n\u2022 {color([255, 255, 170, 0] ; invRGB)}HH:mm\n\u2022 HH:mm{\\n12 small}EEE d MMM\n\u2022 HH|mm aligns the pipe position across every piped row\n\u2022 left||right removes the middle section before split alignment\n\u2022 {/font}{/\\n big}HH:mm starts a new row without carrying the previous font tags forward Browse font families ▾ Show pattern instructions ▴ Hide pattern instructions - Font families + Font families (tap to copy) Search font family Copied: %1$s Date/time pattern reference only: https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html