- Revision
- 15959
- Author
- grant
- Date
- 2007-11-29 08:40:34 -0800 (Thu, 29 Nov 2007)
Log Message
[APP] Add summary view
Modified Paths
Added Paths
Diff
Modified: branches/rearchitecture/Chandler-App/ocap/chandler/interaction.py (15958 => 15959)
--- branches/rearchitecture/Chandler-App/ocap/chandler/interaction.py 2007-11-29 16:39:45 UTC (rev 15958) +++ branches/rearchitecture/Chandler-App/ocap/chandler/interaction.py 2007-11-29 16:40:34 UTC (rev 15959) @@ -3,6 +3,88 @@ import datetime _ = lambda x:x +class CommunicationStatus(trellis.Component): + trellis.values( + modifiedFlags = frozenset(), + lastModification = None, + stampTypes = frozenset(), + fromMe = False, + toMe = False, + needsReply = False, + read = False, + error = None + ) + +class DashboardEntry(trellis.Component): + trellis.values( + task = False, + commStatus = None, + unread = False, + who = None, + title = u"", + eventAndReminder = (False, None), + date = None, + triage='now' + ) + + @trellis.rule + def commStatus(self): + return CommunicationStatus() + +now = datetime.datetime.now() + +dashboardEntries = ( + DashboardEntry( + task=True, + title = _(u'Welcome to Chandler\u2122 Preview'), + date=datetime.datetime.combine(now.date(), datetime.time(12, 0)), + ), + + DashboardEntry( + title=_(u'Play around with the Calendar'), + unread=True, + date=datetime.datetime.combine(now.date(), datetime.time(15, 0)), + eventAndReminder=(True, None), + ), + + DashboardEntry( + title=_(u'Delete sample items and collections'), + date=now, + triage='later', + ), + + DashboardEntry( + task=True, + who=(u'to', u'someone'), + commStatus=CommunicationStatus(fromMe=True, stampTypes=set(['MailStamp'])), + title=_(u'Set up your accounts'), + date=datetime.datetime.combine(now.date(), datetime.time(16, 0)), + eventAndReminder=(True, None), + triage='later', + ), + + DashboardEntry( + task=True, + unread=True, + title=_(u'Try sharing a Home task list'), + eventAndReminder=(False, 'custom'), + date=datetime.datetime.combine(now.date() + datetime.timedelta(days=1), + datetime.time(8, 0)), + triage='later', + ), + + DashboardEntry( + task=True, + title=u'Download Chandler\u2122 Preview', + eventAndReminder=(True, None), + date=datetime.datetime.combine(now.date(), datetime.time(11, 0)), + triage='done', + ) +) + + + + class MiniCalendar(trellis.Component): trellis.values( range_start = None, @@ -43,8 +125,13 @@ canAdd = True, allowOverlay = True, searchMatches = 0, - checked = False + checked = False, + items = None, ) + + @trellis.rule + def items(self): + return trellis.Set() def __repr__(self): return u"<%s(%s) at 0x%x>" % (type(self).__name__, self.displayName, @@ -69,7 +156,8 @@ iconNameHasClassVariant=True, colorizeIcon=False, dontDisplayAsCalendar=True, - allowOverlay=False + allowOverlay=False, + items=trellis.Set(dashboardEntries), ) inCollection = SidebarEntry( @@ -78,7 +166,7 @@ iconName="In", dontDisplayAsCalendar=True, colorizeIcon=False, - allowOverlay=False + allowOverlay=False, ) outCollection = SidebarEntry( @@ -87,7 +175,8 @@ iconName="Out", colorizeIcon=False, dontDisplayAsCalendar=True, - allowOverlay=False + allowOverlay=False, + items=trellis.Set([dashboardEntries[3]]), ) trashCollection = SidebarEntry( @@ -113,10 +202,23 @@ inCollection, outCollection, trashCollection, - UserSidebarEntry(displayName=_(u"Work"), checked=True), - UserSidebarEntry(displayName=_(u"Home"), checked=True, hue=0), - UserSidebarEntry(displayName=_(u"Fun"), hue=330), + UserSidebarEntry( + displayName=_(u"Work"), + checked=True, + items=trellis.Set(dashboardEntries[i] for i in (1, 2, 5)), + ), + UserSidebarEntry( + displayName=_(u"Home"), + checked=True, + items=trellis.Set(dashboardEntries[i] for i in (0, 1, 4)), + hue=0), + UserSidebarEntry( + displayName=_(u"Fun"), + hue=330, + items=trellis.Set(dashboardEntries[i] for i in (3,)), + ), ]) + return domain.ListModel( data = "" sort_key = lambda coll: (-coll.outOfTheBoxCollection, coll.sortKey),
Added: branches/rearchitecture/Chandler-App/ocap/chandler/summary.py (15958 => 15959)
--- branches/rearchitecture/Chandler-App/ocap/chandler/summary.py 2007-11-29 16:39:45 UTC (rev 15958) +++ branches/rearchitecture/Chandler-App/ocap/chandler/summary.py 2007-11-29 16:40:34 UTC (rev 15959) @@ -0,0 +1,579 @@ +import wx +import wx.grid as grid +import ocap.chandler.domain as domain +import ocap.wxui.table as table +import ocap.wxui.drawing as drawing +import ocap.wxui.multi_state as multi_state +import ocap.wxui.styles as styles +import ocap.chandler.interaction as interaction +import ocap.chandler.triage_buttons as triage_buttons +import peak.events.trellis as trellis +from ocap.wxui.image import get_image, get_raw_image +from datetime import datetime, timedelta +_ = lambda x:x + +class TaskRenderer(table.IconRenderer): + def getStateInfos(self): + cls = type(self) + + for (state, normal, selected) in ( + (False, "pixel", "pixel"), + (True, "TaskStamped", "TaskStampedSelected") + ): + bmInfo = multi_state.BitmapInfo() + bmInfo.stateName = state + bmInfo.normal = normal + bmInfo.selected = selected + bmInfo.rollover = "TaskRollover" + bmInfo.rolloverselected = "TaskRolloverSelected" + bmInfo.mousedown = "TaskMousedown" + bmInfo.mousedownselected = "TaskMousedownSelected" + yield bmInfo + + def advanceValue(self, value): + # "advance" == "toggle" + return not value + +class EventRenderer(table.IconRenderer): + + def getStateInfos(self): + return ( + multi_state.BitmapInfo( + stateName="SumEvent.Unstamped", + normal="pixel", + selected="pixel", + rollover="EventTicklerRollover", + rolloverselected="EventTicklerRolloverSelected", + mousedown="EventTicklerMousedown", + mousedownselected="EventTicklerMousedownSelected" + ), + multi_state.BitmapInfo( + stateName="SumEvent.Stamped", + normal="EventStamped", + selected="EventStampedSelected", + rollover="EventTicklerRollover", + rolloverselected="EventTicklerRolloverSelected", + mousedown="EventTicklerMousedown", + mousedownselected="EventTicklerMousedownSelected" + ), + multi_state.BitmapInfo( + stateName="SumEvent.Tickled", + normal="EventTickled", + selected="EventTickledSelected", + rollover="EventTicklerRollover", + rolloverselected="EventTicklerRolloverSelected", + mousedown="EventTicklerMousedown", + mousedownselected="EventTicklerMousedownSelected" + ), + ) + + def advanceValue(self, value): + event, reminder = value + + if reminder is None: + return (event, 'custom') + else: + return (event, None) + + def mapValueToIconState(self, value): + event, reminder = value + + if reminder: + return "SumEvent.Tickled" + elif event: + return "SumEvent.Stamped" + else: + return "SumEvent.Unstamped" + +class CommStatusRenderer(table.IconRenderer): + def getStateInfos(self): + def getState(name, **kwds): + args = {} + for state in ("Normal", "Selected", "Rollover", "RolloverSelected", + "Mousedown", "MousedownSelected"): + lcState = state.lower() + if not kwds.has_key(lcState): + # If a given state isn't specified, build the name automatically + args[lcState] = "Mail%s%s" % (name, + state != "Normal" and state or '') + elif kwds[lcState] is None: + # If a given state is specified as None, use a blank image. + args[lcState] = "pixel" + else: + # Use the given name, but prepend "Mail" to it because that's + # how the files are named. + args[lcState] = "Mail%s" % kwds[lcState] + return multi_state.BitmapInfo(stateName=name, **args) + + # Build pairs of states (Read and Unread) + for name, hasRead in ( + # Base name, True if it shows an icon when 'read' + ("Plain", False), + ("PlainDraft", True), + ("InDraft", True), + ("In", False), + ("OutDraft", True), + ("Out", False), + ("OutdateDraft", True), + ("Outdate", False), + ("IndateDraft", True), + ("Indate", False), + ("Queued", True), + ("Error", True), + ): + # Each pair has these variations in common + args = { + 'rollover': '%sRollover' % name, + 'rolloverselected': '%sRolloverSelected' % name, + 'mousedown': '%sMousedown' % name, + 'mousedownselected': '%sMousedownSelected' % name + } + + # Do Unread + yield getState("%sUnread" % name, selected='%sUnreadSelected' % name, + **args) + + # Do Read, whether it has 'read' icon or not. + if hasRead: + yield getState("%sRead" % name, selected='%sReadSelected' % name, + **args) + else: + yield getState("%sRead" % name, normal=None, selected=None, + **args) + + # Do NeedsReply by itself + yield getState("NeedsReply") + + UPDATE, OUT, IN, NEITHER, EDITED, SENT, ERROR, QUEUED, DRAFT, NEEDS_REPLY, READ = ( + 1<<n for n in xrange(11) + ) + + def mapValueToIconState(self, value): + flags = self._commStateToFlags(value) + + if flags & self.NEEDS_REPLY: + return "NeedsReply" + + read = (flags & self.READ) and "Read" or "Unread" + + # These don't depend on in vs out, so check them first. + if flags & self.ERROR: + return "Error%s" % (read, ) + if flags & self.QUEUED: + return "Queued%s" % (read, ) + + # Note In vs Out (Out wins if both) vs Plain (if neither, we're done). + draft = (flags & self.DRAFT) and "Draft" or "" + if flags & self.OUT: + inOut = "Out" + # # and keep going... + elif flags & self.IN: + inOut = "In" + # and keep going... + else: + return "Plain%s%s" % (draft, read) + + # We're Out or In -- do Updating and Draft. + updating = (flags & self.UPDATE) and "date" or "" + return "%s%s%s%s" % (inOut, updating, draft, read) + + def _commStateToFlags(self, value): + flags = 0 + + # error + if value.error: + flags |= self.ERROR + + if 'MailStamp' in value.stampTypes: + # update: This means either: we have just + # received an update, or it's ready to go + # out as an update + if ('updated' == value.lastModification or + ('sent' != value.lastModification and + 'sent' in value.modifiedFlags)): + flags |= self.UPDATE + + # in, out, neither + if value.toMe: + flags |= self.IN + if value.fromMe: + flags |= self.OUT + elif not value.toMe: + flags |= self.NEITHER + + # queued + if 'queued' in value.modifiedFlags: + flags |= self.QUEUED + # sent + if value.lastModification in ('sent', 'updated'): + flags |= self.SENT + # draft if it's not one of sent/queued/error + if flags & (self.SENT | + self.QUEUED | + self.ERROR) == 0: + flags |= self.DRAFT + + # edited + if 'edited' in value.modifiedFlags: + flags |= self.EDITED + + # needsReply + if value.needsReply: + flags |= self.NEEDS_REPLY + + # read + if value.read: + flags |= self.READ + + return flags + + def advanceValue(self, value): + newCommStateValues = dict( + (key, getattr(value, key)) + for key in ( + 'modifiedFlags', + 'lastModification', + 'stampTypes', + 'fromMe', + 'toMe', + 'needsReply', + 'read', + 'error' + ) + ) + + if value.needsReply: + newCommStateValues['read'] = False + newCommStateValues['needsReply'] = False + elif not value.read: + newCommStateValues['read'] = True + newCommStateValues['needsReply'] = False + else: # make it needs-reply (and make sure it's read). + newCommStateValues['read'] = True + newCommStateValues['needsReply'] = True + return interaction.CommunicationStatus(**newCommStateValues) + +class TriageRenderer(table.IconRenderer): + values = ["now", "later", "done"] + bitmapProvider = staticmethod( + triage_buttons.TriageButtonImageProvider("Triage.Now.png") + ) + + def getStateInfos(self): + for s in self.values: + yield multi_state.BitmapInfo( + stateName=s, + normal="Triage.%s" % s, + selected="Triage.%s" % s, + rollover="Triage.%s.Rollover" % s, + rolloverselected="Triage.%s.Rollover" % s, + mousedown="Triage.%s.Mousedown" % s, + mousedownselected="Triage.%s.Mousedown" % s + ) + + def advanceValue(self, value): + index = self.values.index(value) + + return self.values[(index+1) % len(self.values)] + + +class WhoRenderer(grid.PyGridCellRenderer): + + def Draw(self, grid, attr, dc, rect, row, col, isSelected): + """ + Note: @@@ Currently only handles left justified single line text. + """ + # Erase the bounding box + dc.SetBackgroundMode(wx.SOLID) + dc.SetPen(wx.TRANSPARENT_PEN) + + dc.DrawRectangleRect(rect) + + # Get the text we'll display + value = grid.Table.GetValue(row, col) + if not value: + prefix = None + text = u"" + else: + prefix, text = value + + # @@@ [grant] disabled for now + isSample = False + + # We'll draw the sample or prefix in gray (unless it's part of the + # selection, in which case we'll leave it white) + if not isSelected and (isSample or prefix is not None): + oldForeground = wx.Colour(dc.GetTextForeground()) + dc.SetTextForeground(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT)) + + haveText = bool(text) + if (prefix is not None) or haveText: + # Draw inside the lines. + dc.SetBackgroundMode(wx.TRANSPARENT) + rect.Inflate(-1, -1) + dc.SetClippingRect(rect) + + textFont = styles.getFont(styles.SummaryRowStyle) + textMeasurements = styles.getMeasurements(textFont) + textHeight = textMeasurements.height + textTop = (rect.GetHeight() - textHeight) / 2 + + if prefix is not None: + # Match up baselines + prefixFont = styles.getFont(styles.SummaryPrefixStyle) + prefixMeasurements = styles.getMeasurements(prefixFont) + prefixHeight = prefixMeasurements.height + prefixTop = textTop + ((textHeight - textMeasurements.descent) - + (prefixHeight - prefixMeasurements.descent)) + dc.SetFont(prefixFont) + width = drawing.DrawClippedTextWithDots(dc, prefix, rect, + top=prefixTop) + if width > 0: + rect.x += width + rect.width -= width + if not isSelected: + dc.SetTextForeground(oldForeground) + dc.SetFont(textFont) + + if haveText: + drawing.DrawClippedTextWithDots(dc, text, rect, top=textTop) + + dc.DestroyClippingRegion() + +class WhenRenderer(grid.PyGridCellRenderer): + @staticmethod + def SHORT_TIME_FORMAT(t): + # @@@ should be replaced by PyICU equivalent + return t.strftime("%H:%M") + + def MEDIUM_DATE_FORMAT(d): + # @@@ should be replaced by PyICU equivalent + return d.strftime("%x") + + def Draw(self, grid, attr, dc, rect, row, col, isSelected): + """ + Draw the date & time, somewhat in the style that Apple Mail does: + Date left justified, time right justified. + """ + # Erase the bounding box + dc.SetBackgroundMode(wx.SOLID) + dc.SetPen(wx.TRANSPARENT_PEN) + dc.DrawRectangleRect(rect) + + # Figure out what to draw. + value = grid.Table.GetValue(row, col) + if value is None: + return # don't draw anything. + + # @@@ [grant] Move this to the code that creates the item (make a + # rule that returns a date rather than a datetime). + # + # Is this date the start time of an anytime or allday event? + # (We won't want to show the time if so, bug 7325) + hideTime = False + #if attributeName == 'displayDate' and item.displayDateSource == 'startTime': + # event = pim.EventStamp(item) + # hideTime = event.allDay or event.anyTime + + itemDate = value.date() + today = datetime.today() + todayDate = today.date() + dateString = None + timeString = None + tzWidth = 0 + tzFont = None + preferDate = True + if itemDate == todayDate: + # Today? say so, and show the time if we only have room for one value + # (unless it's allDay/anyTime) + preferDate = hideTime + dateString = _(u'Today') + elif itemDate == (today + timedelta(days=-1)).date(): + # Yesterday? say so. + dateString = _(u'Yesterday') + elif itemDate == (today + timedelta(days=1)).date(): + # To-morrow? say so. + dateString = _(u'Tomorrow') + #else: + # Do day names for days in the last week. + # (not anymore, see bug 6707) + #dateString = pim.weekdayName(value) + + if dateString is None: + dateString = self.MEDIUM_DATE_FORMAT(value) + if timeString is None and not hideTime: + timeString = self.SHORT_TIME_FORMAT(value) + #tzString = shortTZ(view, value) + tzString = '' + if len(tzString) > 0: + tzFont = styles.getFont(styles.SummaryPrefixStyle) + tzWidth = dc.GetFullTextExtent(tzString, tzFont)[0] + + # Draw inside the lines. + dc.SetBackgroundMode(wx.TRANSPARENT) + rect.Inflate(-1, -1) + dc.SetClippingRect(rect) + + dateWidth = dc.GetTextExtent(dateString)[0] + timeWidth = dc.GetTextExtent(timeString)[0] if not hideTime else 0 + spaceWidth = dc.GetTextExtent(' ')[0] + + # If we don't have room for both values, draw one, clipped if necessary. + if (dateWidth + spaceWidth + timeWidth + tzWidth) > rect.width: + if preferDate: + drawing.DrawClippedTextWithDots(dc, dateString, rect) + hideTime = True # suppress timezone display, bug 9942. + else: + if tzWidth: + rect.width -= tzWidth + + hideTime.DrawClippedTextWithDots(dc, timeString, rect, + alignRight=True) + if tzWidth: + rect.width += tzWidth + else: + # Enough room to draw both + dc.DrawText(dateString, rect.x + 1, rect.y + 1) + if not hideTime: + dc.DrawText(timeString, + rect.x + rect.width - (timeWidth + 2 + tzWidth), + rect.y + 1) + + if tzWidth and not hideTime: + oldFont = dc.GetFont() + dc.SetFont(tzFont) + hideTime.DrawClippedTextWithDots(dc, tzString, rect, + alignRight=True) + dc.SetFont(oldFont) + + dc.DestroyClippingRegion() + +class SummaryPresentation(table.TablePresentation): + + def __init__(self, parent, items): + titleColumn = table.Column(attributeName="title", width=120, + scaleColumn=grid.Grid.GRID_COLUMN_SCALABLE, + heading=_(u"Title") + ) + columns = trellis.List([ + table.Column( + attributeName="task", + type="Task", + width=23, + icon="ColHTask", + useSortArrows=False, + ), + table.Column( + attributeName="commStatus", + type="CommunicationStatus", + width=23, + icon="ColHMail", + useSortArrows=False, + ), + table.Column( + attributeName="who", + type="Who", + width=85, + heading=_(u"Who") + ), + titleColumn, + table.Column( + attributeName="eventAndReminder", + type="Event", + width=23, + icon="ColHEvent", + useSortArrows=False, + ), + table.Column( + attributeName="date", + type="When", + width=115, + scaleColumn=grid.Grid.GRID_COLUMN_SCALABLE, + sortKey=lambda item: item.date + if item.date is not None + else datetime.datetime(datetime.MINYEAR, 1, 1), + heading=_(u"Date") + ), + table.Column( + attributeName="triage", + type="TriageStatus", + width=TriageRenderer.bitmapProvider.getImageSize().width, + icon="ColHTriageStatus", + useSortArrows=False, + ), + ]) + activeColumn = titleColumn + items = domain.ListModel(data="" + summaryGrid = table.Table(parent, items, size=parent.Size) + + super(SummaryPresentation, self).__init__(parent, items, columns, + activeColumn=titleColumn, table=summaryGrid) + summaryGrid.SetColLabelSize(20) + summaryGrid.SetDefaultCellFont(styles.getFont(styles.SummaryRowStyle)) + summaryGrid.SetLabelFont(styles.getFont(styles.SummaryHeaderStyle)) + + summaryGrid.ScaleWidthToFit(True) + summaryGrid.RegisterDataType("Task", TaskRenderer(), None) + summaryGrid.RegisterDataType("CommunicationStatus", CommStatusRenderer(), None) + summaryGrid.RegisterDataType("Who", WhoRenderer(), None) + summaryGrid.RegisterDataType("When", WhenRenderer(), None) + summaryGrid.RegisterDataType("Event", EventRenderer(), None) + summaryGrid.RegisterDataType("TriageStatus", TriageRenderer(), None) + + @trellis.optional + @trellis.rule + def printStuff(self): + if self.listModel is not None: + print 'Selected items: %s' % (', '.join(unicode(x.displayName) for x in self.listModel.selected),) + + def CanClick(self, row, col): + return col in (0, 1, 4, 6) + + def OnClick(self, row, col): + renderer = self.View.GetCellRenderer(row, col) + advance = getattr(renderer, 'advanceValue', None) + + if advance is not None: + column = self.columns[col] + item = self.listModel[self.RowToIndex(row)] + oldValue = getattr(item, column.attributeName) + newValue = advance(oldValue) + setattr(item, column.attributeName, newValue) + + def TrackMouse(self, row, col): + return col in (0, 1, 4, 6) + + def GetToolTipString(self, row, col): + if col == 0: + item = self.listModel[self.RowToIndex(row)] + if item.task: + return _(u"Remove from Task list") + else: + return _(u"Add to Task list") + elif col == 4: + item = self.listModel[self.RowToIndex(row)] + if item.eventAndReminder[1]: + return _(u"Remove custom alarm") + else: + return _(u"Add custom alarm") + + + +def Demo(app): + """ + Small demo of a standalone Summary View. Run it via + + ocap-debug ocap.chandler.summary:Demo + """ + iApp = interaction.Application() + frame = wx.Frame(None, title="Summary Mockup") + frame.Size = wx.Size(640, frame.Size.height) + frame.CenterOnParent() + summary = SummaryPresentation(frame, iApp.sidebar[0].items) + frame.Show() + + # Make summary and iApp variables available to PyCrust. + from ocap.debugging import Debugger + debugger = Debugger(app) + debugger.variables['summary'] = summary + debugger.variables['iApp'] = iApp Property changes on: branches/rearchitecture/Chandler-App/ocap/chandler/summary.py ___________________________________________________________________ Name: svn:eol-style + native
_______________________________________________ Commits mailing list [email protected] http://lists.osafoundation.org/mailman/listinfo/commits
