/* * Copyright (c) Henrik Niehaus * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * 3. Neither the name of the project (Lazy Bones) nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package lazybones.gui.components.timeline; import static lazybones.gui.components.timeline.Timeline.PADDING; import static lazybones.gui.components.timeline.Timeline.ROW_HEIGHT; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.HashSet; import java.util.List; import java.util.Observable; import java.util.Observer; import java.util.Set; import javax.swing.JPanel; import javax.swing.UIManager; import lazybones.LazyBones; import lazybones.LazyBonesTimer; import lazybones.TimerManager; import lazybones.TimersChangedEvent; import lazybones.utils.Utilities; public class TimelineList extends JPanel implements Observer { private final List<LazyBonesTimer> data = new ArrayList<LazyBonesTimer>(); private final List<TimelineListener> listeners = new ArrayList<TimelineListener>(); private Calendar calendar = new GregorianCalendar(); private int rowCount = 0; private Color background; private Color rowBackground; private Color rowBackgroundAlt; private Color lineColor; private Color textColor; private Color overlayTextColor; private Color midnightLineColor; private TimerManager timerManager; public TimelineList(TimerManager timerManager) { this.timerManager = timerManager; setCalendar(calendar); setBackground(background); setLayout(new TimelineLayout()); timerManager.addObserver(this); Thread repainter = new Thread() { @Override public void run() { while (true) { try { Thread.sleep(10 * 1000); repaint(); } catch (InterruptedException e) { } } } }; repainter.setName("Lazy Bones Timeline repainter"); repainter.start(); } public int getRowCount() { return rowCount; } public void addTimer(LazyBonesTimer timer) { for (int i = 0; i < data.size(); i++) { LazyBonesTimer t = data.get(i); if (t.getChannelNumber() > timer.getChannelNumber()) { data.add(i, timer); fireTimelineChanged(); return; } } // the channelNumber was greater than the channelNumber of the other timers // -> add this timer at the end of the table data.add(timer); fireTimelineChanged(); } public void removeTimer(LazyBonesTimer timer) { int index = data.indexOf(timer); if (index >= 0) { data.remove(timer); fireTimelineChanged(); } } public void clear() { data.clear(); fireTimelineChanged(); } private void fireTimelineChanged() { this.removeAll(); for (LazyBonesTimer timer : data) { TimelineElement te = new TimelineElement(timer, getCalendar()); add(te); } Set<Integer> channelNumbers = new HashSet<>(); for (LazyBonesTimer timer : data) { channelNumbers.add(timer.getChannelNumber()); } rowCount = channelNumbers.size(); for (TimelineListener l : listeners) { l.timelineChanged(data); } revalidate(); repaint(); } public Calendar getCalendar() { return calendar; } public void setCalendar(Calendar calendar) { this.calendar = (Calendar) calendar.clone(); this.calendar.set(Calendar.HOUR_OF_DAY, 0); this.calendar.set(Calendar.MINUTE, 0); this.calendar.set(Calendar.SECOND, 0); this.calendar.set(Calendar.MILLISECOND, 0); showTimersForCurrentDate(timerManager.getTimers()); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); // paint row background for (int i = 0; i < getRowCount(); i++) { g.setColor(i % 2 == 0 ? rowBackground : rowBackgroundAlt); g.fillRect(0, i * (ROW_HEIGHT + PADDING), getWidth(), (ROW_HEIGHT + PADDING)); } // paint vertical lines double pixelsPerHour = (double) (getWidth() - 1) / (double) 24; int startHour = Integer.parseInt(LazyBones.getProperties().getProperty("timelineStartHour")); for (int i = 0; i < 25; i++) { if (i == (24 - startHour)) { g.setColor(midnightLineColor); g.drawLine((int) (i * pixelsPerHour) + 1, 0, (int) (i * pixelsPerHour) + 1, getHeight()); } else { g.setColor(lineColor); } g.drawLine((int) (i * pixelsPerHour), 0, (int) (i * pixelsPerHour), getHeight()); } // paint day string around midnight to clarify on which day a timer runs paintDayOverlay(g, startHour, pixelsPerHour); } private void paintDayOverlay(Graphics g, int startHour, double pixelsPerHour) { // enable font anti aliasing Graphics2D g2d = (Graphics2D) g; // storing original anitalising flag Object state = g2d.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING); if (state != RenderingHints.VALUE_TEXT_ANTIALIAS_ON) { g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); } Calendar selectedDay = getCalendar(); Calendar nextDay = (Calendar) getCalendar().clone(); nextDay.add(Calendar.DAY_OF_MONTH, 1); SimpleDateFormat sdf = new SimpleDateFormat("EEEE"); String selectedDayName = sdf.format(selectedDay.getTime()); String nextDayName = sdf.format(nextDay.getTime()); double positionOfMidnight = (24 - startHour) * pixelsPerHour; int fontSize = 24; Font font = new Font("SansSerif", Font.BOLD, fontSize); FontMetrics fm = g.getFontMetrics(font); int selectedDayNameWidth = fm.stringWidth(selectedDayName); // int nextDayNameWidth = fm.stringWidth(nextDayName); g.setColor(overlayTextColor); g.setFont(font); int x = (int) (positionOfMidnight - selectedDayNameWidth - 20); int y = (ROW_HEIGHT + PADDING) * getRowCount(); g.drawString(selectedDayName, x, y + ROW_HEIGHT); x = (int) (positionOfMidnight + 20); g.drawString(nextDayName, x, y + ROW_HEIGHT); g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, state); } @Override protected void paintChildren(Graphics g) { super.paintChildren(g); // paint current time line int startHour = Integer.parseInt(LazyBones.getProperties().getProperty("timelineStartHour")); Calendar currentTime = Calendar.getInstance(); // currentTime.set(Calendar.HOUR_OF_DAY, 1); // currentTime.set(Calendar.MINUTE, 23); // currentTime.add(Calendar.DAY_OF_MONTH, 1); if (isOnCurrentDate(currentTime)) { // are we showing the current day ? g.setColor(new Color(255, 0, 0, 128)); double pixelsPerMinute = (double) (getWidth() - 1) / (double) (24 * 60); int minute = currentTime.get(Calendar.MINUTE); int hour = currentTime.get(Calendar.HOUR_OF_DAY); if (hour >= startHour) { hour -= startHour; } else { hour += (24 - startHour); } int minuteOfDay = hour * 60 + minute; int position = (int) (minuteOfDay * pixelsPerMinute); g.drawLine(position, 0, position, getHeight()); } } private boolean isOnCurrentDate(Calendar time) { int startHour = Integer.parseInt(LazyBones.getProperties().getProperty("timelineStartHour")); Calendar selectedDayAtStartHour = (Calendar) getCalendar().clone(); selectedDayAtStartHour.set(Calendar.HOUR_OF_DAY, startHour); Calendar dayAfterAtStartHour = (Calendar) selectedDayAtStartHour.clone(); dayAfterAtStartHour.add(Calendar.DAY_OF_MONTH, 1); return (time.after(selectedDayAtStartHour) & time.before(dayAfterAtStartHour)); } public void showTimersForCurrentDate(List<LazyBonesTimer> timers) { clear(); for (LazyBonesTimer timer : timers) { if (Utilities.timerRunsOnDate(timer, calendar)) { addTimer(timer); } } } @Override public void update(Observable o, Object arg) { if (o == timerManager) { if (arg instanceof TimersChangedEvent) { TimersChangedEvent tce = (TimersChangedEvent) arg; switch (tce.getType()) { case TimersChangedEvent.ALL: List<LazyBonesTimer> timers = tce.getTimers(); clear(); for (LazyBonesTimer timer : timers) { if (Utilities.timerRunsOnDate(timer, calendar)) { addTimer(timer); } } break; case TimersChangedEvent.TIMER_ADDED: LazyBonesTimer timer = tce.getTimer(); if (Utilities.timerRunsOnDate(timer, calendar)) { addTimer(timer); } break; case TimersChangedEvent.TIMER_REMOVED: timer = tce.getTimer(); if (Utilities.timerRunsOnDate(timer, calendar)) { removeTimer(timer); } break; } } } } public void addTimelineListener(TimelineListener l) { listeners.add(l); } public void removeTimelineListener(TimelineListener l) { listeners.remove(l); } @Override public Dimension getPreferredSize() { Dimension d = super.getPreferredSize(); d.height = (ROW_HEIGHT + PADDING) * getRowCount(); return d; } @Override public void updateUI() { super.updateUI(); background = UIManager.getColor("List.background"); lineColor = UIManager.getColor("Panel.background").darker(); rowBackground = UIManager.getColor("List.background"); rowBackgroundAlt = UIManager.getColor("Panel.background"); textColor = UIManager.getColor("Label.foreground"); overlayTextColor = new Color(textColor.getRed(), textColor.getGreen(), textColor.getBlue(), 51); // text color with 20% opacity midnightLineColor = textColor; } }