diff --git a/.gitignore b/.gitignore index 814731b..d1c8c2c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,18 @@ installer feeds.dat log.txt settings.dat + +__pycache__ +.vscode +.mypy_cache +.venv + +parser.out +parsetab.py + +*.pyc +*.bak +*.log + +Pipfile.lock + diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..7f3e065 --- /dev/null +++ b/Pipfile @@ -0,0 +1,31 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +pylint = "*" +autopep8 = "*" +jupyter = "*" +notebook = "*" +ipython = "*" +flake8 = "*" +flake8-mypy = "*" +mypy-lang = "*" +isort = "*" +winpdb-reborn = "==2.0.0.dev5" +ipdb = "*" +pycallgraph = "*" +psutil = "*" +pudb = "*" + +[packages] +pygame = "*" +wxpython = "*" +feedparser = "*" +ply = "*" +pywin32 = {version = "*",sys_platform = "== 'win32'"} +py2exe = {version = "*",sys_platform = "== 'win32'"} +# winsound = {version = "*",sys_platform = "== 'win32'"} +#[requires] +#python_version = "3.8" diff --git a/controller.py b/controller.py index 54f3535..8368ca3 100644 --- a/controller.py +++ b/controller.py @@ -1,144 +1,272 @@ -import wx -import idle -import feeds -import popups -import view -import updater -import util -import winsound -import socket -from settings import settings - -class Controller(object): - def __init__(self): - socket.setdefaulttimeout(settings.SOCKET_TIMEOUT) - self.icon = view.TaskBarIcon(self) - self.manager = feeds.FeedManager() - self.manager.load() - self.add_default_feeds() - self.popup = None - self.polling = False - self.enabled = True - self.on_poll() - self.on_check_for_updates() - def add_default_feeds(self): - if self.manager.feeds: - return - for url in settings.DEFAULT_FEED_URLS: - feed = feeds.Feed(url) - feed.interval = 60 * 60 * 24 - self.manager.add_feed(feed) - def parse_args(self, message): - urls = message.split('\n') - for url in urls: - url = url.strip() - if not url: - continue - self.add_feed(url) - def enable(self): - self.icon.set_icon('icons/feed.png') - self.enabled = True - self.poll() - def disable(self): - self.icon.set_icon('icons/feed_disabled.png') - self.enabled = False - def save(self): - self.manager.save() - def on_check_for_updates(self): - try: - self.check_for_updates(False) - finally: - wx.CallLater(1000 * 60 * 5, self.on_check_for_updates) - def check_for_updates(self, force=True): - updater.run(self, force) - def on_poll(self): - try: - self.poll() - finally: - wx.CallLater(1000 * 5, self.on_poll) - def poll(self): - if self.polling: - return - if not self.enabled: - return - if settings.DISABLE_WHEN_IDLE and idle.get_idle_duration() > settings.USER_IDLE_TIMEOUT: - return - if not self.manager.should_poll(): - return - self.polling = True - self.icon.set_icon('icons/feed_go.png') - util.start_thread(self._poll_thread) - def _poll_thread(self): - found_new = False - try: - for new_items in self.manager.poll(): - found_new = True - wx.CallAfter(self._poll_result, new_items) - finally: - wx.CallAfter(self._poll_complete, found_new) - def _poll_result(self, new_items): - items = self.manager.items - if self.popup: - index = self.popup.index - else: - index = len(items) - items.extend(new_items) - self.show_items(items, index, False) - def _poll_complete(self, found_new): - if found_new: - self.save() - self.polling = False - self.icon.set_icon('icons/feed.png') - def force_poll(self): - for feed in self.manager.feeds: - feed.last_poll = 0 - self.poll() - def show_items(self, items, index, focus): - play_sound = False - if not items: - return - if not self.popup: - self.popup = popups.PopupManager() - self.popup.Bind(popups.EVT_POPUP_CLOSE, self.on_popup_close) - if not focus: - play_sound = True - self.popup.set_items(items, index, focus) - if focus: - self.popup.auto = False - if play_sound: - self.play_sound() - def play_sound(self): - if settings.PLAY_SOUND: - path = settings.SOUND_PATH - flags = winsound.SND_FILENAME | winsound.SND_ASYNC - try: - winsound.PlaySound(path, flags) - except Exception: - pass - def show_popup(self): - items = self.manager.items - index = len(items) - 1 - self.show_items(items, index, True) - def add_feed(self, url=''): - feed = view.AddFeedDialog.show_wizard(None, url) - if not feed: - return - self.manager.add_feed(feed) - self.save() - self.poll() - def edit_settings(self): - window = view.SettingsDialog(None, self) - window.Center() - window.ShowModal() - window.Destroy() - def close(self): - try: - if self.popup: - self.popup.on_close() - wx.CallAfter(self.icon.Destroy) - finally: - pass #wx.GetApp().ExitMainLoop() - def on_popup_close(self, event): - self.popup = None - self.manager.purge_items(settings.ITEM_CACHE_AGE) - \ No newline at end of file +# -*- coding: utf-8 -*- + +"""[summary] + +Returns: + [type] -- [description] +""" + +import logging +import socket +import sys + +try: + import wx +except ModuleNotFoundError: + sys.exit('\n\timport wx\n') + +import feeds +import idle +import popups +import updater +import util +import view +from settings import settings + +if sys.platform.startswith('win32'): + import winsound + + +class Controller(object): + """[summary] + + Arguments: + object {[type]} -- [description] + """ + + def __init__(self): + """[summary] + """ + + print('Initializing Controller class') # FIXME: Delete this + + socket.setdefaulttimeout(settings.SOCKET_TIMEOUT) + self.icon = view.TaskBarIcon(self) + self.manager = feeds.FeedManager() + self.manager.load() + self.add_default_feeds() + self.popup = None + self.polling = False + self.enabled = True + self.on_poll() + self.on_check_for_updates() + + print('Initialized Controller class') # FIXME: Delete this + + def add_default_feeds(self): + """[summary] + """ + + if self.manager.feeds: + return + for url in settings.DEFAULT_FEED_URLS: + feed = feeds.Feed(url) + feed.interval = 60 * 60 * 24 + self.manager.add_feed(feed) + + def parse_args(self, message): + """[summary] + + Arguments: + message {[type]} -- [description] + """ + + urls = message.split('\n') + for url in urls: + url = url.strip() + if not url: + continue + self.add_feed(url) + + def enable(self): + """[summary] + """ + + self.icon.set_icon('icons/feed.png') + self.enabled = True + self.poll() + + def disable(self): + """[summary] + """ + + self.icon.set_icon('icons/feed_disabled.png') + self.enabled = False + + def save(self): + """[summary] + """ + + self.manager.save() + + def on_check_for_updates(self): + """[summary] + """ + + try: + self.check_for_updates(False) + finally: + wx.CallLater(1000 * 60 * 5, self.on_check_for_updates) + + def check_for_updates(self, force=True): + """[summary] + + Keyword Arguments: + force {bool} -- [description] (default: {True}) + """ + + updater.run(self, force) + + def on_poll(self): + """[summary] + """ + + try: + self.poll() + finally: + wx.CallLater(1000 * 5, self.on_poll) + + def poll(self): + """[summary] + """ + + if self.polling: + return + if not self.enabled: + return + if settings.DISABLE_WHEN_IDLE and idle.get_idle_duration() > \ + settings.USER_IDLE_TIMEOUT: + return + if not self.manager.should_poll(): + return + self.polling = True + self.icon.set_icon('icons/feed_go.png') + util.start_thread(self._poll_thread) + + def _poll_thread(self): + """[summary] + """ + + found_new = False + try: + for new_items in self.manager.poll(): + found_new = True + wx.CallAfter(self._poll_result, new_items) + finally: + wx.CallAfter(self._poll_complete, found_new) + + def _poll_result(self, new_items): + """[summary] + + Arguments: + new_items {[type]} -- [description] + """ + + items = self.manager.items + if self.popup: + index = self.popup.index + else: + index = len(items) + items.extend(new_items) + self.show_items(items, index, False) + + def _poll_complete(self, found_new): + """[summary] + + Arguments: + found_new {[type]} -- [description] + """ + if found_new: + self.save() + self.polling = False + self.icon.set_icon('icons/feed.png') + + def force_poll(self): + """[summary] + """ + + for feed in self.manager.feeds: + feed.last_poll = 0 + self.poll() + + def show_items(self, items, index, focus): + """[summary] + + Arguments: + items {[type]} -- [description] + index {[type]} -- [description] + focus {[type]} -- [description] + """ + + play_sound = False + if not items: + return + if not self.popup: + self.popup = popups.PopupManager() + self.popup.Bind(popups.EVT_POPUP_CLOSE, self.on_popup_close) + if not focus: + play_sound = True + self.popup.set_items(items, index, focus) + if focus: + self.popup.auto = False + if play_sound: + self.play_sound() + + def play_sound(self): + """[summary] + """ + + if settings.PLAY_SOUND: + path = settings.SOUND_PATH + flags = winsound.SND_FILENAME | winsound.SND_ASYNC + try: + winsound.PlaySound(path, flags) + except Exception: + pass + + def show_popup(self): + """[summary] + """ + items = self.manager.items + index = len(items) - 1 + self.show_items(items, index, True) + + def add_feed(self, url=''): + """[summary] + + Keyword Arguments: + url {str} -- [description] (default: {''}) + """ + + feed = view.AddFeedDialog.show_wizard(None, url) + if not feed: + return + self.manager.add_feed(feed) + self.save() + self.poll() + + def edit_settings(self): + """[summary] + """ + + window = view.SettingsDialog(None, self) + window.Center() + window.ShowModal() + window.Destroy() + + def close(self): + """[summary] + """ + + try: + if self.popup: + self.popup.on_close() + wx.CallAfter(self.icon.Destroy) + finally: + pass # wx.GetApp().ExitMainLoop() + + def on_popup_close(self, event): + self.popup = None + self.manager.purge_items(settings.ITEM_CACHE_AGE) + +# EOF diff --git a/controls.py b/controls.py index dec9a25..9eba171 100644 --- a/controls.py +++ b/controls.py @@ -1,192 +1,451 @@ -import wx -import wx.lib.wordwrap as wordwrap -import util - -class Event(wx.PyEvent): - def __init__(self, event_object, type): - super(Event, self).__init__() - self.SetEventType(type.typeId) - self.SetEventObject(event_object) - -EVT_HYPERLINK = wx.PyEventBinder(wx.NewEventType()) - -class Line(wx.PyPanel): - def __init__(self, parent, pen=wx.BLACK_PEN): - super(Line, self).__init__(parent, -1, style=wx.BORDER_NONE) - self.pen = pen - self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) - self.Bind(wx.EVT_PAINT, self.on_paint) - self.Bind(wx.EVT_SIZE, self.on_size) - def on_size(self, event): - self.Refresh() - def on_paint(self, event): - dc = wx.AutoBufferedPaintDC(self) - dc.Clear() - dc.SetPen(self.pen) - width, height = self.GetClientSize() - y = height / 2 - dc.DrawLine(0, y, width, y) - def DoGetBestSize(self): - return -1, self.pen.GetWidth() - -class Text(wx.PyPanel): - def __init__(self, parent, width, text): - super(Text, self).__init__(parent, -1, style=wx.BORDER_NONE) - self.text = text - self.width = width - self.wrap = True - self.rects = [] - self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) - self.Bind(wx.EVT_PAINT, self.on_paint) - self.Bind(wx.EVT_SIZE, self.on_size) - def on_size(self, event): - self.Refresh() - def on_paint(self, event): - dc = wx.AutoBufferedPaintDC(self) - self.setup_dc(dc) - dc.Clear() - self.draw_lines(dc) - def setup_dc(self, dc): - parent = self.GetParent() - dc.SetFont(self.GetFont()) - dc.SetTextBackground(parent.GetBackgroundColour()) - dc.SetTextForeground(parent.GetForegroundColour()) - dc.SetBackground(wx.Brush(parent.GetBackgroundColour())) - def draw_lines(self, dc, emulate=False): - if self.wrap: - text = wordwrap.wordwrap(self.text.strip(), self.width, dc) - else: - text = self.text.strip() - lines = text.split('\n') - lines = [line.strip() for line in lines] - lines = [line for line in lines if line] - x, y = 0, 0 - rects = [] - for line in lines: - if not emulate: - dc.DrawText(line, x, y) - w, h = dc.GetTextExtent(line) - rects.append(wx.Rect(x, y, w, h)) - y += h - if not emulate: - self.rects = rects - return y - def compute_height(self): - dc = wx.ClientDC(self) - self.setup_dc(dc) - height = self.draw_lines(dc, True) - return height - def fit_no_wrap(self): - dc = wx.ClientDC(self) - self.setup_dc(dc) - width, height = dc.GetTextExtent(self.text.strip()) - self.width = width - self.wrap = False - def DoGetBestSize(self): - height = self.compute_height() - return self.width, height - -class Link(Text): - def __init__(self, parent, width, link, text): - super(Link, self).__init__(parent, width, text) - self.link = link - self.trigger = False - self.hover = False - self.Bind(wx.EVT_LEAVE_WINDOW, self.on_leave) - self.Bind(wx.EVT_MOTION, self.on_motion) - self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down) - self.Bind(wx.EVT_LEFT_UP, self.on_left_up) - self.Bind(wx.EVT_RIGHT_UP, self.on_right_up) - def hit_test(self, point): - for rect in self.rects: - if rect.Contains(point): - self.on_hover() - break - else: - self.on_unhover() - def on_motion(self, event): - self.hit_test(event.GetPosition()) - def on_leave(self, event): - self.on_unhover() - def on_hover(self): - if self.hover: - return - self.hover = True - font = self.GetFont() - font.SetUnderlined(True) - self.SetFont(font) - self.SetCursor(wx.StockCursor(wx.CURSOR_HAND)) - self.Refresh() - def on_unhover(self): - if not self.hover: - return - self.hover = False - self.trigger = False - font = self.GetFont() - font.SetUnderlined(False) - self.SetFont(font) - self.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT)) - self.Refresh() - def on_left_down(self, event): - if self.hover: - self.trigger = True - def on_left_up(self, event): - if self.hover and self.trigger: - self.post_event() - self.trigger = False - def on_right_up(self, event): - menu = wx.Menu() - util.menu_item(menu, 'Open Link', self.on_open_link) - util.menu_item(menu, 'Copy Link', self.on_copy_link) - self.PopupMenu(menu, event.GetPosition()) - def on_open_link(self, event): - self.post_event() - def on_copy_link(self, event): - if wx.TheClipboard.Open(): - wx.TheClipboard.SetData(wx.TextDataObject(self.link)) - wx.TheClipboard.Close() - def post_event(self): - event = Event(self, EVT_HYPERLINK) - event.link = self.link - wx.PostEvent(self, event) - -class BitmapLink(wx.PyPanel): - def __init__(self, parent, link, bitmap, hover_bitmap=None): - super(BitmapLink, self).__init__(parent, -1) - self.link = link - self.bitmap = bitmap - self.hover_bitmap = hover_bitmap or bitmap - self.hover = False - self.trigger = False - self.SetInitialSize(bitmap.GetSize()) - self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) - self.Bind(wx.EVT_PAINT, self.on_paint) - self.Bind(wx.EVT_ENTER_WINDOW, self.on_enter) - self.Bind(wx.EVT_LEAVE_WINDOW, self.on_leave) - self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down) - self.Bind(wx.EVT_LEFT_UP, self.on_left_up) - def on_paint(self, event): - parent = self.GetParent() - dc = wx.AutoBufferedPaintDC(self) - dc.SetBackground(wx.Brush(parent.GetBackgroundColour())) - dc.Clear() - bitmap = self.hover_bitmap if self.hover else self.bitmap - dc.DrawBitmap(bitmap, 0, 0, True) - def on_enter(self, event): - self.hover = True - self.SetCursor(wx.StockCursor(wx.CURSOR_HAND)) - self.Refresh() - def on_leave(self, event): - self.trigger = False - self.hover = False - self.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT)) - self.Refresh() - def on_left_down(self, event): - self.trigger = True - def on_left_up(self, event): - if self.trigger: - event = Event(self, EVT_HYPERLINK) - event.link = self.link - wx.PostEvent(self, event) - self.trigger = False - \ No newline at end of file +# -*- coding: utf-8 -*- + +"""[summary] + +Returns: + [type] -- [description] +""" + + +import wx +import wx.lib.wordwrap as wordwrap + +import util + + +class Event(wx.PyEvent): + """[summary] + + Arguments: + wx {[type]} -- [description] + """ + + def __init__(self, event_object, type): + """[summary] + + Arguments: + event_object {[type]} -- [description] + type {[type]} -- [description] + """ + + super(Event, self).__init__() + self.SetEventType(type.typeId) + self.SetEventObject(event_object) + + +EVT_HYPERLINK = wx.PyEventBinder(wx.NewEventType()) + + +class Line(wx.PyPanel): + """[summary] + + Arguments: + wx {[type]} -- [description] + """ + + def __init__(self, parent, pen=wx.BLACK_PEN): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Keyword Arguments: + pen {[type]} -- [description] (default: {wx.BLACK_PEN}) + """ + + super(Line, self).__init__(parent, -1, style=wx.BORDER_NONE) + self.pen = pen + self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) + self.Bind(wx.EVT_PAINT, self.on_paint) + self.Bind(wx.EVT_SIZE, self.on_size) + + def on_size(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.Refresh() + + def on_paint(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + dc = wx.AutoBufferedPaintDC(self) + dc.Clear() + dc.SetPen(self.pen) + width, height = self.GetClientSize() + y = height / 2 + dc.DrawLine(0, y, width, y) + + def DoGetBestSize(self): + """[summary] + + Returns: + [type] -- [description] + """ + + return -1, self.pen.GetWidth() + + +class Text(wx.PyPanel): + """[summary] + + Arguments: + wx {[type]} -- [description] + """ + + def __init__(self, parent, width, text): + """[summary] + + Arguments: + parent {[type]} -- [description] + width {[type]} -- [description] + text {[type]} -- [description] + """ + + super(Text, self).__init__(parent, -1, style=wx.BORDER_NONE) + self.text = text + self.width = width + self.wrap = True + self.rects = [] + self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) + self.Bind(wx.EVT_PAINT, self.on_paint) + self.Bind(wx.EVT_SIZE, self.on_size) + + def on_size(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.Refresh() + + def on_paint(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + dc = wx.AutoBufferedPaintDC(self) + self.setup_dc(dc) + dc.Clear() + self.draw_lines(dc) + + def setup_dc(self, dc): + """[summary] + + Arguments: + dc {[type]} -- [description] + """ + + parent = self.GetParent() + dc.SetFont(self.GetFont()) + dc.SetTextBackground(parent.GetBackgroundColour()) + dc.SetTextForeground(parent.GetForegroundColour()) + dc.SetBackground(wx.Brush(parent.GetBackgroundColour())) + + def draw_lines(self, dc, emulate=False): + """[summary] + + Arguments: + dc {[type]} -- [description] + + Keyword Arguments: + emulate {bool} -- [description] (default: {False}) + + Returns: + [type] -- [description] + """ + + if self.wrap: + text = wordwrap.wordwrap(self.text.strip(), self.width, dc) + else: + text = self.text.strip() + lines = text.split('\n') + lines = [line.strip() for line in lines] + lines = [line for line in lines if line] + x, y = 0, 0 + rects = [] + for line in lines: + if not emulate: + dc.DrawText(line, x, y) + w, h = dc.GetTextExtent(line) + rects.append(wx.Rect(x, y, w, h)) + y += h + if not emulate: + self.rects = rects + return y + + def compute_height(self): + """[summary] + + Returns: + [type] -- [description] + """ + + dc = wx.ClientDC(self) + self.setup_dc(dc) + height = self.draw_lines(dc, True) + return height + + def fit_no_wrap(self): + """[summary] + """ + + dc = wx.ClientDC(self) + self.setup_dc(dc) + width, height = dc.GetTextExtent(self.text.strip()) + self.width = width + self.wrap = False + + def DoGetBestSize(self): + """[summary] + + Returns: + [type] -- [description] + """ + + height = self.compute_height() + return self.width, height + + +class Link(Text): + """[summary] + + Arguments: + Text {[type]} -- [description] + """ + + def __init__(self, parent, width, link, text): + super(Link, self).__init__(parent, width, text) + self.link = link + self.trigger = False + self.hover = False + self.Bind(wx.EVT_LEAVE_WINDOW, self.on_leave) + self.Bind(wx.EVT_MOTION, self.on_motion) + self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down) + self.Bind(wx.EVT_LEFT_UP, self.on_left_up) + self.Bind(wx.EVT_RIGHT_UP, self.on_right_up) + + def hit_test(self, point): + """[summary] + + Arguments: + point {[type]} -- [description] + """ + + for rect in self.rects: + if rect.Contains(point): + self.on_hover() + break + else: + self.on_unhover() + + def on_motion(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.hit_test(event.GetPosition()) + + def on_leave(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.on_unhover() + + def on_hover(self): + """[summary] + """ + + if self.hover: + return + self.hover = True + font = self.GetFont() + font.SetUnderlined(True) + self.SetFont(font) + self.SetCursor(wx.StockCursor(wx.CURSOR_HAND)) + self.Refresh() + + def on_unhover(self): + """[summary] + """ + + if not self.hover: + return + self.hover = False + self.trigger = False + font = self.GetFont() + font.SetUnderlined(False) + self.SetFont(font) + self.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT)) + self.Refresh() + + def on_left_down(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + if self.hover: + self.trigger = True + + def on_left_up(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + if self.hover and self.trigger: + self.post_event() + + self.trigger = False + + def on_right_up(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + menu = wx.Menu() + util.menu_item(menu, 'Open Link', self.on_open_link) + util.menu_item(menu, 'Copy Link', self.on_copy_link) + self.PopupMenu(menu, event.GetPosition()) + + def on_open_link(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.post_event() + + def on_copy_link(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + if wx.TheClipboard.Open(): + wx.TheClipboard.SetData(wx.TextDataObject(self.link)) + wx.TheClipboard.Close() + + def post_event(self): + """[summary] + """ + + event = Event(self, EVT_HYPERLINK) + event.link = self.link + wx.PostEvent(self, event) + + +class BitmapLink(wx.PyPanel): + """[summary] + + Arguments: + wx {[type]} -- [description] + """ + + def __init__(self, parent, link, bitmap, hover_bitmap=None): + """[summary] + + Arguments: + parent {[type]} -- [description] + link {[type]} -- [description] + bitmap {[type]} -- [description] + + Keyword Arguments: + hover_bitmap {[type]} -- [description] (default: {None}) + """ + + super(BitmapLink, self).__init__(parent, -1) + self.link = link + self.bitmap = bitmap + self.hover_bitmap = hover_bitmap or bitmap + self.hover = False + self.trigger = False + self.SetInitialSize(bitmap.GetSize()) + self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) + self.Bind(wx.EVT_PAINT, self.on_paint) + self.Bind(wx.EVT_ENTER_WINDOW, self.on_enter) + self.Bind(wx.EVT_LEAVE_WINDOW, self.on_leave) + self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down) + self.Bind(wx.EVT_LEFT_UP, self.on_left_up) + + def on_paint(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + parent = self.GetParent() + dc = wx.AutoBufferedPaintDC(self) + dc.SetBackground(wx.Brush(parent.GetBackgroundColour())) + dc.Clear() + bitmap = self.hover_bitmap if self.hover else self.bitmap + dc.DrawBitmap(bitmap, 0, 0, True) + + def on_enter(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.hover = True + self.SetCursor(wx.StockCursor(wx.CURSOR_HAND)) + self.Refresh() + + def on_leave(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.trigger = False + self.hover = False + self.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT)) + self.Refresh() + + def on_left_down(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.trigger = True + + def on_left_up(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + if self.trigger: + event = Event(self, EVT_HYPERLINK) + event.link = self.link + wx.PostEvent(self, event) + self.trigger = False + +# EOF \ No newline at end of file diff --git a/defaults.py b/defaults.py index 6694d8b..6ef5498 100644 --- a/defaults.py +++ b/defaults.py @@ -1,57 +1,72 @@ -# Helper Functions -def load_revision(): - try: - with open('revision.txt', 'r') as file: - return int(file.read().strip()) - except Exception: - return -1 - -# Popup Settings -POPUP_DURATION = 5 -POPUP_AUTO_PLAY = True -POPUP_WAIT_ON_HOVER = True -POPUP_THEME = 'default' -POPUP_WIDTH = 400 -POPUP_POSITION = (1, 1) -POPUP_TRANSPARENCY = 230 -POPUP_TITLE_LENGTH = 120 -POPUP_BODY_LENGTH = 400 -POPUP_DISPLAY = 0 -POPUP_STAY_ON_TOP = True -POPUP_BORDER_SIZE = 3 -POPUP_BORDER_COLOR = (0, 0, 0) - -# Application Settings -APP_ID = 'FeedNotifier' -APP_NAME = 'Feed Notifier' -APP_VERSION = '2.6' -APP_URL = 'http://www.feednotifier.com/' -USER_AGENT = '%s/%s +%s' % (APP_ID, APP_VERSION, APP_URL) -DEFAULT_POLLING_INTERVAL = 60 * 15 -USER_IDLE_TIMEOUT = 60 -DISABLE_WHEN_IDLE = True -ITEM_CACHE_AGE = 60 * 60 * 24 * 1 -FEED_CACHE_SIZE = 1000 -MAX_WORKER_THREADS = 10 -PLAY_SOUND = True -SOUND_PATH = 'sounds/notification.wav' -SOCKET_TIMEOUT = 15 - -# Initial Setup -DEFAULT_FEED_URLS = [ - 'http://www.feednotifier.com/welcome.xml', -] - -# Proxy Settings -USE_PROXY = False -PROXY_URL = '' - -# Updater Settings -LOCAL_REVISION = load_revision() -REVISION_URL = 'http://www.feednotifier.com/update/revision.txt' -INSTALLER_URL = 'http://www.feednotifier.com/update/installer.exe' -CHECK_FOR_UPDATES = True -UPDATE_INTERVAL = 60 * 60 * 24 * 1 -UPDATE_TIMESTAMP = 0 - -del load_revision +# -*- coding: utf-8 -*- + +"""[summary] + +Returns: + [type] -- [description] +""" + +import logging + + +# Helper Functions +def load_revision(): + try: + with open('revision.txt', 'r') as file: + logging.info(f'Reading the file revision.txt.') + return int(file.read().strip()) + except Exception: + logging.error(f'The file revision.txt does not exist.') + return -1 + + +# Popup Settings +POPUP_DURATION = 5 +POPUP_AUTO_PLAY = True +POPUP_WAIT_ON_HOVER = True +POPUP_THEME = 'default' +POPUP_WIDTH = 400 +POPUP_POSITION = (1, 1) +POPUP_TRANSPARENCY = 230 +POPUP_TITLE_LENGTH = 120 +POPUP_BODY_LENGTH = 400 +POPUP_DISPLAY = 0 +POPUP_STAY_ON_TOP = True +POPUP_BORDER_SIZE = 3 +POPUP_BORDER_COLOR = (0, 0, 0) + +# Application Settings +APP_ID = 'FeedNotifier' +APP_NAME = 'Feed Notifier' +APP_VERSION = '2.6' +APP_URL = 'http://www.feednotifier.com/' +USER_AGENT = '%s/%s +%s' % (APP_ID, APP_VERSION, APP_URL) +DEFAULT_POLLING_INTERVAL = 60 * 15 +USER_IDLE_TIMEOUT = 60 +DISABLE_WHEN_IDLE = True +ITEM_CACHE_AGE = 60 * 60 * 24 * 1 +FEED_CACHE_SIZE = 1000 +MAX_WORKER_THREADS = 10 +PLAY_SOUND = True +SOUND_PATH = 'sounds/notification.wav' +SOCKET_TIMEOUT = 15 + +# Initial Setup +DEFAULT_FEED_URLS = [ + 'http://www.feednotifier.com/welcome.xml', + 'https://www.pic-sl.com/unycop-beta.xml', +] + +# Proxy Settings +USE_PROXY = False +PROXY_URL = '' + +# Updater Settings +LOCAL_REVISION = load_revision() +REVISION_URL = 'http://www.feednotifier.com/update/revision.txt' +INSTALLER_URL = 'http://www.feednotifier.com/update/installer.exe' +CHECK_FOR_UPDATES = True +UPDATE_INTERVAL = 60 * 60 * 24 * 1 +UPDATE_TIMESTAMP = 0 + +del load_revision diff --git a/doc/FeedNotifier.png b/doc/FeedNotifier.png new file mode 100644 index 0000000..af3400f Binary files /dev/null and b/doc/FeedNotifier.png differ diff --git a/doc/UML.png b/doc/UML.png new file mode 100644 index 0000000..144b840 Binary files /dev/null and b/doc/UML.png differ diff --git a/doc/UML.svg b/doc/UML.svg new file mode 100644 index 0000000..cca09d3 --- /dev/null +++ b/doc/UML.svg @@ -0,0 +1,39975 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/pycallgrap-FeedNotifier-Windows.bat b/doc/pycallgrap-FeedNotifier-Windows.bat new file mode 100644 index 0000000..3cd2407 --- /dev/null +++ b/doc/pycallgrap-FeedNotifier-Windows.bat @@ -0,0 +1 @@ +pycallgraph -s -t --max-depth 14 graphviz --output-file=FeedNotifier.png ..\main.py diff --git a/doc/pycallgraph_-t_--max-depth_14_graphviz_--_main.py.png b/doc/pycallgraph_-t_--max-depth_14_graphviz_--_main.py.png new file mode 100644 index 0000000..7ad4e9c Binary files /dev/null and b/doc/pycallgraph_-t_--max-depth_14_graphviz_--_main.py.png differ diff --git a/dummy.py b/dummy.py index b8fe5fe..47a453d 100644 --- a/dummy.py +++ b/dummy.py @@ -1 +1,11 @@ -# used for dummy.__file__ to setup path +# -*- coding: utf-8 -*- + +"""[summary] + +Returns: + [type] -- [description] +""" + +# used for dummy.__file__ to setup path + +# EOF \ No newline at end of file diff --git a/feeds.py b/feeds.py index ef87bb2..28eb7f3 100644 --- a/feeds.py +++ b/feeds.py @@ -1,275 +1,590 @@ -import os -import time -import calendar -import uuid -import urlparse -import urllib2 -import filters -import util -import Queue -import logging -import safe_pickle -from settings import settings - -def cmp_timestamp(a, b): - return cmp(a.timestamp, b.timestamp) - -def create_id(entry): - keys = ['id', 'link', 'title'] - values = tuple(util.get(entry, key, None) for key in keys) - return values if any(values) else uuid.uuid4().hex - -class Item(object): - def __init__(self, feed, id): - self.feed = feed - self.id = id - self.timestamp = int(time.time()) - self.received = int(time.time()) - self.title = '' - self.description = '' - self.link = '' - self.author = '' - self.read = False - @property - def time_since(self): - return util.time_since(self.timestamp) - -class Feed(object): - def __init__(self, url): - self.uuid = uuid.uuid4().hex - self.url = url - self.username = None - self.password = None - self.enabled = True - self.last_poll = 0 - self.interval = settings.DEFAULT_POLLING_INTERVAL - self.etag = None - self.modified = None - self.title = '' - self.link = '' - self.clicks = 0 - self.item_count = 0 - self.color = None - self.id_list = [] - self.id_set = set() - def make_copy(self): - feed = Feed(self.url) - for key in ['uuid', 'enabled', 'interval', 'title', 'link', 'clicks', 'item_count', 'color']: - value = getattr(self, key) - setattr(feed, key, value) - return feed - def copy_from(self, feed): - for key in ['enabled', 'interval', 'title', 'link', 'color']: - value = getattr(feed, key) - setattr(self, key, value) - @property - def favicon_url(self): - components = urlparse.urlsplit(self.link) - scheme, domain = components[:2] - return '%s://%s/favicon.ico' % (scheme, domain) - @property - def favicon_path(self): - components = urlparse.urlsplit(self.link) - scheme, domain = components[:2] - path = 'icons/cache/%s.ico' % domain - return os.path.abspath(path) - @property - def has_favicon(self): - return os.path.exists(self.favicon_path) - def download_favicon(self): - # make cache directory if needed - try: - dir, name = os.path.split(self.favicon_path) - os.makedirs(dir) - except Exception: - pass - # try to download the favicon - try: - opener = urllib2.build_opener(util.get_proxy()) - f = opener.open(self.favicon_url) - data = f.read() - f.close() - f = open(self.favicon_path, 'wb') - f.write(data) - f.close() - except Exception: - pass - def clear_cache(self): - self.id_list = [] - self.id_set = set() - self.etag = None - self.modified = None - def clean_cache(self, size): - for id in self.id_list[:-size]: - self.id_set.remove(id) - self.id_list = self.id_list[-size:] - def should_poll(self): - if not self.enabled: - return False - now = int(time.time()) - duration = now - self.last_poll - return duration >= self.interval - def poll(self, timestamp, filters): - logging.info('Polling feed "%s"' % self.url) - result = [] - self.last_poll = timestamp - username = util.decode_password(self.username) - password = util.decode_password(self.password) - d = util.parse(self.url, username, password, self.etag, self.modified) - self.etag = util.get(d, 'etag', None) - self.modified = util.get(d, 'modified', None) - feed = util.get(d, 'feed', None) - if feed: - self.title = self.title or util.get(feed, 'title', '') - self.link = self.link or util.get(feed, 'link', self.url) - entries = util.get(d, 'entries', []) - for entry in reversed(entries): - id = create_id(entry) - if id in self.id_set: - continue - self.item_count += 1 - self.id_list.append(id) - self.id_set.add(id) - item = Item(self, id) - item.timestamp = calendar.timegm(util.get(entry, 'date_parsed', time.gmtime())) - item.title = util.format(util.get(entry, 'title', ''), settings.POPUP_TITLE_LENGTH) - item.description = util.format(util.get(entry, 'description', ''), settings.POPUP_BODY_LENGTH) - item.link = util.get(entry, 'link', '') - item.author = util.format(util.get(entry, 'author', '')) # TODO: max length - if all(filter.filter(item) for filter in filters): - result.append(item) - self.clean_cache(settings.FEED_CACHE_SIZE) - return result - -class Filter(object): - def __init__(self, code, ignore_case=True, whole_word=True, feeds=None): - self.uuid = uuid.uuid4().hex - self.enabled = True - self.code = code - self.ignore_case = ignore_case - self.whole_word = whole_word - self.feeds = set(feeds) if feeds else set() - self.inputs = 0 - self.outputs = 0 - def make_copy(self): - filter = Filter(self.code, self.ignore_case, self.whole_word, self.feeds) - for key in ['uuid', 'enabled', 'inputs', 'outputs']: - value = getattr(self, key) - setattr(filter, key, value) - return filter - def copy_from(self, filter): - for key in ['enabled', 'code', 'ignore_case', 'whole_word', 'feeds']: - value = getattr(filter, key) - setattr(self, key, value) - def filter(self, item): - if not self.enabled: - return True - if self.feeds and item.feed not in self.feeds: - return True - self.inputs += 1 - rule = filters.parse(self.code) # TODO: cache parsed rules - if rule.evaluate(item, self.ignore_case, self.whole_word): - self.outputs += 1 - return True - else: - return False - -class FeedManager(object): - def __init__(self): - self.feeds = [] - self.items = [] - self.filters = [] - def add_feed(self, feed): - logging.info('Adding feed "%s"' % feed.url) - self.feeds.append(feed) - def remove_feed(self, feed): - logging.info('Removing feed "%s"' % feed.url) - self.feeds.remove(feed) - for filter in self.filters: - filter.feeds.discard(feed) - def add_filter(self, filter): - logging.info('Adding filter "%s"' % filter.code) - self.filters.append(filter) - def remove_filter(self, filter): - logging.info('Removing filter "%s"' % filter.code) - self.filters.remove(filter) - def should_poll(self): - return any(feed.should_poll() for feed in self.feeds) - def poll(self): - now = int(time.time()) - jobs = Queue.Queue() - results = Queue.Queue() - feeds = [feed for feed in self.feeds if feed.should_poll()] - for feed in feeds: - jobs.put(feed) - count = len(feeds) - logging.info('Starting worker threads') - for i in range(min(count, settings.MAX_WORKER_THREADS)): - util.start_thread(self.worker, now, jobs, results) - while count: - items = results.get() - count -= 1 - if items: - yield items - logging.info('Worker threads completed') - def worker(self, now, jobs, results): - while True: - try: - feed = jobs.get(False) - except Queue.Empty: - break - try: - items = feed.poll(now, self.filters) - items.sort(cmp=cmp_timestamp) - if items and not feed.has_favicon: - feed.download_favicon() - results.put(items) - jobs.task_done() - except Exception: - results.put([]) - jobs.task_done() - def purge_items(self, max_age): - now = int(time.time()) - feeds = set(self.feeds) - for item in list(self.items): - age = now - item.received - if age > max_age or item.feed not in feeds: - self.items.remove(item) - def load(self, path='feeds.dat'): - logging.info('Loading feed data from "%s"' % path) - try: - data = safe_pickle.load(path) - except Exception: - data = ([], [], []) - # backward compatibility - if len(data) == 2: - self.feeds, self.items = data - self.filters = [] - else: - self.feeds, self.items, self.filters = data - attributes = { - 'clicks': 0, - 'item_count': 0, - 'username': None, - 'password': None, - 'color': None, - } - for feed in self.feeds: - for name, value in attributes.iteritems(): - if not hasattr(feed, name): - setattr(feed, name, value) - if not hasattr(feed, 'id_list'): - feed.id_list = list(feed.id_set) - logging.info('Loaded %d feeds, %d items, %d filters' % (len(self.feeds), len(self.items), len(self.filters))) - def save(self, path='feeds.dat'): - logging.info('Saving feed data to "%s"' % path) - data = (self.feeds, self.items, self.filters) - safe_pickle.save(path, data) - def clear_item_history(self): - logging.info('Clearing item history') - del self.items[:] - def clear_feed_cache(self): - logging.info('Clearing feed caches') - for feed in self.feeds: - feed.clear_cache() - \ No newline at end of file +# -*- coding: utf-8 -*- + +"""[summary] + +Returns: + [type] -- [description] +""" + + +import calendar +import logging +import os +import queue +import time +import urllib.error +import urllib.parse +import urllib.request +import uuid + +import filters +import safe_pickle +import util + +from settings import settings + + +def cmp_timestamp(a, b): + """[summary] + + Arguments: + a {[type]} -- [description] + b {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + return cmp(a.timestamp, b.timestamp) + + +def create_id(entry): + """[summary] + + Arguments: + entry {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + keys = ['id', 'link', 'title'] + values = tuple(util.get(entry, key, None) for key in keys) + return values if any(values) else uuid.uuid4().hex + + +class Item(object): + """[summary] + + Arguments: + object {[type]} -- [description] + """ + + def __init__(self, feed, id): + """[summary] + + Arguments: + feed {[type]} -- [description] + id {[type]} -- [description] + """ + + self.feed = feed + self.id = id + self.timestamp = int(time.time()) + self.received = int(time.time()) + self.title = '' + self.description = '' + self.link = '' + self.author = '' + self.read = False + + @property + def time_since(self): + """[summary] + + Returns: + [type] -- [description] + """ + + return util.time_since(self.timestamp) + + +class Feed(object): + """[summary] + + Arguments: + object {[type]} -- [description] + """ + + def __init__(self, url): + """[summary] + + Arguments: + url {[type]} -- [description] + """ + + self.uuid = uuid.uuid4().hex + self.url = url + self.username = None + self.password = None + self.enabled = True + self.last_poll = 0 + self.interval = settings.DEFAULT_POLLING_INTERVAL + self.etag = None + self.modified = None + self.title = '' + self.link = '' + self.clicks = 0 + self.item_count = 0 + self.color = None + self.id_list = [] + self.id_set = set() + + def make_copy(self): + """[summary] + + Returns: + [type] -- [description] + """ + + feed = Feed(self.url) + + for key in ['uuid', 'enabled', 'interval', 'title', 'link', 'clicks', 'item_count', 'color']: + value = getattr(self, key) + setattr(feed, key, value) + + return feed + + def copy_from(self, feed): + """[summary] + + Arguments: + feed {[type]} -- [description] + """ + + for key in ['enabled', 'interval', 'title', 'link', 'color']: + value = getattr(feed, key) + setattr(self, key, value) + + @property + def favicon_url(self): + """[summary] + + Returns: + [type] -- [description] + """ + + components = urllib.parse.urlsplit(self.link) + scheme, domain = components[:2] + + return '%s://%s/favicon.ico' % (scheme, domain) + + @property + def favicon_path(self): + """[summary] + + Returns: + [type] -- [description] + """ + + components = urllib.parse.urlsplit(self.link) + scheme, domain = components[:2] + path = 'icons/cache/%s.ico' % domain + + return os.path.abspath(path) + + @property + def has_favicon(self): + """[summary] + + Returns: + [type] -- [description] + """ + + return os.path.exists(self.favicon_path) + + def download_favicon(self): + """[summary] + """ + + # make cache directory if needed + try: + dir, name = os.path.split(self.favicon_path) + os.makedirs(dir) + except Exception: + pass + + # try to download the favicon + try: + opener = urllib.request.build_opener(util.get_proxy()) + f = opener.open(self.favicon_url) + data = f.read() + f.close() + f = open(self.favicon_path, 'wb') + f.write(data) + f.close() + except Exception: + pass + + def clear_cache(self): + """[summary] + """ + + self.id_list = [] + self.id_set = set() + self.etag = None + self.modified = None + + def clean_cache(self, size): + """[summary] + + Arguments: + size {[type]} -- [description] + """ + + for id in self.id_list[:-size]: + self.id_set.remove(id) + self.id_list = self.id_list[-size:] + + def should_poll(self): + """[summary] + + Returns: + [type] -- [description] + """ + + if not self.enabled: + return False + + now = int(time.time()) + duration = now - self.last_poll + + return duration >= self.interval + + def poll(self, timestamp, filters): + """[summary] + + Arguments: + timestamp {[type]} -- [description] + filters {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + logging.info('Polling feed "%s"' % self.url) + result = [] + self.last_poll = timestamp + username = util.decode_password(self.username) + password = util.decode_password(self.password) + d = util.parse(self.url, username, password, self.etag, self.modified) + self.etag = util.get(d, 'etag', None) + self.modified = util.get(d, 'modified', None) + feed = util.get(d, 'feed', None) + + if feed: + self.title = self.title or util.get(feed, 'title', '') + self.link = self.link or util.get(feed, 'link', self.url) + + entries = util.get(d, 'entries', []) + + for entry in reversed(entries): + id = create_id(entry) + if id in self.id_set: + continue + self.item_count += 1 + self.id_list.append(id) + self.id_set.add(id) + item = Item(self, id) + item.timestamp = calendar.timegm( + util.get(entry, 'date_parsed', time.gmtime())) + item.title = util.format( + util.get(entry, 'title', ''), settings.POPUP_TITLE_LENGTH) + item.description = util.format( + util.get(entry, 'description', ''), settings.POPUP_BODY_LENGTH) + item.link = util.get(entry, 'link', '') + item.author = util.format( + util.get(entry, 'author', '')) # TODO: max length + if all(filter.filter(item) for filter in filters): + result.append(item) + + self.clean_cache(settings.FEED_CACHE_SIZE) + + return result + + +class Filter(object): + """[summary] + + Arguments: + object {[type]} -- [description] + """ + + def __init__(self, code, ignore_case=True, whole_word=True, feeds=None): + """[summary] + + Arguments: + code {[type]} -- [description] + + Keyword Arguments: + ignore_case {bool} -- [description] (default: {True}) + whole_word {bool} -- [description] (default: {True}) + feeds {[type]} -- [description] (default: {None}) + """ + + self.uuid = uuid.uuid4().hex + self.enabled = True + self.code = code + self.ignore_case = ignore_case + self.whole_word = whole_word + self.feeds = set(feeds) if feeds else set() + self.inputs = 0 + self.outputs = 0 + + def make_copy(self): + """[summary] + + Returns: + [type] -- [description] + """ + + filter = Filter(self.code, self.ignore_case, + self.whole_word, self.feeds) + + for key in ['uuid', 'enabled', 'inputs', 'outputs']: + value = getattr(self, key) + setattr(filter, key, value) + + return filter + + def copy_from(self, filter): + """[summary] + + Arguments: + filter {[type]} -- [description] + """ + + for key in ['enabled', 'code', 'ignore_case', 'whole_word', 'feeds']: + value = getattr(filter, key) + setattr(self, key, value) + + def filter(self, item): + """[summary] + + Arguments: + item {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + if not self.enabled: + return True + + if self.feeds and item.feed not in self.feeds: + return True + + self.inputs += 1 + rule = filters.parse(self.code) # TODO: cache parsed rules + + if rule.evaluate(item, self.ignore_case, self.whole_word): + self.outputs += 1 + return True + else: + return False + + +class FeedManager(object): + """[summary] + + Arguments: + object {[type]} -- [description] + """ + + def __init__(self): + """Initializing FeedManager class + """ + + logging.debug(f'Initializing FeedManager class') + + self.feeds = [] + self.items = [] + self.filters = [] + + logging.debug(f'Initialized FeedManager class') + + def add_feed(self, feed): + """[summary] + + Arguments: + feed {[type]} -- [description] + """ + + logging.info('Adding feed "%s"' % feed.url) + + self.feeds.append(feed) + + def remove_feed(self, feed): + """[summary] + + Arguments: + feed {[type]} -- [description] + """ + + logging.info('Removing feed "%s"' % feed.url) + + self.feeds.remove(feed) + + for filter in self.filters: + filter.feeds.discard(feed) + + def add_filter(self, filter): + """[summary] + + Arguments: + filter {[type]} -- [description] + """ + + logging.info('Adding new filter "%s"' % filter.code) + + self.filters.append(filter) + + def remove_filter(self, filter): + """[summary] + + Arguments: + filter {[type]} -- [description] + """ + + logging.info('Removing filter "%s"' % filter.code) + + self.filters.remove(filter) + + def should_poll(self): + """[summary] + + Returns: + [type] -- [description] + """ + + logging.info(f'Should poll each feed') + + return any(feed.should_poll() for feed in self.feeds) + + def poll(self): + """[summary] + + Yields: + [type] -- [description] + """ + + logging.debug(f'') + now = int(time.time()) + jobs = queue.Queue() + results = queue.Queue() + feeds = [feed for feed in self.feeds if feed.should_poll()] + + for feed in feeds: + jobs.put(feed) + + count = len(feeds) + logging.info('Starting worker threads') + + for i in range(min(count, settings.MAX_WORKER_THREADS)): + util.start_thread(self.worker, now, jobs, results) + + while count: + items = results.get() + count -= 1 + if items: + yield items + + logging.info('Worker threads completed') + + def worker(self, now, jobs, results): + """[summary] + + Arguments: + now {[type]} -- [description] + jobs {[type]} -- [description] + results {[type]} -- [description] + """ + + while True: + try: + feed = jobs.get(False) + except queue.Empty: + break + try: + items = feed.poll(now, self.filters) + items.sort(cmp=cmp_timestamp) + if items and not feed.has_favicon: + feed.download_favicon() + results.put(items) + jobs.task_done() + except Exception: + results.put([]) + jobs.task_done() + + def purge_items(self, max_age): + """[summary] + + Arguments: + max_age {[type]} -- [description] + """ + + now = int(time.time()) + feeds = set(self.feeds) + + for item in list(self.items): + age = now - item.received + if age > max_age or item.feed not in feeds: + self.items.remove(item) + + def load(self, path='feeds.dat'): + """[summary] + + Keyword Arguments: + path {str} -- [description] (default: {'feeds.dat'}) + """ + + logging.info('Loading feed data from "%s"' % path) + + try: + data = safe_pickle.load(path) + except Exception: + data = ([], [], []) + + # backward compatibility + if len(data) == 2: + self.feeds, self.items = data + self.filters = [] + else: + self.feeds, self.items, self.filters = data + + attributes = { + 'clicks': 0, + 'item_count': 0, + 'username': None, + 'password': None, + 'color': None, + } + + for feed in self.feeds: + for name, value in list(attributes.items()): + if not hasattr(feed, name): + setattr(feed, name, value) + if not hasattr(feed, 'id_list'): + feed.id_list = list(feed.id_set) + + logging.info('Loaded %d feeds, %d items, %d filters' % + (len(self.feeds), len(self.items), len(self.filters))) + + def save(self, path='feeds.dat'): + """[summary] + + Keyword Arguments: + path {str} -- [description] (default: {'feeds.dat'}) + """ + + logging.info('Saving feed data to "%s"' % path) + data = (self.feeds, self.items, self.filters) + safe_pickle.save(path, data) + + def clear_item_history(self): + """[summary] + """ + + logging.info('Clearing item history') + del self.items[:] + + def clear_feed_cache(self): + """[summary] + """ + + logging.info('Clearing feed caches') + + for feed in self.feeds: + feed.clear_cache() + +# EOF \ No newline at end of file diff --git a/filters.py b/filters.py index 40da2d4..7d55a4f 100644 --- a/filters.py +++ b/filters.py @@ -1,223 +1,401 @@ -# Keyword Filter Parser - -EXCLUDE = 0 -INCLUDE = 1 - -ALL = 0xf -TITLE = 1 -LINK = 2 -AUTHOR = 4 -CONTENT = 8 - -TYPES = { - None: INCLUDE, - '+': INCLUDE, - '-': EXCLUDE, -} - -QUALIFIERS = { - None: ALL, - 'title:': TITLE, - 'link:': LINK, - 'author:': AUTHOR, - 'content:': CONTENT, -} - -TYPE_STR = { - EXCLUDE: '-', - INCLUDE: '+', -} - -QUALIFIER_STR = { - ALL: 'all', - TITLE: 'title', - LINK: 'link', - AUTHOR: 'author', - CONTENT: 'content', -} - -class Rule(object): - def __init__(self, type, qualifier, word): - self.type = TYPES.get(type, type) - self.qualifier = QUALIFIERS.get(qualifier, qualifier) - self.word = word - def evaluate(self, item, ignore_case=True, whole_word=True): - strings = [] - if self.qualifier & TITLE: - strings.append(item.title) - if self.qualifier & LINK: - strings.append(item.link) - if self.qualifier & AUTHOR: - strings.append(item.author) - if self.qualifier & CONTENT: - strings.append(item.description) - text = '\n'.join(strings) - word = self.word - if ignore_case: - text = text.lower() - word = word.lower() - if whole_word: - text = set(text.split()) - if word in text: - return self.type == INCLUDE - else: - return self.type == EXCLUDE - def __str__(self): - type = TYPE_STR[self.type] - qualifier = QUALIFIER_STR[self.qualifier] - return '(%s, %s, "%s")' % (type, qualifier, self.word) - -class AndRule(object): - def __init__(self, left, right): - self.left = left - self.right = right - def evaluate(self, item, ignore_case=True, whole_word=True): - a = self.left.evaluate(item, ignore_case, whole_word) - b = self.right.evaluate(item, ignore_case, whole_word) - return a and b - def __str__(self): - return '(%s and %s)' % (self.left, self.right) - -class OrRule(object): - def __init__(self, left, right): - self.left = left - self.right = right - def evaluate(self, item, ignore_case=True, whole_word=True): - a = self.left.evaluate(item, ignore_case, whole_word) - b = self.right.evaluate(item, ignore_case, whole_word) - return a or b - def __str__(self): - return '(%s or %s)' % (self.left, self.right) - -class NotRule(object): - def __init__(self, rule): - self.rule = rule - def evaluate(self, item, ignore_case=True, whole_word=True): - return not self.rule.evaluate(item, ignore_case, whole_word) - def __str__(self): - return '(not %s)' % (self.rule) - -# Lexer Rules -reserved = { - 'and': 'AND', - 'or': 'OR', - 'not': 'NOT', -} - -tokens = [ - 'PLUS', - 'MINUS', - 'LPAREN', - 'RPAREN', - 'TITLE', - 'LINK', - 'AUTHOR', - 'CONTENT', - 'WORD', -] + reserved.values() - -t_PLUS = r'\+' -t_MINUS = r'\-' -t_LPAREN = r'\(' -t_RPAREN = r'\)' - -def t_TITLE(t): - r'title:' - return t - -def t_LINK(t): - r'link:' - return t - -def t_AUTHOR(t): - r'author:' - return t - -def t_CONTENT(t): - r'content:' - return t - -def t_WORD(t): - r'(\'[^\']+\') | (\"[^\"]+\") | ([^ \n\t\r+\-()\'"]+)' - t.type = reserved.get(t.value, 'WORD') - if t.value[0] == '"' and t.value[-1] == '"': - t.value = t.value[1:-1] - if t.value[0] == "'" and t.value[-1] == "'": - t.value = t.value[1:-1] - return t - -t_ignore = ' \n\t\r' - -def t_error(t): - raise Exception - -# Parser Rules -precedence = ( - ('left', 'OR'), - ('left', 'AND'), - ('right', 'NOT') -) - -def p_filter(t): - 'filter : expression' - t[0] = t[1] - -def p_expression_rule(t): - 'expression : rule' - t[0] = t[1] - -def p_expression_and(t): - 'expression : expression AND expression' - t[0] = AndRule(t[1], t[3]) - -def p_expression_or(t): - 'expression : expression OR expression' - t[0] = OrRule(t[1], t[3]) - -def p_expression_not(t): - 'expression : NOT expression' - t[0] = NotRule(t[2]) - -def p_expression_group(t): - 'expression : LPAREN expression RPAREN' - t[0] = t[2] - -def p_rule(t): - 'rule : type qualifier WORD' - t[0] = Rule(t[1], t[2], t[3]) - -def p_type(t): - '''type : PLUS - | MINUS - | empty''' - t[0] = t[1] - -def p_qualifier(t): - '''qualifier : TITLE - | LINK - | AUTHOR - | CONTENT - | empty''' - t[0] = t[1] - -def p_empty(t): - 'empty :' - pass - -def p_error(t): - raise Exception - -import ply.lex as lex -import ply.yacc as yacc - -lexer = lex.lex() -parser = yacc.yacc() - -def parse(text): - return parser.parse(text, lexer=lexer) - -if __name__ == '__main__': - while True: - text = raw_input('> ') - print parse(text) - \ No newline at end of file +# Keyword Filter Parser + +EXCLUDE = 0 +INCLUDE = 1 + +ALL = 0xf +TITLE = 1 +LINK = 2 +AUTHOR = 4 +CONTENT = 8 + +TYPES = { + None: INCLUDE, + '+': INCLUDE, + '-': EXCLUDE, +} + +QUALIFIERS = { + None: ALL, + 'title:': TITLE, + 'link:': LINK, + 'author:': AUTHOR, + 'content:': CONTENT, +} + +TYPE_STR = { + EXCLUDE: '-', + INCLUDE: '+', +} + +QUALIFIER_STR = { + ALL: 'all', + TITLE: 'title', + LINK: 'link', + AUTHOR: 'author', + CONTENT: 'content', +} + + +class Rule(object): + """[summary] + + Arguments: + object {[type]} -- [description] + """ + + def __init__(self, type, qualifier, word): + """[summary] + + Arguments: + type {[type]} -- [description] + qualifier {[type]} -- [description] + word {[type]} -- [description] + """ + + self.type = TYPES.get(type, type) + self.qualifier = QUALIFIERS.get(qualifier, qualifier) + self.word = word + + def evaluate(self, item, ignore_case=True, whole_word=True): + """[summary] + + Arguments: + item {[type]} -- [description] + + Keyword Arguments: + ignore_case {bool} -- [description] (default: {True}) + whole_word {bool} -- [description] (default: {True}) + + Returns: + [type] -- [description] + """ + + strings = [] + if self.qualifier & TITLE: + strings.append(item.title) + if self.qualifier & LINK: + strings.append(item.link) + if self.qualifier & AUTHOR: + strings.append(item.author) + if self.qualifier & CONTENT: + strings.append(item.description) + text = '\n'.join(strings) + word = self.word + if ignore_case: + text = text.lower() + word = word.lower() + if whole_word: + text = set(text.split()) + if word in text: + return self.type == INCLUDE + else: + return self.type == EXCLUDE + + def __str__(self): + """[summary] + + Returns: + [type] -- [description] + """ + + type = TYPE_STR[self.type] + qualifier = QUALIFIER_STR[self.qualifier] + return '(%s, %s, "%s")' % (type, qualifier, self.word) + + +class AndRule(object): + """[summary] + + Arguments: + object {[type]} -- [description] + """ + + def __init__(self, left, right): + """[summary] + + Arguments: + left {[type]} -- [description] + right {[type]} -- [description] + """ + + self.left = left + self.right = right + + def evaluate(self, item, ignore_case=True, whole_word=True): + """[summary] + + Arguments: + item {[type]} -- [description] + + Keyword Arguments: + ignore_case {bool} -- [description] (default: {True}) + whole_word {bool} -- [description] (default: {True}) + + Returns: + [type] -- [description] + """ + + a = self.left.evaluate(item, ignore_case, whole_word) + b = self.right.evaluate(item, ignore_case, whole_word) + + return a and b + + def __str__(self): + """[summary] + + Returns: + [type] -- [description] + """ + + return '(%s and %s)' % (self.left, self.right) + + +class OrRule(object): + """[summary] + + Arguments: + object {[type]} -- [description] + """ + + def __init__(self, left, right): + """[summary] + + Arguments: + left {[type]} -- [description] + right {[type]} -- [description] + """ + + self.left = left + self.right = right + + def evaluate(self, item, ignore_case=True, whole_word=True): + """[summary] + + Arguments: + item {[type]} -- [description] + + Keyword Arguments: + ignore_case {bool} -- [description] (default: {True}) + whole_word {bool} -- [description] (default: {True}) + + Returns: + [type] -- [description] + """ + + a = self.left.evaluate(item, ignore_case, whole_word) + b = self.right.evaluate(item, ignore_case, whole_word) + + return a or b + + def __str__(self): + """[summary] + + Returns: + [type] -- [description] + """ + + return '(%s or %s)' % (self.left, self.right) + + +class NotRule(object): + """[summary] + + Arguments: + object {[type]} -- [description] + """ + + def __init__(self, rule): + """[summary] + + Arguments: + rule {[type]} -- [description] + """ + + self.rule = rule + + def evaluate(self, item, ignore_case=True, whole_word=True): + """[summary] + + Arguments: + item {[type]} -- [description] + + Keyword Arguments: + ignore_case {bool} -- [description] (default: {True}) + whole_word {bool} -- [description] (default: {True}) + + Returns: + [type] -- [description] + """ + + return not self.rule.evaluate(item, ignore_case, whole_word) + + def __str__(self): + """[summary] + + Returns: + [type] -- [description] + """ + + return '(not %s)' % (self.rule) + + +# Lexer Rules +reserved = { + 'and': 'AND', + 'or': 'OR', + 'not': 'NOT', +} + +tokens = [ + 'PLUS', + 'MINUS', + 'LPAREN', + 'RPAREN', + 'TITLE', + 'LINK', + 'AUTHOR', + 'CONTENT', + 'WORD', +] + list(reserved.values()) + +t_PLUS = r'\+' +t_MINUS = r'\-' +t_LPAREN = r'\(' +t_RPAREN = r'\)' + + +def t_TITLE(t): + r'title:' + return t + + +def t_LINK(t): + r'link:' + return t + + +def t_AUTHOR(t): + r'author:' + return t + + +def t_CONTENT(t): + r'content:' + return t + + +def t_WORD(t): + r'(\'[^\']+\') | (\"[^\"]+\") | ([^ \n\t\r+\-()\'"]+)' + t.type = reserved.get(t.value, 'WORD') + if t.value[0] == '"' and t.value[-1] == '"': + t.value = t.value[1:-1] + if t.value[0] == "'" and t.value[-1] == "'": + t.value = t.value[1:-1] + return t + + +t_ignore = ' \n\t\r' + + +def t_error(t): + raise Exception + + +# Parser Rules +precedence = ( + ('left', 'OR'), + ('left', 'AND'), + ('right', 'NOT') +) + + +def p_filter(t): + 'filter : expression' + t[0] = t[1] + + +def p_expression_rule(t): + 'expression : rule' + t[0] = t[1] + + +def p_expression_and(t): + 'expression : expression AND expression' + t[0] = AndRule(t[1], t[3]) + + +def p_expression_or(t): + 'expression : expression OR expression' + t[0] = OrRule(t[1], t[3]) + + +def p_expression_not(t): + 'expression : NOT expression' + t[0] = NotRule(t[2]) + + +def p_expression_group(t): + 'expression : LPAREN expression RPAREN' + t[0] = t[2] + + +def p_rule(t): + 'rule : type qualifier WORD' + t[0] = Rule(t[1], t[2], t[3]) + + +def p_type(t): + '''type : PLUS + | MINUS + | empty''' + t[0] = t[1] + + +def p_qualifier(t): + '''qualifier : TITLE + | LINK + | AUTHOR + | CONTENT + | empty''' + t[0] = t[1] + + +def p_empty(t): + 'empty :' + pass + + +def p_error(t): + raise Exception + + +try: + import ply.lex as lex + import ply.yacc as yacc +except ModuleNotFoundError: + print("\n\tpip install ply\n") + +lexer = lex.lex() +parser = yacc.yacc() + + +def parse(text): + """[summary] + + Arguments: + text {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + return parser.parse(text, lexer=lexer) + + +if __name__ == '__main__': + while True: + text = eval(input('> ')) + print((parse(text))) + +# EOF diff --git a/idle.py b/idle.py index cc13e9e..da291c1 100644 --- a/idle.py +++ b/idle.py @@ -1,30 +1,79 @@ -import sys - -if sys.platform == 'win32': - from ctypes import * - - class LASTINPUTINFO(Structure): - _fields_ = [ - ('cbSize', c_uint), - ('dwTime', c_int), - ] - - def get_idle_duration(): - lastInputInfo = LASTINPUTINFO() - lastInputInfo.cbSize = sizeof(lastInputInfo) - if windll.user32.GetLastInputInfo(byref(lastInputInfo)): - millis = windll.kernel32.GetTickCount() - lastInputInfo.dwTime - return millis / 1000.0 - else: - return 0 -else: - def get_idle_duration(): - return 0 - -if __name__ == '__main__': - import time - while True: - duration = get_idle_duration() - print 'User idle for %.2f seconds.' % duration - time.sleep(1) - \ No newline at end of file +# -*- coding: utf-8 -*- + +"""[summary] + +Returns: + [type] -- [description] +""" + +import logging +import sys +import time + +if sys.platform.startswith('win32'): + from ctypes import * + +if sys.platform.startswith('win32'): + + # Detecting idle time using python + # https://stackoverflow.com/questions/911856/detecting-idle-time-using-python + # + # https://stackoverflow.com/questions/217157/how-can-i-determine-the-display-idle-time-from-python-in-windows-linux-and-mac?noredirect=1&lq=1 + # TODO: + + class LASTINPUTINFO(Structure): + """[summary] + + Arguments: + Structure {[type]} -- [description] + """ + + _fields_ = [ + ('cbSize', c_uint), + ('dwTime', c_int), + ] + + def get_idle_duration(): + """[summary] + + Returns: + [type] -- [description] + """ + + lastInputInfo = LASTINPUTINFO() + lastInputInfo.cbSize = sizeof(lastInputInfo) + + if windll.user32.GetLastInputInfo(byref(lastInputInfo)): + millis = windll.kernel32.GetTickCount() - lastInputInfo.dwTime + return millis / 1000.0 + else: + return 0 + +elif sys.platform.startswith('darwin'): + + print('We are in MacOX') # FIXME: ¿? + + def get_idle_duration(): + return 0 + +elif sys.platform.startswith('linux'): + + print('We are in linux') # FIXME: ¿? + + def get_idle_duration(): + return 0 + +else: + pass + + # def get_idle_duration(): + # return 0 + +if __name__ == '__main__': + + while True: + duration = get_idle_duration() + logging.debug(('User idle for %.2f seconds.' % duration)) + time.sleep(1) + +# EOF diff --git a/ipc.py b/ipc.py index b92a51f..7bbe195 100644 --- a/ipc.py +++ b/ipc.py @@ -1,102 +1,336 @@ -import wx -import sys -import util - -class CallbackContainer(object): - def __init__(self): - self.callback = None - def __call__(self, message): - if self.callback: - wx.CallAfter(self.callback, message) - -if sys.platform == 'win32': - import win32file - import win32pipe - import time - - def init(): - container = CallbackContainer() - message = '\n'.join(sys.argv[1:]) - name = r'\\.\pipe\FeedNotifier_%s' % wx.GetUserId() - if client(name, message): - return None, message - else: - util.start_thread(server, name, container) - return container, message - - def server(name, callback_func): - buffer = 4096 - timeout = 1000 - error = False - while True: - if error: - time.sleep(1) - error = False - handle = win32pipe.CreateNamedPipe( - name, - win32pipe.PIPE_ACCESS_INBOUND, - win32pipe.PIPE_TYPE_BYTE | win32pipe.PIPE_READMODE_BYTE | win32pipe.PIPE_WAIT, - win32pipe.PIPE_UNLIMITED_INSTANCES, - buffer, - buffer, - timeout, - None) - if handle == win32file.INVALID_HANDLE_VALUE: - error = True - continue - try: - if win32pipe.ConnectNamedPipe(handle) != 0: - error = True - else: - code, message = win32file.ReadFile(handle, buffer, None) - if code == 0: - callback_func(message) - else: - error = True - except Exception: - error = True - finally: - win32pipe.DisconnectNamedPipe(handle) - win32file.CloseHandle(handle) - - def client(name, message): - try: - file = open(name, 'wb') - file.write(message) - file.close() - return True - except IOError: - return False -else: - import functools - import socket - import SocketServer - - def init(): - container = CallbackContainer() - message = '\n'.join(sys.argv[1:]) - host, port = 'localhost', 31763 - try: - server(host, port, container) - return container, message - except socket.error: - client(host, port, message) - return None, message - - def server(host, port, callback_func): - class Handler(SocketServer.StreamRequestHandler): - def __init__(self, callback_func, *args, **kwargs): - self.callback_func = callback_func - SocketServer.StreamRequestHandler.__init__(self, *args, **kwargs) - def handle(self): - data = self.rfile.readline().strip() - self.callback_func(data) - server = SocketServer.TCPServer((host, port), functools.partial(Handler, callback_func)) - util.start_thread(server.serve_forever) - - def client(host, port, message): - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.connect((host, port)) - sock.send(message) - sock.close() - \ No newline at end of file +# -*- coding: utf-8 -*- + +"""[summary] + +Returns: + [type] -- [description] +""" + +import functools +import logging +import socket +import socketserver +import sys +import time + +import wx + +import util + +if sys.platform.startswith('win32'): + try: + import win32file + import win32pipe + except ModuleNotFoundError: + sys.exit("\n\tpip install pywin32 ...\n") + + +class CallbackContainer(object): + """[summary] + + Arguments: + object {[type]} -- [description] + """ + + def __init__(self): + """Initialize CallbackContainer class. + """ + + self.callback = None + logging.debug("Return callback=%s", self.callback) + + def __call__(self, message): + """Making GUI method calls from non-GUI threads. + + Any extra positional or keyword args are passed on to the callable + when it is called. + + Arguments: + message {[type]} -- [description] + """ + + if self.callback: + wx.CallAfter(self.callback, message) + + logging.debug("Launch wx.CallAfter(self.callback, message)") + + +if sys.platform.startswith('win32'): + + def init(): + """initialize the thread server + + Returns: + [type] -- [description] + """ + + logging.debug('initializing') + + container = CallbackContainer() + message = '\n'.join(sys.argv[1:]) + name = r'\\.\pipe\FeedNotifier_%s' % wx.GetUserId() + + if client(name, message): + logging.debug("Initialized: return message='%s' and name='%s'", message, name) + return None, message + else: + logging.debug("Initialized: message='%s' and name='%s'", message, name) + + logging.debug('Jump to util::start_thread()') + util.start_thread(server, name, container) + logging.debug('return of util::start_thread()') + + return container, message + + def server(name, callback_func): + """[summary] + + Arguments: + name {[type]} -- [description] + callback_func {[type]} -- [description] + """ + + buffer = 4096 + timeout = 1000 + error = False + + while True: + if error: + time.sleep(1) + error = False + + handle = win32pipe.CreateNamedPipe( + name, + win32pipe.PIPE_ACCESS_INBOUND, + win32pipe.PIPE_TYPE_BYTE | + win32pipe.PIPE_READMODE_BYTE | + win32pipe.PIPE_WAIT, + win32pipe.PIPE_UNLIMITED_INSTANCES, + buffer, + buffer, + timeout, + None) + + if handle == win32file.INVALID_HANDLE_VALUE: + error = True + continue + + try: + if win32pipe.ConnectNamedPipe(handle) != 0: + error = True + else: + code, message = win32file.ReadFile(handle, buffer, None) + if code == 0: + callback_func(message) + else: + error = True + except Exception: + error = True + finally: + win32pipe.DisconnectNamedPipe(handle) + win32file.CloseHandle(handle) + + def client(name, message): + """[summary] + + Arguments: + name {[type]} -- [description] + message {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + try: + file = open(name, 'wb') + file.write(message) + file.close() + return True + except IOError: + return False + +elif sys.platform.startswith('darwin'): + + sys.exit('\n\tdarwin platform not soported\n') + +elif sys.platform.startswith('linux'): + + # sys.exit('\n\tlinux in not suported at time...\n') + + def init(): + """initialize the thread server + + Returns: + [type] -- [description] + """ + + logging.debug(f'Initializing ipc') + + container = CallbackContainer() + # print(f"Container: {container}") # FIXME: delete this + # print(f"Container: {type(container)}") # FIXME: delete this + # print(f"Container: {container.__doc__}") # FIXME: delete this + message = '\n'.join(sys.argv[1:]) + name = r'\\.\pipe\FeedNotifier_%s' % wx.GetUserId() + + # if client(name, message): + # print('ipc::init:win32 - Existen "message" y "name"') + # print('Salimos de ipc::init()::linux') + # return None, message + # else: + # print('ipc::init:win32 - No existen "message" y "name"') + # print('Salimos de ipc::init()::linux') + # # jump to util.py + # util.start_thread(server, name, container) + # + # return container, message + + logging.debug(f'Initialized ipc') + + def server(name, callback_func): + """[summary] + + Arguments: + name {[type]} -- [description] + callback_func {[type]} -- [description] + """ + + buffer = 4096 + timeout = 1000 + error = False + + while True: + if error: + time.sleep(1) + error = False + + handle = win32pipe.CreateNamedPipe( + name, + win32pipe.PIPE_ACCESS_INBOUND, + win32pipe.PIPE_TYPE_BYTE | + win32pipe.PIPE_READMODE_BYTE | + win32pipe.PIPE_WAIT, + win32pipe.PIPE_UNLIMITED_INSTANCES, + buffer, + buffer, + timeout, + None) + + if handle == win32file.INVALID_HANDLE_VALUE: + error = True + continue + + try: + if win32pipe.ConnectNamedPipe(handle) != 0: + error = True + else: + code, message = win32file.ReadFile(handle, buffer, None) + if code == 0: + callback_func(message) + else: + error = True + except Exception: + error = True + finally: + win32pipe.DisconnectNamedPipe(handle) + win32file.CloseHandle(handle) + + def client(name, message): + """[summary] + + Arguments: + name {[type]} -- [description] + message {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + try: + file = open(name, 'wb') + file.write(message) + file.close() + return True + except IOError: + return False + + +else: + + # import functools # FIXME: delete this + # import socket # FIXME: delete this + # import socketserver # FIXME: delete this + + def init(): + """[summary] + + Returns: + [type] -- [description] + """ + + container = CallbackContainer() + message = '\n'.join(sys.argv[1:]) + host, port = 'localhost', 31763 + + try: + server(host, port, container) + return container, message + except socket.error: + client(host, port, message) + return None, message + + def server(host, port, callback_func): + """[summary] + + Arguments: + host {[type]} -- [description] + port {[type]} -- [description] + callback_func {[type]} -- [description] + """ + + class Handler(socketserver.StreamRequestHandler): + """[summary] + + Arguments: + socketserver {[type]} -- [description] + """ + + def __init__(self, callback_func, *args, **kwargs): + """[summary] + + Arguments: + callback_func {[type]} -- [description] + """ + + self.callback_func = callback_func + socketserver.StreamRequestHandler.__init__(self, + *args, + **kwargs) + + def handle(self): + """[summary] + """ + + data = self.rfile.readline().strip() + self.callback_func(data) + + server = socketserver.TCPServer((host, port), + functools.partial(Handler, + callback_func)) + util.start_thread(server.serve_forever) + + def client(host, port, message): + """[summary] + + Arguments: + host {[type]} -- [description] + port {[type]} -- [description] + message {[type]} -- [description] + """ + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((host, port)) + sock.send(message) + sock.close() + +# EOF diff --git a/main.py b/main.py index e2f2d7c..5a934e4 100644 --- a/main.py +++ b/main.py @@ -1,48 +1,138 @@ -def init_path(): - import os - import dummy - file = dummy.__file__ - file = os.path.abspath(file) - while file and not os.path.isdir(file): - file, ext = os.path.split(file) - os.chdir(file) - -def init_logging(): - import sys - import logging - logging.basicConfig( - level=logging.DEBUG, - filename='log.txt', - filemode='w', - format='%(asctime)s %(levelname)s %(message)s', - datefmt='%H:%M:%S', - ) - if not hasattr(sys, 'frozen'): - console = logging.StreamHandler(sys.stdout) - console.setLevel(logging.DEBUG) - formatter = logging.Formatter( - '%(asctime)s %(levelname)s %(message)s', - '%H:%M:%S', - ) - console.setFormatter(formatter) - logging.getLogger('').addHandler(console) - -def main(): - init_path() - init_logging() - import wx - import ipc - import controller - container, message = ipc.init() - if not container: - return - app = wx.PySimpleApp()#redirect=True, filename='log.txt') - wx.Log_SetActiveTarget(wx.LogStderr()) - ctrl = controller.Controller() - container.callback = ctrl.parse_args - container(message) - app.MainLoop() - -if __name__ == '__main__': - main() - \ No newline at end of file +# -*- coding: utf-8 -*- + +""" +docstring +""" + +# import pdb; pdb.set_trace() +# import gettext +import logging +import os +import sys + +import controller +import dummy +import ipc + +try: + import wx + # import wx.Locale + # import wx.GetTranslation +except ImportError: + sys.exit('\n\tInstall wxPython.\n') + + +APPNAME = os.path.splitext(os.path.basename(sys.argv[0]))[0] +INI_FILENAME = APPNAME + '.ini' +LOG_FILENAME = APPNAME + '.log' +LOG_LEVEL = logging.DEBUG #: Example: "DEBUG" o "WARNING" + +# languagelist = [locale.getdefaultlocale()[0], 'en_US'] +# t = gettext.translation('FeedNotifier', localedir, ['es_ES', 'en_US']) +# _ = t.ugettext +# # pygettext -d FeedNotifier main.py + + +def init_path(): + """Set the current directory. + """ + + file = dummy.__file__ + file = os.path.abspath(file) + + while file and not os.path.isdir(file): + file = os.path.split(file)[0] + os.chdir(file) + + +def init_logging(): + """[summary] + """ + + # import sys # FIXME: delete this import + # import logging + + mformat = "%(asctime)s" \ + " %(levelname)s %(module)s:%(lineno)s %(funcName)s %(message)s" + + logging.basicConfig(format=mformat, + datefmt='%Y%m%d%H%M%S', + filename=LOG_FILENAME, + level=LOG_LEVEL, + # style="{" + ) + + logging.logThreads = 0 + logging.logProcesses = 0 + + # logging.basicConfig( + # level=LOG_LEVEL, + # filename=LOG_FILENAME, + # filemode='w', + # format='%(asctime)s %(levelname)s %(message)s', + # datefmt='%H:%M:%S', + # ) + + # if not hasattr(sys, 'frozen'): + # console = logging.StreamHandler(sys.stdout) + # console.setLevel(logging.DEBUG) + # formatter = logging.Formatter( + # '%(asctime)s %(levelname)s %(message)s', + # '%H:%M:%S', + # ) + # console.setFormatter(formatter) + # logging.getLogger('').addHandler(console) + + # logging.debug('Test DEBUG') # DEBUG 10 + # logging.info('Test INFO') # INFO 20 + # logging.warning('Test WARNING') # WARNING 30 + # logging.error('Test ERROR') # ERROR 40 + # logging.critical('Test CRITICAL') # CRITICAL 50 + + +def main(): + """Start point of app. + """ + + init_path() + init_logging() + + # import sys # FIXME: delete this import + # import wx + # import ipc + # import controller + + logging.debug('-> ipc.init() - In') # FIXME: delete this + try: + container, message = ipc.init() + except TypeError: + logging.error('IPC no initialize!\nExit.') + sys.exit('Problems! IPC no initialize') + logging.debug(f'container: {container}') + logging.debug(f'message: {message}') + logging.debug('<- ipc.init() - Out') + + if not container: + logging.error('main:: The container could not be created.') + return + + logging.debug(f'Initializing wx.app class') + + # app = wx.App() # redirect=True, filename='log.txt') + app = wx.App(redirect=True, filename="log.txt", useBestVisual=True) + logging.debug(f'Initialized wx.app class') + wx.Log.SetActiveTarget(wx.LogStderr()) + logging.debug(f'Initializing wx.Log.SetActiveTarget(wx.LogStderr())') + ctrl = controller.Controller() + logging.debug(f'Initializing ctrl with controller.Controller()') + container.callback = ctrl.parse_args + logging.debug(f'Initializing container.callback with ctrl.parse_args') + container(message) + logging.debug(f'Initializing container(message)') + app.MainLoop() + + +if __name__ == '__main__': + main() + +# EOF diff --git a/parsetab.py b/parsetab.py index 9539607..4cef381 100644 --- a/parsetab.py +++ b/parsetab.py @@ -1,45 +1,56 @@ - -# parsetab.py -# This file is automatically generated. Do not edit. -_tabversion = '3.2' - -_lr_method = 'LALR' - -_lr_signature = '\x03\xd8\xc9Q1\x0e\x13W\xf5\xf7\xacu\x8b$z\xd4' - -_lr_action_items = {'AND':([2,8,16,17,20,21,22,23,],[-2,18,18,-5,-7,-6,-3,18,]),'WORD':([0,1,3,5,6,7,9,10,11,12,13,14,15,18,19,],[-16,-16,-9,-8,-16,-16,-10,20,-13,-11,-14,-12,-15,-16,-16,]),'AUTHOR':([0,1,3,5,6,7,9,18,19,],[-16,11,-9,-8,-16,-16,-10,-16,-16,]),'TITLE':([0,1,3,5,6,7,9,18,19,],[-16,12,-9,-8,-16,-16,-10,-16,-16,]),'OR':([2,8,16,17,20,21,22,23,],[-2,19,19,-5,-7,-6,-3,-4,]),'CONTENT':([0,1,3,5,6,7,9,18,19,],[-16,13,-9,-8,-16,-16,-10,-16,-16,]),'LINK':([0,1,3,5,6,7,9,18,19,],[-16,14,-9,-8,-16,-16,-10,-16,-16,]),'LPAREN':([0,6,7,18,19,],[6,6,6,6,6,]),'NOT':([0,6,7,18,19,],[7,7,7,7,7,]),'PLUS':([0,6,7,18,19,],[5,5,5,5,5,]),'$end':([2,4,8,17,20,21,22,23,],[-2,0,-1,-5,-7,-6,-3,-4,]),'MINUS':([0,6,7,18,19,],[3,3,3,3,3,]),'RPAREN':([2,16,17,20,21,22,23,],[-2,21,-5,-7,-6,-3,-4,]),} - -_lr_action = { } -for _k, _v in _lr_action_items.items(): - for _x,_y in zip(_v[0],_v[1]): - if not _x in _lr_action: _lr_action[_x] = { } - _lr_action[_x][_k] = _y -del _lr_action_items - -_lr_goto_items = {'qualifier':([1,],[10,]),'type':([0,6,7,18,19,],[1,1,1,1,1,]),'rule':([0,6,7,18,19,],[2,2,2,2,2,]),'filter':([0,],[4,]),'expression':([0,6,7,18,19,],[8,16,17,22,23,]),'empty':([0,1,6,7,18,19,],[9,15,9,9,9,9,]),} - -_lr_goto = { } -for _k, _v in _lr_goto_items.items(): - for _x,_y in zip(_v[0],_v[1]): - if not _x in _lr_goto: _lr_goto[_x] = { } - _lr_goto[_x][_k] = _y -del _lr_goto_items -_lr_productions = [ - ("S' -> filter","S'",1,None,None,None), - ('filter -> expression','filter',1,'p_filter','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',161), - ('expression -> rule','expression',1,'p_expression_rule','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',165), - ('expression -> expression AND expression','expression',3,'p_expression_and','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',169), - ('expression -> expression OR expression','expression',3,'p_expression_or','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',173), - ('expression -> NOT expression','expression',2,'p_expression_not','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',177), - ('expression -> LPAREN expression RPAREN','expression',3,'p_expression_group','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',181), - ('rule -> type qualifier WORD','rule',3,'p_rule','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',185), - ('type -> PLUS','type',1,'p_type','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',189), - ('type -> MINUS','type',1,'p_type','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',190), - ('type -> empty','type',1,'p_type','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',191), - ('qualifier -> TITLE','qualifier',1,'p_qualifier','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',195), - ('qualifier -> LINK','qualifier',1,'p_qualifier','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',196), - ('qualifier -> AUTHOR','qualifier',1,'p_qualifier','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',197), - ('qualifier -> CONTENT','qualifier',1,'p_qualifier','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',198), - ('qualifier -> empty','qualifier',1,'p_qualifier','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',199), - ('empty -> ','empty',0,'p_empty','C:\\Documents and Settings\\Michael Fogleman\\My Documents\\Workspace\\Feed Notifier 2\\filters.py',203), -] +# -*- coding: utf-8 -*- + +# parsetab.py +# This file is automatically generated. Do not edit. +# pylint: disable=W,C,R +_tabversion = '3.10' + +_lr_method = 'LALR' + +_lr_signature = 'leftORleftANDrightNOTAND AUTHOR CONTENT LINK LPAREN MINUS NOT OR PLUS RPAREN TITLE WORDfilter : expressionexpression : ruleexpression : expression AND expressionexpression : expression OR expressionexpression : NOT expressionexpression : LPAREN expression RPARENrule : type qualifier WORDtype : PLUS\n | MINUS\n | emptyqualifier : TITLE \n | LINK \n | AUTHOR \n | CONTENT\n | emptyempty :' + +_lr_action_items = {'NOT': ([0, 4, 5, 10, 11, ], [4, 4, 4, 4, 4, ]), 'LPAREN': ([0, 4, 5, 10, 11, ], [5, 5, 5, 5, 5, ]), 'PLUS': ([0, 4, 5, 10, 11, ], [7, 7, 7, 7, 7, ]), 'MINUS': ([0, 4, 5, 10, 11, ], [8, 8, 8, 8, 8, ]), 'TITLE': ([0, 4, 5, 6, 7, 8, 9, 10, 11, ], [-16, -16, -16, 15, -8, -9, -10, -16, -16, ]), 'LINK': ([0, 4, 5, 6, 7, 8, 9, 10, 11, ], [-16, -16, -16, 16, -8, -9, -10, -16, -16, ]), 'AUTHOR': ([0, 4, 5, 6, 7, 8, 9, 10, 11, ], [-16, -16, -16, 17, -8, -9, -10, -16, -16, ]), 'CONTENT': ([0, 4, 5, 6, 7, + 8, 9, 10, 11, ], [-16, -16, -16, 18, -8, -9, -10, -16, -16, ]), 'WORD': ([0, 4, 5, 6, 7, 8, 9, 10, 11, 14, 15, 16, 17, 18, 19, ], [-16, -16, -16, -16, -8, -9, -10, -16, -16, 23, -11, -12, -13, -14, -15, ]), '$end': ([1, 2, 3, 12, 20, 21, 22, 23, ], [0, -1, -2, -5, -3, -4, -6, -7, ]), 'AND': ([2, 3, 12, 13, 20, 21, 22, 23, ], [10, -2, -5, 10, -3, 10, -6, -7, ]), 'OR': ([2, 3, 12, 13, 20, 21, 22, 23, ], [11, -2, -5, 11, -3, -4, -6, -7, ]), 'RPAREN': ([3, 12, 13, 20, 21, 22, 23, ], [-2, -5, 22, -3, -4, -6, -7, ]), } + +_lr_action = {} +for _k, _v in _lr_action_items.items(): + for _x, _y in zip(_v[0], _v[1]): + if not _x in _lr_action: + _lr_action[_x] = {} + _lr_action[_x][_k] = _y +del _lr_action_items + +_lr_goto_items = {'filter': ([0, ], [1, ]), 'expression': ([0, 4, 5, 10, 11, ], [2, 12, 13, 20, 21, ]), 'rule': ([0, 4, 5, 10, 11, ], [3, 3, 3, 3, 3, ]), 'type': ( + [0, 4, 5, 10, 11, ], [6, 6, 6, 6, 6, ]), 'empty': ([0, 4, 5, 6, 10, 11, ], [9, 9, 9, 19, 9, 9, ]), 'qualifier': ([6, ], [14, ]), } + +_lr_goto = {} +for _k, _v in _lr_goto_items.items(): + for _x, _y in zip(_v[0], _v[1]): + if not _x in _lr_goto: + _lr_goto[_x] = {} + _lr_goto[_x][_k] = _y +del _lr_goto_items +_lr_productions = [ + ("S' -> filter", "S'", 1, None, None, None), + ('filter -> expression', 'filter', 1, 'p_filter', 'filters.py', 162), + ('expression -> rule', 'expression', 1, + 'p_expression_rule', 'filters.py', 166), + ('expression -> expression AND expression', + 'expression', 3, 'p_expression_and', 'filters.py', 170), + ('expression -> expression OR expression', + 'expression', 3, 'p_expression_or', 'filters.py', 174), + ('expression -> NOT expression', 'expression', + 2, 'p_expression_not', 'filters.py', 178), + ('expression -> LPAREN expression RPAREN', 'expression', + 3, 'p_expression_group', 'filters.py', 182), + ('rule -> type qualifier WORD', 'rule', 3, 'p_rule', 'filters.py', 186), + ('type -> PLUS', 'type', 1, 'p_type', 'filters.py', 190), + ('type -> MINUS', 'type', 1, 'p_type', 'filters.py', 191), + ('type -> empty', 'type', 1, 'p_type', 'filters.py', 192), + ('qualifier -> TITLE', 'qualifier', 1, 'p_qualifier', 'filters.py', 196), + ('qualifier -> LINK', 'qualifier', 1, 'p_qualifier', 'filters.py', 197), + ('qualifier -> AUTHOR', 'qualifier', 1, 'p_qualifier', 'filters.py', 198), + ('qualifier -> CONTENT', 'qualifier', 1, 'p_qualifier', 'filters.py', 199), + ('qualifier -> empty', 'qualifier', 1, 'p_qualifier', 'filters.py', 200), + ('empty -> ', 'empty', 0, 'p_empty', 'filters.py', 204), +] diff --git a/patch/build_exe.py b/patch/build_exe.py index cd3138d..3e997fa 100644 --- a/patch/build_exe.py +++ b/patch/build_exe.py @@ -1,1712 +1,1765 @@ -# Changes: -# -# can now specify 'zipfile = None', in this case the Python module -# library archive is appended to the exe. - -# Todo: -# -# Make 'unbuffered' a per-target option - -from distutils.core import Command -from distutils.spawn import spawn -from distutils.errors import * -import sys, os, imp, types, stat -import marshal -import zipfile -import sets -import tempfile -import struct -import re - -is_win64 = struct.calcsize("P") == 8 - -def _is_debug_build(): - for ext, _, _ in imp.get_suffixes(): - if ext == "_d.pyd": - return True - return False - -is_debug_build = _is_debug_build() - -if is_debug_build: - python_dll = "python%d%d_d.dll" % sys.version_info[:2] -else: - python_dll = "python%d%d.dll" % sys.version_info[:2] - -# resource constants -RT_BITMAP=2 -RT_MANIFEST=24 - -# Pattern for modifying the 'requestedExecutionLevel' in the manifest. Groups -# are setup so all text *except* for the values is matched. -pat_manifest_uac = re.compile(r'(^.*", path - mod = imp.load_dynamic(__name__, path) -## mod.frozen = 1 -__load() -del __load -""" - -# A very loosely defined "target". We assume either a "script" or "modules" -# attribute. Some attributes will be target specific. -class Target: - # A custom requestedExecutionLevel for the User Access Control portion - # of the manifest for the target. May be a string, which will be used for - # the 'requestedExecutionLevel' portion and False for 'uiAccess', or a tuple - # of (string, bool) which specifies both values. If specified and the - # target's 'template' executable has no manifest (ie, python 2.5 and - # earlier), then a default manifest is created, otherwise the manifest from - # the template is copied then updated. - uac_info = None - - def __init__(self, **kw): - self.__dict__.update(kw) - # If modules is a simple string, assume they meant list - m = self.__dict__.get("modules") - if m and type(m) in types.StringTypes: - self.modules = [m] - def get_dest_base(self): - dest_base = getattr(self, "dest_base", None) - if dest_base: return dest_base - script = getattr(self, "script", None) - if script: - return os.path.basename(os.path.splitext(script)[0]) - modules = getattr(self, "modules", None) - assert modules, "no script, modules or dest_base specified" - return modules[0].split(".")[-1] - - def validate(self): - resources = getattr(self, "bitmap_resources", []) + \ - getattr(self, "icon_resources", []) - for r_id, r_filename in resources: - if type(r_id) != type(0): - raise DistutilsOptionError, "Resource ID must be an integer" - if not os.path.isfile(r_filename): - raise DistutilsOptionError, "Resource filename '%s' does not exist" % r_filename - -def FixupTargets(targets, default_attribute): - if not targets: - return targets - ret = [] - for target_def in targets: - if type(target_def) in types.StringTypes : - # Create a default target object, with the string as the attribute - target = Target(**{default_attribute: target_def}) - else: - d = getattr(target_def, "__dict__", target_def) - if not d.has_key(default_attribute): - raise DistutilsOptionError, \ - "This target class requires an attribute '%s'" % default_attribute - target = Target(**d) - target.validate() - ret.append(target) - return ret - -class py2exe(Command): - description = "" - # List of option tuples: long name, short name (None if no short - # name), and help string. - user_options = [ - ('optimize=', 'O', - "optimization level: -O1 for \"python -O\", " - "-O2 for \"python -OO\", and -O0 to disable [default: -O0]"), - ('dist-dir=', 'd', - "directory to put final built distributions in (default is dist)"), - - ("excludes=", 'e', - "comma-separated list of modules to exclude"), - ("dll-excludes=", None, - "comma-separated list of DLLs to exclude"), - ("ignores=", None, - "comma-separated list of modules to ignore if they are not found"), - ("includes=", 'i', - "comma-separated list of modules to include"), - ("packages=", 'p', - "comma-separated list of packages to include"), - ("skip-scan=", None, - "comma-separated list of modules not to scan for imported modules"), - - ("compressed", 'c', - "create a compressed zipfile"), - - ("xref", 'x', - "create and show a module cross reference"), - - ("bundle-files=", 'b', - "bundle dlls in the zipfile or the exe. Valid levels are 1, 2, or 3 (default)"), - - ("skip-archive", None, - "do not place Python bytecode files in an archive, put them directly in the file system"), - - ("ascii", 'a', - "do not automatically include encodings and codecs"), - - ('custom-boot-script=', None, - "Python file that will be run when setting up the runtime environment"), - ] - - boolean_options = ["compressed", "xref", "ascii", "skip-archive"] - - def initialize_options (self): - self.xref =0 - self.compressed = 0 - self.unbuffered = 0 - self.optimize = 0 - self.includes = None - self.excludes = None - self.skip_scan = None - self.ignores = None - self.packages = None - self.dist_dir = None - self.dll_excludes = None - self.typelibs = None - self.bundle_files = 3 - self.skip_archive = 0 - self.ascii = 0 - self.custom_boot_script = None - - def finalize_options (self): - self.optimize = int(self.optimize) - self.excludes = fancy_split(self.excludes) - self.includes = fancy_split(self.includes) - self.skip_scan = fancy_split(self.skip_scan) - self.ignores = fancy_split(self.ignores) - self.bundle_files = int(self.bundle_files) - if self.bundle_files < 1 or self.bundle_files > 3: - raise DistutilsOptionError, \ - "bundle-files must be 1, 2, or 3, not %s" % self.bundle_files - if is_win64 and self.bundle_files < 3: - raise DistutilsOptionError, \ - "bundle-files %d not yet supported on win64" % self.bundle_files - if self.skip_archive: - if self.compressed: - raise DistutilsOptionError, \ - "can't compress when skipping archive" - if self.distribution.zipfile is None: - raise DistutilsOptionError, \ - "zipfile cannot be None when skipping archive" - # includes is stronger than excludes - for m in self.includes: - if m in self.excludes: - self.excludes.remove(m) - self.packages = fancy_split(self.packages) - self.set_undefined_options('bdist', - ('dist_dir', 'dist_dir')) - self.dll_excludes = [x.lower() for x in fancy_split(self.dll_excludes)] - - def run(self): - build = self.reinitialize_command('build') - build.run() - sys_old_path = sys.path[:] - if build.build_platlib is not None: - sys.path.insert(0, build.build_platlib) - if build.build_lib is not None: - sys.path.insert(0, build.build_lib) - try: - self._run() - finally: - sys.path = sys_old_path - - def _run(self): - self.create_directories() - self.plat_prepare() - self.fixup_distribution() - - dist = self.distribution - - # all of these contain module names - required_modules = [] - for target in dist.com_server + dist.service + dist.ctypes_com_server: - required_modules.extend(target.modules) - # and these contains file names - required_files = [target.script - for target in dist.windows + dist.console] - - mf = self.create_modulefinder() - - # These are the name of a script, but used as a module! - for f in dist.isapi: - mf.load_file(f.script) - - if self.typelibs: - print "*** generate typelib stubs ***" - from distutils.dir_util import mkpath - genpy_temp = os.path.join(self.temp_dir, "win32com", "gen_py") - mkpath(genpy_temp) - num_stubs = collect_win32com_genpy(genpy_temp, - self.typelibs, - verbose=self.verbose, - dry_run=self.dry_run) - print "collected %d stubs from %d type libraries" \ - % (num_stubs, len(self.typelibs)) - mf.load_package("win32com.gen_py", genpy_temp) - self.packages.append("win32com.gen_py") - - # monkey patching the compile builtin. - # The idea is to include the filename in the error message - orig_compile = compile - import __builtin__ - def my_compile(source, filename, *args): - try: - result = orig_compile(source, filename, *args) - except Exception, details: - raise DistutilsError, "compiling '%s' failed\n %s: %s" % \ - (filename, details.__class__.__name__, details) - return result - __builtin__.compile = my_compile - - print "*** searching for required modules ***" - self.find_needed_modules(mf, required_files, required_modules) - - print "*** parsing results ***" - py_files, extensions, builtins = self.parse_mf_results(mf) - - if self.xref: - mf.create_xref() - - print "*** finding dlls needed ***" - dlls = self.find_dlls(extensions) - self.plat_finalize(mf.modules, py_files, extensions, dlls) - dlls = [item for item in dlls - if os.path.basename(item).lower() not in self.dll_excludes] - # should we filter self.other_depends in the same way? - - print "*** create binaries ***" - self.create_binaries(py_files, extensions, dlls) - - self.fix_badmodules(mf) - - if mf.any_missing(): - print "The following modules appear to be missing" - print mf.any_missing() - - if self.other_depends: - print - print "*** binary dependencies ***" - print "Your executable(s) also depend on these dlls which are not included," - print "you may or may not need to distribute them." - print - print "Make sure you have the license if you distribute any of them, and" - print "make sure you don't distribute files belonging to the operating system." - print - for fnm in self.other_depends: - print " ", os.path.basename(fnm), "-", fnm.strip() - - def create_modulefinder(self): - from modulefinder import ReplacePackage - from py2exe.mf import ModuleFinder - ReplacePackage("_xmlplus", "xml") - return ModuleFinder(excludes=self.excludes, skip_scan=self.skip_scan) - - def fix_badmodules(self, mf): - # This dictionary maps additional builtin module names to the - # module that creates them. - # For example, 'wxPython.misc' creates a builtin module named - # 'miscc'. - builtins = {"clip_dndc": "wxPython.clip_dnd", - "cmndlgsc": "wxPython.cmndlgs", - "controls2c": "wxPython.controls2", - "controlsc": "wxPython.controls", - "eventsc": "wxPython.events", - "filesysc": "wxPython.filesys", - "fontsc": "wxPython.fonts", - "framesc": "wxPython.frames", - "gdic": "wxPython.gdi", - "imagec": "wxPython.image", - "mdic": "wxPython.mdi", - "misc2c": "wxPython.misc2", - "miscc": "wxPython.misc", - "printfwc": "wxPython.printfw", - "sizersc": "wxPython.sizers", - "stattoolc": "wxPython.stattool", - "streamsc": "wxPython.streams", - "utilsc": "wxPython.utils", - "windows2c": "wxPython.windows2", - "windows3c": "wxPython.windows3", - "windowsc": "wxPython.windows", - } - - # Somewhat hackish: change modulefinder's badmodules dictionary in place. - bad = mf.badmodules - # mf.badmodules is a dictionary mapping unfound module names - # to another dictionary, the keys of this are the module names - # importing the unknown module. For the 'miscc' module - # mentioned above, it looks like this: - # mf.badmodules["miscc"] = { "wxPython.miscc": 1 } - for name in mf.any_missing(): - if name in self.ignores: - del bad[name] - continue - mod = builtins.get(name, None) - if mod is not None: - if mod in bad[name] and bad[name] == {mod: 1}: - del bad[name] - - def find_dlls(self, extensions): - dlls = [item.__file__ for item in extensions] -## extra_path = ["."] # XXX - extra_path = [] - dlls, unfriendly_dlls, other_depends = \ - self.find_dependend_dlls(dlls, - extra_path + sys.path, - self.dll_excludes) - self.other_depends = other_depends - # dlls contains the path names of all dlls we need. - # If a dll uses a function PyImport_ImportModule (or what was it?), - # it's name is additionally in unfriendly_dlls. - for item in extensions: - if item.__file__ in dlls: - dlls.remove(item.__file__) - return dlls - - def create_directories(self): - bdist_base = self.get_finalized_command('bdist').bdist_base - self.bdist_dir = os.path.join(bdist_base, 'winexe') - - collect_name = "collect-%d.%d" % sys.version_info[:2] - self.collect_dir = os.path.abspath(os.path.join(self.bdist_dir, collect_name)) - self.mkpath(self.collect_dir) - - bundle_name = "bundle-%d.%d" % sys.version_info[:2] - self.bundle_dir = os.path.abspath(os.path.join(self.bdist_dir, bundle_name)) - self.mkpath(self.bundle_dir) - - self.temp_dir = os.path.abspath(os.path.join(self.bdist_dir, "temp")) - self.mkpath(self.temp_dir) - - self.dist_dir = os.path.abspath(self.dist_dir) - self.mkpath(self.dist_dir) - - if self.distribution.zipfile is None: - self.lib_dir = self.dist_dir - else: - self.lib_dir = os.path.join(self.dist_dir, - os.path.dirname(self.distribution.zipfile)) - self.mkpath(self.lib_dir) - - def copy_extensions(self, extensions): - print "*** copy extensions ***" - # copy the extensions to the target directory - for item in extensions: - src = item.__file__ - if self.bundle_files > 2: # don't bundle pyds and dlls - dst = os.path.join(self.lib_dir, (item.__pydfile__)) - self.copy_file(src, dst, preserve_mode=0) - self.lib_files.append(dst) - else: - # we have to preserve the packages - package = "\\".join(item.__name__.split(".")[:-1]) - if package: - dst = os.path.join(package, os.path.basename(src)) - else: - dst = os.path.basename(src) - self.copy_file(src, os.path.join(self.collect_dir, dst), preserve_mode=0) - self.compiled_files.append(dst) - - def copy_dlls(self, dlls): - # copy needed dlls where they belong. - print "*** copy dlls ***" - if self.bundle_files < 3: - self.copy_dlls_bundle_files(dlls) - return - # dlls belong into the lib_dir, except those listed in dlls_in_exedir, - # which have to go into exe_dir (pythonxy.dll, w9xpopen.exe). - for dll in dlls: - base = os.path.basename(dll) - if base.lower() in self.dlls_in_exedir: - # These special dlls cannot be in the lib directory, - # they must go into the exe directory. - dst = os.path.join(self.exe_dir, base) - else: - dst = os.path.join(self.lib_dir, base) - _, copied = self.copy_file(dll, dst, preserve_mode=0) - if not self.dry_run and copied and base.lower() == python_dll.lower(): - # If we actually copied pythonxy.dll, we have to patch it. - # - # Previously, the code did it every time, but this - # breaks if, for example, someone runs UPX over the - # dist directory. Patching an UPX'd dll seems to work - # (no error is detected when patching), but the - # resulting dll does not work anymore. - # - # The function restores the file times so - # dependencies still work correctly. - self.patch_python_dll_winver(dst) - - self.lib_files.append(dst) - - def copy_dlls_bundle_files(self, dlls): - # If dlls have to be bundled, they are copied into the - # collect_dir and will be added to the list of files to - # include in the zip archive 'self.compiled_files'. - # - # dlls listed in dlls_in_exedir have to be treated differently: - # - for dll in dlls: - base = os.path.basename(dll) - if base.lower() in self.dlls_in_exedir: - # pythonXY.dll must be bundled as resource. - # w9xpopen.exe must be copied to self.exe_dir. - if base.lower() == python_dll.lower() and self.bundle_files < 2: - dst = os.path.join(self.bundle_dir, base) - else: - dst = os.path.join(self.exe_dir, base) - _, copied = self.copy_file(dll, dst, preserve_mode=0) - if not self.dry_run and copied and base.lower() == python_dll.lower(): - # If we actually copied pythonxy.dll, we have to - # patch it. Well, since it's impossible to load - # resources from the bundled dlls it probably - # doesn't matter. - self.patch_python_dll_winver(dst) - self.lib_files.append(dst) - continue - - dst = os.path.join(self.collect_dir, os.path.basename(dll)) - self.copy_file(dll, dst, preserve_mode=0) - # Make sure they will be included into the zipfile. - self.compiled_files.append(os.path.basename(dst)) - - def create_binaries(self, py_files, extensions, dlls): - dist = self.distribution - - # byte compile the python modules into the target directory - print "*** byte compile python files ***" - self.compiled_files = byte_compile(py_files, - target_dir=self.collect_dir, - optimize=self.optimize, - force=0, - verbose=self.verbose, - dry_run=self.dry_run) - - self.lib_files = [] - self.console_exe_files = [] - self.windows_exe_files = [] - self.service_exe_files = [] - self.comserver_files = [] - - self.copy_extensions(extensions) - self.copy_dlls(dlls) - - # create the shared zipfile containing all Python modules - if dist.zipfile is None: - fd, archive_name = tempfile.mkstemp() - os.close(fd) - else: - archive_name = os.path.join(self.lib_dir, - os.path.basename(dist.zipfile)) - - arcname = self.make_lib_archive(archive_name, - base_dir=self.collect_dir, - files=self.compiled_files, - verbose=self.verbose, - dry_run=self.dry_run) - if dist.zipfile is not None: - self.lib_files.append(arcname) - - for target in self.distribution.isapi: - print "*** copy isapi support DLL ***" - # Locate the support DLL, and copy as "_script.dll", just like - # isapi itself - import isapi - src_name = is_debug_build and "PyISAPI_loader_d.dll" or \ - "PyISAPI_loader.dll" - src = os.path.join(isapi.__path__[0], src_name) - # destination name is "_{module_name}.dll" just like pyisapi does. - script_base = os.path.splitext(os.path.basename(target.script))[0] - dst = os.path.join(self.exe_dir, "_" + script_base + ".dll") - self.copy_file(src, dst, preserve_mode=0) - - if self.distribution.has_data_files(): - print "*** copy data files ***" - install_data = self.reinitialize_command('install_data') - install_data.install_dir = self.dist_dir - install_data.ensure_finalized() - install_data.run() - - self.lib_files.extend(install_data.get_outputs()) - - # build the executables - for target in dist.console: - dst = self.build_executable(target, self.get_console_template(), - arcname, target.script) - self.console_exe_files.append(dst) - for target in dist.windows: - dst = self.build_executable(target, self.get_windows_template(), - arcname, target.script) - self.windows_exe_files.append(dst) - for target in dist.service: - dst = self.build_service(target, self.get_service_template(), - arcname) - self.service_exe_files.append(dst) - - for target in dist.isapi: - dst = self.build_isapi(target, self.get_isapi_template(), arcname) - - for target in dist.com_server: - if getattr(target, "create_exe", True): - dst = self.build_comserver(target, self.get_comexe_template(), - arcname) - self.comserver_files.append(dst) - if getattr(target, "create_dll", True): - dst = self.build_comserver(target, self.get_comdll_template(), - arcname) - self.comserver_files.append(dst) - - for target in dist.ctypes_com_server: - dst = self.build_comserver(target, self.get_ctypes_comdll_template(), - arcname, boot_script="ctypes_com_server") - self.comserver_files.append(dst) - - if dist.zipfile is None: - os.unlink(arcname) - else: - if self.bundle_files < 3 or self.compressed: - arcbytes = open(arcname, "rb").read() - arcfile = open(arcname, "wb") - - if self.bundle_files < 2: # bundle pythonxy.dll also - print "Adding %s to %s" % (python_dll, arcname) - arcfile.write("") - bytes = open(os.path.join(self.bundle_dir, python_dll), "rb").read() - arcfile.write(struct.pack("i", len(bytes))) - arcfile.write(bytes) # python dll - - if self.compressed: - # prepend zlib.pyd also - zlib_file = imp.find_module("zlib")[0] - if zlib_file: - print "Adding zlib%s.pyd to %s" % (is_debug_build and "_d" or "", arcname) - arcfile.write("") - bytes = zlib_file.read() - arcfile.write(struct.pack("i", len(bytes))) - arcfile.write(bytes) # zlib.pyd - - arcfile.write(arcbytes) - -#### if self.bundle_files < 2: -#### # remove python dll from the exe_dir, since it is now bundled. -#### os.remove(os.path.join(self.exe_dir, python_dll)) - - - # for user convenience, let subclasses override the templates to use - def get_console_template(self): - return is_debug_build and "run_d.exe" or "run.exe" - - def get_windows_template(self): - return is_debug_build and "run_w_d.exe" or "run_w.exe" - - def get_service_template(self): - return is_debug_build and "run_d.exe" or "run.exe" - - def get_isapi_template(self): - return is_debug_build and "run_isapi_d.dll" or "run_isapi.dll" - - def get_comexe_template(self): - return is_debug_build and "run_w_d.exe" or "run_w.exe" - - def get_comdll_template(self): - return is_debug_build and "run_dll_d.dll" or "run_dll.dll" - - def get_ctypes_comdll_template(self): - return is_debug_build and "run_ctypes_dll_d.dll" or "run_ctypes_dll.dll" - - def fixup_distribution(self): - dist = self.distribution - - # Convert our args into target objects. - dist.com_server = FixupTargets(dist.com_server, "modules") - dist.ctypes_com_server = FixupTargets(dist.ctypes_com_server, "modules") - dist.service = FixupTargets(dist.service, "modules") - dist.windows = FixupTargets(dist.windows, "script") - dist.console = FixupTargets(dist.console, "script") - dist.isapi = FixupTargets(dist.isapi, "script") - - # make sure all targets use the same directory, this is - # also the directory where the pythonXX.dll must reside - paths = sets.Set() - for target in dist.com_server + dist.service \ - + dist.windows + dist.console + dist.isapi: - paths.add(os.path.dirname(target.get_dest_base())) - - if len(paths) > 1: - raise DistutilsOptionError, \ - "all targets must use the same directory: %s" % \ - [p for p in paths] - if paths: - exe_dir = paths.pop() # the only element - if os.path.isabs(exe_dir): - raise DistutilsOptionError, \ - "exe directory must be relative: %s" % exe_dir - self.exe_dir = os.path.join(self.dist_dir, exe_dir) - self.mkpath(self.exe_dir) - else: - # Do we allow to specify no targets? - # We can at least build a zipfile... - self.exe_dir = self.lib_dir - - def get_boot_script(self, boot_type): - # return the filename of the script to use for com servers. - thisfile = sys.modules['py2exe.build_exe'].__file__ - return os.path.join(os.path.dirname(thisfile), - "boot_" + boot_type + ".py") - - def build_comserver(self, target, template, arcname, boot_script="com_servers"): - # Build a dll and an exe executable hosting all the com - # objects listed in module_names. - # The basename of the dll/exe is the last part of the first module. - # Do we need a way to specify the name of the files to be built? - - # Setup the variables our boot script needs. - vars = {"com_module_names" : target.modules} - boot = self.get_boot_script(boot_script) - # and build it - return self.build_executable(target, template, arcname, boot, vars) - - def get_service_names(self, module_name): - # import the script with every side effect :) - __import__(module_name) - mod = sys.modules[module_name] - for name, klass in mod.__dict__.items(): - if hasattr(klass, "_svc_name_"): - break - else: - raise RuntimeError, "No services in module" - deps = () - if hasattr(klass, "_svc_deps_"): - deps = klass._svc_deps_ - return klass.__name__, klass._svc_name_, klass._svc_display_name_, deps - - def build_service(self, target, template, arcname): - # It should be possible to host many modules in a single service - - # but this is yet to be tested. - assert len(target.modules)==1, "We only support one service module" - - cmdline_style = getattr(target, "cmdline_style", "py2exe") - if cmdline_style not in ["py2exe", "pywin32", "custom"]: - raise RuntimeError, "cmdline_handler invalid" - - vars = {"service_module_names" : target.modules, - "cmdline_style": cmdline_style, - } - boot = self.get_boot_script("service") - return self.build_executable(target, template, arcname, boot, vars) - - def build_isapi(self, target, template, arcname): - target_module = os.path.splitext(os.path.basename(target.script))[0] - vars = {"isapi_module_name" : target_module, - } - return self.build_executable(target, template, arcname, None, vars) - - def build_manifest(self, target, template): - # Optionally return a manifest to be included in the target executable. - # Note for Python 2.6 and later, its *necessary* to include a manifest - # which correctly references the CRT. For earlier versions, a manifest - # is optional, and only necessary to customize things like - # Vista's User Access Control 'requestedExecutionLevel' setting, etc. - default_manifest = """ - - - - - - - - - - """ - from py2exe_util import load_resource - if os.path.splitext(template)[1]==".exe": - rid = 1 - else: - rid = 2 - try: - # Manfiests have resource type of 24, and ID of either 1 or 2. - mfest = load_resource(ensure_unicode(template), RT_MANIFEST, rid) - # we consider the manifest 'changed' as we know we clobber all - # resources including the existing manifest - so this manifest must - # get written even if we make no other changes. - changed = True - except RuntimeError: - mfest = default_manifest - # in this case the template had no existing manifest, so its - # not considered 'changed' unless we make further changes later. - changed = False - # update the manifest according to our options. - # for now a regex will do. - if target.uac_info: - changed = True - if isinstance(target.uac_info, tuple): - exec_level, ui = target.uac_info - else: - exec_level = target.uac_info - ui = False - new_lines = [] - for line in mfest.splitlines(): - repl = r'\1%s\3%s\5' % (exec_level, ui) - new_lines.append(re.sub(pat_manifest_uac, repl, line)) - mfest = "".join(new_lines) - if not changed: - return None, None - return mfest, rid - - def build_executable(self, target, template, arcname, script, vars={}): - # Build an executable for the target - # template is the exe-stub to use, and arcname is the zipfile - # containing the python modules. - from py2exe_util import add_resource, add_icon - ext = os.path.splitext(template)[1] - exe_base = target.get_dest_base() - exe_path = os.path.join(self.dist_dir, exe_base + ext) - # The user may specify a sub-directory for the exe - that's fine, we - # just specify the parent directory for the .zip - parent_levels = len(os.path.normpath(exe_base).split(os.sep))-1 - lib_leaf = self.lib_dir[len(self.dist_dir)+1:] - relative_arcname = ((".." + os.sep) * parent_levels) - if lib_leaf: relative_arcname += lib_leaf + os.sep - relative_arcname += os.path.basename(arcname) - - src = os.path.join(os.path.dirname(__file__), template) - # We want to force the creation of this file, as otherwise distutils - # will see the earlier time of our 'template' file versus the later - # time of our modified template file, and consider our old file OK. - old_force = self.force - self.force = True - self.copy_file(src, exe_path, preserve_mode=0) - self.force = old_force - - # Make sure the file is writeable... - os.chmod(exe_path, stat.S_IREAD | stat.S_IWRITE) - try: - f = open(exe_path, "a+b") - f.close() - except IOError, why: - print "WARNING: File %s could not be opened - %s" % (exe_path, why) - - # We create a list of code objects, and write it as a marshaled - # stream. The framework code then just exec's these in order. - # First is our common boot script. - boot = self.get_boot_script("common") - boot_code = compile(file(boot, "U").read(), - os.path.abspath(boot), "exec") - code_objects = [boot_code] - if self.bundle_files < 3: - code_objects.append( - compile("import zipextimporter; zipextimporter.install()", - "", "exec")) - for var_name, var_val in vars.items(): - code_objects.append( - compile("%s=%r\n" % (var_name, var_val), var_name, "exec") - ) - if self.custom_boot_script: - code_object = compile(file(self.custom_boot_script, "U").read() + "\n", - os.path.abspath(self.custom_boot_script), "exec") - code_objects.append(code_object) - if script: - code_object = compile(open(script, "U").read() + "\n", - os.path.basename(script), "exec") - code_objects.append(code_object) - code_bytes = marshal.dumps(code_objects) - - if self.distribution.zipfile is None: - relative_arcname = "" - - si = struct.pack("iiii", - 0x78563412, # a magic value, - self.optimize, - self.unbuffered, - len(code_bytes), - ) + relative_arcname + "\000" - - script_bytes = si + code_bytes + '\000\000' - self.announce("add script resource, %d bytes" % len(script_bytes)) - if not self.dry_run: - add_resource(ensure_unicode(exe_path), script_bytes, u"PYTHONSCRIPT", 1, True) - - # add the pythondll as resource, and delete in self.exe_dir - if self.bundle_files < 2 and self.distribution.zipfile is None: - # bundle pythonxy.dll - dll_path = os.path.join(self.bundle_dir, python_dll) - bytes = open(dll_path, "rb").read() - # image, bytes, lpName, lpType - - print "Adding %s as resource to %s" % (python_dll, exe_path) - add_resource(ensure_unicode(exe_path), bytes, - # for some reason, the 3. argument MUST BE UPPER CASE, - # otherwise the resource will not be found. - ensure_unicode(python_dll).upper(), 1, False) - - if self.compressed and self.bundle_files < 3 and self.distribution.zipfile is None: - zlib_file = imp.find_module("zlib")[0] - if zlib_file: - print "Adding zlib.pyd as resource to %s" % exe_path - zlib_bytes = zlib_file.read() - add_resource(ensure_unicode(exe_path), zlib_bytes, - # for some reason, the 3. argument MUST BE UPPER CASE, - # otherwise the resource will not be found. - u"ZLIB.PYD", 1, False) - - # Handle all resources specified by the target - bitmap_resources = getattr(target, "bitmap_resources", []) - for bmp_id, bmp_filename in bitmap_resources: - bmp_data = open(bmp_filename, "rb").read() - # skip the 14 byte bitmap header. - if not self.dry_run: - add_resource(ensure_unicode(exe_path), bmp_data[14:], RT_BITMAP, bmp_id, False) - icon_resources = getattr(target, "icon_resources", []) - for ico_id, ico_filename in icon_resources: - if not self.dry_run: - add_icon(ensure_unicode(exe_path), ensure_unicode(ico_filename), ico_id) - - # a manifest - mfest, mfest_id = self.build_manifest(target, src) - if mfest: - self.announce("add manifest, %d bytes" % len(mfest)) - if not self.dry_run: - add_resource(ensure_unicode(exe_path), mfest, RT_MANIFEST, mfest_id, False) - - for res_type, res_id, data in getattr(target, "other_resources", []): - if not self.dry_run: - if isinstance(res_type, basestring): - res_type = ensure_unicode(res_type) - add_resource(ensure_unicode(exe_path), data, res_type, res_id, False) - - typelib = getattr(target, "typelib", None) - if typelib is not None: - data = open(typelib, "rb").read() - add_resource(ensure_unicode(exe_path), data, u"TYPELIB", 1, False) - - self.add_versioninfo(target, exe_path) - - # Hm, this doesn't make sense with normal executables, which are - # already small (around 20 kB). - # - # But it would make sense with static build pythons, but not - # if the zipfile is appended to the exe - it will be too slow - # then (although it is a wonder it works at all in this case). - # - # Maybe it would be faster to use the frozen modules machanism - # instead of the zip-import? -## if self.compressed: -## import gc -## gc.collect() # to close all open files! -## os.system("upx -9 %s" % exe_path) - - if self.distribution.zipfile is None: - zip_data = open(arcname, "rb").read() - open(exe_path, "a+b").write(zip_data) - - return exe_path - - def add_versioninfo(self, target, exe_path): - # Try to build and add a versioninfo resource - - def get(name, md = self.distribution.metadata): - # Try to get an attribute from the target, if not defined - # there, from the distribution's metadata, or None. Note - # that only *some* attributes are allowed by distutils on - # the distribution's metadata: version, description, and - # name. - return getattr(target, name, getattr(md, name, None)) - - version = get("version") - if version is None: - return - - from py2exe.resources.VersionInfo import Version, RT_VERSION, VersionError - version = Version(version, - file_description = get("description"), - comments = get("comments"), - company_name = get("company_name"), - legal_copyright = get("copyright"), - legal_trademarks = get("trademarks"), - original_filename = os.path.basename(exe_path), - product_name = get("name"), - product_version = get("product_version") or version) - try: - bytes = version.resource_bytes() - except VersionError, detail: - self.warn("Version Info will not be included:\n %s" % detail) - return - - from py2exe_util import add_resource - add_resource(ensure_unicode(exe_path), bytes, RT_VERSION, 1, False) - - def patch_python_dll_winver(self, dll_name, new_winver = None): - from py2exe.resources.StringTables import StringTable, RT_STRING - from py2exe_util import add_resource, load_resource - from py2exe.resources.VersionInfo import RT_VERSION - - new_winver = new_winver or self.distribution.metadata.name or "py2exe" - if self.verbose: - print "setting sys.winver for '%s' to '%s'" % (dll_name, new_winver) - if self.dry_run: - return - - # We preserve the times on the file, so the dependency tracker works. - st = os.stat(dll_name) - # and as the resource functions silently fail if the open fails, - # check it explicitly. - os.chmod(dll_name, stat.S_IREAD | stat.S_IWRITE) - try: - f = open(dll_name, "a+b") - f.close() - except IOError, why: - print "WARNING: File %s could not be opened - %s" % (dll_name, why) - # We aren't smart enough to update string resources in place, so we need - # to persist other resources we care about. - unicode_name = ensure_unicode(dll_name) - - # Preserve existing version info (all versions should have this) - ver_info = load_resource(unicode_name, RT_VERSION, 1) - # Preserve an existing manifest (only python26.dll+ will have this) - try: - # Manfiests have resource type of 24, and ID of either 1 or 2. - mfest = load_resource(unicode_name, RT_MANIFEST, 2) - except RuntimeError: - mfest = None - - # Start putting the resources back, passing 'delete=True' for the first. - add_resource(unicode_name, ver_info, RT_VERSION, 1, True) - if mfest is not None: - add_resource(unicode_name, mfest, RT_MANIFEST, 2, False) - - # OK - do the strings. - s = StringTable() - # 1000 is the resource ID Python loads for its winver. - s.add_string(1000, new_winver) - for id, data in s.binary(): - add_resource(ensure_unicode(dll_name), data, RT_STRING, id, False) - - # restore the time. - os.utime(dll_name, (st[stat.ST_ATIME], st[stat.ST_MTIME])) - - def find_dependend_dlls(self, dlls, pypath, dll_excludes): - import py2exe_util - sysdir = py2exe_util.get_sysdir() - windir = py2exe_util.get_windir() - # This is the tail of the path windows uses when looking for dlls - # XXX On Windows NT, the SYSTEM directory is also searched - exedir = os.path.dirname(sys.executable) - syspath = os.environ['PATH'] - loadpath = ';'.join([exedir, sysdir, windir, syspath]) - - # Found by Duncan Booth: - # It may be possible that bin_depends needs extension modules, - # so the loadpath must be extended by our python path. - loadpath = loadpath + ';' + ';'.join(pypath) - - templates = sets.Set() - if self.distribution.console: - templates.add(self.get_console_template()) - if self.distribution.windows: - templates.add(self.get_windows_template()) - if self.distribution.service: - templates.add(self.get_service_template()) - for target in self.distribution.com_server: - if getattr(target, "create_exe", True): - templates.add(self.get_comexe_template()) - if getattr(target, "create_dll", True): - templates.add(self.get_comdll_template()) - - templates = [os.path.join(os.path.dirname(__file__), t) for t in templates] - - # We use Python.exe to track the dependencies of our run stubs ... - images = dlls + templates - - self.announce("Resolving binary dependencies:") - excludes_use = dll_excludes[:] - # The MSVCRT modules are never found when using VS2008+ - if sys.version_info > (2,6): - excludes_use.append("msvcr90.dll") - - # we add python.exe (aka sys.executable) to the list of images - # to scan for dependencies, but remove it later again from the - # results list. In this way pythonXY.dll is collected, and - # also the libraries it depends on. - alldlls, warnings, other_depends = \ - bin_depends(loadpath, images + [sys.executable], excludes_use) - alldlls.remove(sys.executable) - for dll in alldlls: - self.announce(" %s" % dll) - # ... but we don't need the exe stubs run_xxx.exe - for t in templates: - alldlls.remove(t) - - return alldlls, warnings, other_depends - # find_dependend_dlls() - - def get_hidden_imports(self): - # imports done from builtin modules in C code (untrackable by py2exe) - return {"time": ["_strptime"], -## "datetime": ["time"], - "cPickle": ["copy_reg"], - "parser": ["copy_reg"], - "codecs": ["encodings"], - - "cStringIO": ["copy_reg"], - "_sre": ["copy", "string", "sre"], - } - - def parse_mf_results(self, mf): - for name, imports in self.get_hidden_imports().items(): - if name in mf.modules.keys(): - for mod in imports: - mf.import_hook(mod) - - tcl_src_dir = tcl_dst_dir = None - if "Tkinter" in mf.modules.keys(): - import Tkinter - import _tkinter - tk = _tkinter.create() - tcl_dir = tk.call("info", "library") - tcl_src_dir = os.path.split(tcl_dir)[0] - tcl_dst_dir = os.path.join(self.lib_dir, "tcl") - - self.announce("Copying TCL files from %s..." % tcl_src_dir) - self.copy_tree(os.path.join(tcl_src_dir, "tcl%s" % _tkinter.TCL_VERSION), - os.path.join(tcl_dst_dir, "tcl%s" % _tkinter.TCL_VERSION)) - self.copy_tree(os.path.join(tcl_src_dir, "tk%s" % _tkinter.TK_VERSION), - os.path.join(tcl_dst_dir, "tk%s" % _tkinter.TK_VERSION)) - del tk, _tkinter, Tkinter - - # Retrieve modules from modulefinder - py_files = [] - extensions = [] - builtins = [] - - for item in mf.modules.values(): - # There may be __main__ modules (from mf.run_script), but - # we don't need them in the zipfile we build. - if item.__name__ == "__main__": - continue - if self.bundle_files < 3 and item.__name__ in ("pythoncom", "pywintypes"): - # these are handled specially in zipextimporter. - continue - src = item.__file__ - if src: - base, ext = os.path.splitext(src) - suffix = ext - if sys.platform.startswith("win") and ext in [".dll", ".pyd"] \ - and base.endswith("_d"): - suffix = "_d" + ext - - if suffix in _py_suffixes: - py_files.append(item) - elif suffix in _c_suffixes: - extensions.append(item) - if not self.bundle_files < 3: - loader = self.create_loader(item) - if loader: - py_files.append(loader) - else: - raise RuntimeError \ - ("Don't know how to handle '%s'" % repr(src)) - else: - builtins.append(item.__name__) - - # sort on the file names, the output is nicer to read - py_files.sort(lambda a, b: cmp(a.__file__, b.__file__)) - extensions.sort(lambda a, b: cmp(a.__file__, b.__file__)) - builtins.sort() - return py_files, extensions, builtins - - def plat_finalize(self, modules, py_files, extensions, dlls): - # platform specific code for final adjustments to the file - # lists - if sys.platform == "win32": - # pythoncom and pywintypes are imported via LoadLibrary calls, - # help py2exe to include the dlls: - if "pythoncom" in modules.keys(): - import pythoncom - dlls.add(pythoncom.__file__) - if "pywintypes" in modules.keys(): - import pywintypes - dlls.add(pywintypes.__file__) - self.copy_w9xpopen(modules, dlls) - else: - raise DistutilsError, "Platform %s not yet implemented" % sys.platform - - def copy_w9xpopen(self, modules, dlls): - # Using popen requires (on Win9X) the w9xpopen.exe helper executable. - if "os" in modules.keys() or "popen2" in modules.keys(): - if is_debug_build: - fname = os.path.join(os.path.dirname(sys.executable), "w9xpopen_d.exe") - else: - fname = os.path.join(os.path.dirname(sys.executable), "w9xpopen.exe") - # Don't copy w9xpopen.exe if it doesn't exist (64-bit - # Python build, for example) - if os.path.exists(fname): - dlls.add(fname) - - def create_loader(self, item): - # Hm, how to avoid needless recreation of this file? - pathname = os.path.join(self.temp_dir, "%s.py" % item.__name__) - if self.bundle_files > 2: # don't bundle pyds and dlls - # all dlls are copied into the same directory, so modify - # names to include the package name to avoid name - # conflicts and tuck it away for future reference - fname = item.__name__ + os.path.splitext(item.__file__)[1] - item.__pydfile__ = fname - else: - fname = os.path.basename(item.__file__) - - # and what about dry_run? - if self.verbose: - print "creating python loader for extension '%s' (%s -> %s)" % (item.__name__,item.__file__,fname) - - source = LOADER % fname - if not self.dry_run: - open(pathname, "w").write(source) - else: - return None - from modulefinder import Module - return Module(item.__name__, pathname) - - def plat_prepare(self): - self.includes.append("warnings") # needed by Python itself - if not self.ascii: - self.packages.append("encodings") - self.includes.append("codecs") - if self.bundle_files < 3: - self.includes.append("zipextimporter") - self.excludes.append("_memimporter") # builtin in run_*.exe and run_*.dll - if self.compressed: - self.includes.append("zlib") - - # os.path will never be found ;-) - self.ignores.append('os.path') - - # update the self.ignores list to ignore platform specific - # modules. - if sys.platform == "win32": - self.ignores += ['AL', - 'Audio_mac', - 'Carbon.File', - 'Carbon.Folder', - 'Carbon.Folders', - 'EasyDialogs', - 'MacOS', - 'Mailman', - 'SOCKS', - 'SUNAUDIODEV', - '_dummy_threading', - '_emx_link', - '_xmlplus', - '_xmlrpclib', - 'al', - 'bundlebuilder', - 'ce', - 'cl', - 'dbm', - 'dos', - 'fcntl', - 'gestalt', - 'grp', - 'ic', - 'java.lang', - 'mac', - 'macfs', - 'macostools', - 'mkcwproject', - 'org.python.core', - 'os.path', - 'os2', - 'poll', - 'posix', - 'pwd', - 'readline', - 'riscos', - 'riscosenviron', - 'riscospath', - 'rourl2path', - 'sgi', - 'sgmlop', - 'sunaudiodev', - 'termios', - 'vms_lib'] - # special dlls which must be copied to the exe_dir, not the lib_dir - self.dlls_in_exedir = [python_dll, - "w9xpopen%s.exe" % (is_debug_build and "_d" or ""), - "msvcr71%s.dll" % (is_debug_build and "d" or "")] - else: - raise DistutilsError, "Platform %s not yet implemented" % sys.platform - - def find_needed_modules(self, mf, files, modules): - # feed Modulefinder with everything, and return it. - for mod in modules: - mf.import_hook(mod) - - for path in files: - mf.run_script(path) - - mf.run_script(self.get_boot_script("common")) - - if self.distribution.com_server: - mf.run_script(self.get_boot_script("com_servers")) - - if self.distribution.ctypes_com_server: - mf.run_script(self.get_boot_script("ctypes_com_server")) - - if self.distribution.service: - mf.run_script(self.get_boot_script("service")) - - if self.custom_boot_script: - mf.run_script(self.custom_boot_script) - - for mod in self.includes: - if mod[-2:] == '.*': - mf.import_hook(mod[:-2], None, ['*']) - else: - mf.import_hook(mod) - - for f in self.packages: - def visit(arg, dirname, names): - if '__init__.py' in names: - arg.append(dirname) - - # Try to find the package using ModuleFinders's method to - # allow for modulefinder.AddPackagePath interactions - mf.import_hook(f) - - # If modulefinder has seen a reference to the package, then - # we prefer to believe that (imp_find_module doesn't seem to locate - # sub-packages) - if f in mf.modules: - module = mf.modules[f] - if module.__path__ is None: - # it's a module, not a package, so paths contains just the - # file entry - paths = [module.__file__] - else: - # it is a package because __path__ is available. __path__ - # is actually a list of paths that are searched to import - # sub-modules and sub-packages - paths = module.__path__ - else: - # Find path of package - try: - paths = [imp_find_module(f)[1]] - except ImportError: - self.warn("No package named %s" % f) - continue - - packages = [] - for path in paths: - # walk the path to find subdirs containing __init__.py files - os.path.walk(path, visit, packages) - - # scan the results (directory of __init__.py files) - # first trim the path (of the head package), - # then convert directory name in package name, - # finally push into modulefinder. - for p in packages: - if p.startswith(path): - package = f + '.' + p[len(path)+1:].replace('\\', '.') - mf.import_hook(package, None, ["*"]) - - return mf - - def make_lib_archive(self, zip_filename, base_dir, files, - verbose=0, dry_run=0): - from distutils.dir_util import mkpath - if not self.skip_archive: - # Like distutils "make_archive", but we can specify the files - # to include, and the compression to use - default is - # ZIP_STORED to keep the runtime performance up. Also, we - # don't append '.zip' to the filename. - mkpath(os.path.dirname(zip_filename), dry_run=dry_run) - - if self.compressed: - compression = zipfile.ZIP_DEFLATED - else: - compression = zipfile.ZIP_STORED - - if not dry_run: - z = zipfile.ZipFile(zip_filename, "w", - compression=compression) - for f in files: - z.write(os.path.join(base_dir, f), f) - z.close() - - return zip_filename - else: - # Don't really produce an archive, just copy the files. - from distutils.file_util import copy_file - - destFolder = os.path.dirname(zip_filename) - - for f in files: - d = os.path.dirname(f) - if d: - mkpath(os.path.join(destFolder, d), verbose=verbose, dry_run=dry_run) - copy_file( - os.path.join(base_dir, f), - os.path.join(destFolder, f), - preserve_mode=0, - verbose=verbose, - dry_run=dry_run - ) - return '.' - - -################################################################ - -class FileSet: - # A case insensitive but case preserving set of files - def __init__(self, iterable=None): - self._dict = {} - if iterable is not None: - for arg in iterable: - self.add(arg) - - def __repr__(self): - return "" % (self._dict.values(), id(self)) - - def add(self, fname): - self._dict[fname.upper()] = fname - - def remove(self, fname): - del self._dict[fname.upper()] - - def __contains__(self, fname): - return fname.upper() in self._dict.keys() - - def __getitem__(self, index): - key = self._dict.keys()[index] - return self._dict[key] - - def __len__(self): - return len(self._dict) - - def copy(self): - res = FileSet() - res._dict.update(self._dict) - return res - -# class FileSet() - -def bin_depends(path, images, excluded_dlls): - import py2exe_util - warnings = FileSet() - images = FileSet(images) - dependents = FileSet() - others = FileSet() - while images: - for image in images.copy(): - images.remove(image) - if not image in dependents: - dependents.add(image) - abs_image = os.path.abspath(image) - loadpath = os.path.dirname(abs_image) + ';' + path - for result in py2exe_util.depends(image, loadpath).items(): - dll, uses_import_module = result - if os.path.basename(dll).lower() not in excluded_dlls: - if isSystemDLL(dll): - others.add(dll) - continue - if dll not in images and dll not in dependents: - images.add(dll) - if uses_import_module: - warnings.add(dll) - return dependents, warnings, others - -# DLLs to be excluded -# XXX This list is NOT complete (it cannot be) -# Note: ALL ENTRIES MUST BE IN LOWER CASE! -EXCLUDED_DLLS = ( - "advapi32.dll", - "comctl32.dll", - "comdlg32.dll", - "crtdll.dll", - "gdi32.dll", - "glu32.dll", - "opengl32.dll", - "imm32.dll", - "kernel32.dll", - "mfc42.dll", - "msvcirt.dll", - "msvcrt.dll", - "msvcrtd.dll", - "ntdll.dll", - "odbc32.dll", - "ole32.dll", - "oleaut32.dll", - "rpcrt4.dll", - "shell32.dll", - "shlwapi.dll", - "user32.dll", - "version.dll", - "winmm.dll", - "winspool.drv", - "ws2_32.dll", - "ws2help.dll", - "wsock32.dll", - "netapi32.dll", - - "gdiplus.dll", - ) - -# XXX Perhaps it would be better to assume dlls from the systemdir are system dlls, -# and make some exceptions for known dlls, like msvcr71, pythonXY.dll, and so on? -def isSystemDLL(pathname): - if os.path.basename(pathname).lower() in ("msvcr71.dll", "msvcr71d.dll"): - return 0 - if os.path.basename(pathname).lower() in EXCLUDED_DLLS: - return 1 - # How can we determine whether a dll is a 'SYSTEM DLL'? - # Is it sufficient to use the Image Load Address? - import struct - file = open(pathname, "rb") - if file.read(2) != "MZ": - raise Exception, "Seems not to be an exe-file" - file.seek(0x3C) - pe_ofs = struct.unpack("i", file.read(4))[0] - file.seek(pe_ofs) - if file.read(4) != "PE\000\000": - raise Exception, ("Seems not to be an exe-file", pathname) - file.read(20 + 28) # COFF File Header, offset of ImageBase in Optional Header - imagebase = struct.unpack("I", file.read(4))[0] - return not (imagebase < 0x70000000) - -def byte_compile(py_files, optimize=0, force=0, - target_dir=None, verbose=1, dry_run=0, - direct=None): - - if direct is None: - direct = (__debug__ and optimize == 0) - - # "Indirect" byte-compilation: write a temporary script and then - # run it with the appropriate flags. - if not direct: - from tempfile import mktemp - from distutils.util import execute - script_name = mktemp(".py") - if verbose: - print "writing byte-compilation script '%s'" % script_name - if not dry_run: - script = open(script_name, "w") - script.write("""\ -from py2exe.build_exe import byte_compile -from modulefinder import Module -files = [ -""") - - for f in py_files: - script.write("Module(%s, %s, %s),\n" % \ - (`f.__name__`, `f.__file__`, `f.__path__`)) - script.write("]\n") - script.write(""" -byte_compile(files, optimize=%s, force=%s, - target_dir=%s, - verbose=%s, dry_run=0, - direct=1) -""" % (`optimize`, `force`, `target_dir`, `verbose`)) - - script.close() - - cmd = [sys.executable, script_name] - if optimize == 1: - cmd.insert(1, "-O") - elif optimize == 2: - cmd.insert(1, "-OO") - spawn(cmd, verbose=verbose, dry_run=dry_run) - execute(os.remove, (script_name,), "removing %s" % script_name, - verbose=verbose, dry_run=dry_run) - - - else: - from py_compile import compile - from distutils.dir_util import mkpath - from distutils.dep_util import newer - from distutils.file_util import copy_file - - for file in py_files: - # Terminology from the py_compile module: - # cfile - byte-compiled file - # dfile - purported source filename (same as 'file' by default) - cfile = file.__name__.replace('.', '\\') - - if file.__path__: - dfile = cfile + '\\__init__.py' + (__debug__ and 'c' or 'o') - else: - dfile = cfile + '.py' + (__debug__ and 'c' or 'o') - if target_dir: - cfile = os.path.join(target_dir, dfile) - - if force or newer(file.__file__, cfile): - if verbose: - print "byte-compiling %s to %s" % (file.__file__, dfile) - if not dry_run: - mkpath(os.path.dirname(cfile)) - suffix = os.path.splitext(file.__file__)[1] - if suffix in (".py", ".pyw"): - compile(file.__file__, cfile, dfile) - elif suffix in _py_suffixes: - # Minor problem: This will happily copy a file - # .pyo to .pyc or .pyc to - # .pyo, but it does seem to work. - copy_file(file.__file__, cfile, preserve_mode=0) - else: - raise RuntimeError \ - ("Don't know how to handle %r" % file.__file__) - else: - if verbose: - print "skipping byte-compilation of %s to %s" % \ - (file.__file__, dfile) - compiled_files = [] - for file in py_files: - cfile = file.__name__.replace('.', '\\') - - if file.__path__: - dfile = cfile + '\\__init__.py' + (optimize and 'o' or 'c') - else: - dfile = cfile + '.py' + (optimize and 'o' or 'c') - compiled_files.append(dfile) - return compiled_files - -# byte_compile() - -# win32com makepy helper. -def collect_win32com_genpy(path, typelibs, verbose=0, dry_run=0): - import win32com - from win32com.client import gencache, makepy - from distutils.file_util import copy_file - - old_gen_path = win32com.__gen_path__ - num = 0 - try: - win32com.__gen_path__ = path - win32com.gen_py.__path__ = [path] - gencache.__init__() - for info in typelibs: - guid, lcid, major, minor = info[:4] - # They may provide an input filename in the tuple - in which case - # they will have pre-generated it on a machine with the typelibs - # installed, and just want us to include it. - fname_in = None - if len(info) > 4: - fname_in = info[4] - if fname_in is not None: - base = gencache.GetGeneratedFileName(guid, lcid, major, minor) - fname_out = os.path.join(path, base) + ".py" - copy_file(fname_in, fname_out, verbose=verbose, dry_run=dry_run) - num += 1 - # That's all we gotta do! - continue - - # It seems bForDemand=True generates code which is missing - # at least sometimes an import of DispatchBaseClass. - # Until this is resolved, set it to false. - # What's the purpose of bForDemand=True? Thomas - # bForDemand is supposed to only generate stubs when each - # individual object is referenced. A side-effect of that is - # that each object gets its own source file. The intent of - # this code was to set bForDemand=True, meaning we get the - # 'file per object' behaviour, but then explicitly walk all - # children forcing them to be built - so the entire object model - # is included, but not in a huge .pyc. - # I'm not sure why its not working :) I'll debug later. - # bForDemand=False isn't really important here - the overhead for - # monolithic typelib stubs is in the compilation, not the loading - # of an existing .pyc. Mark. -## makepy.GenerateFromTypeLibSpec(info, bForDemand = True) - tlb_info = (guid, lcid, major, minor) - makepy.GenerateFromTypeLibSpec(tlb_info, bForDemand = False) - # Now get the module, and build all sub-modules. - mod = gencache.GetModuleForTypelib(*tlb_info) - for clsid, name in mod.CLSIDToPackageMap.items(): - try: - gencache.GetModuleForCLSID(clsid) - num += 1 - #print "", name - except ImportError: - pass - return num - finally: - # restore win32com, just in case. - win32com.__gen_path__ = old_gen_path - win32com.gen_py.__path__ = [old_gen_path] - gencache.__init__() - -# utilities hacked from distutils.dir_util - -def _chmod(file): - os.chmod(file, 0777) - -# Helper for force_remove_tree() -def _build_cmdtuple(path, cmdtuples): - for f in os.listdir(path): - real_f = os.path.join(path,f) - if os.path.isdir(real_f) and not os.path.islink(real_f): - _build_cmdtuple(real_f, cmdtuples) - else: - cmdtuples.append((_chmod, real_f)) - cmdtuples.append((os.remove, real_f)) - cmdtuples.append((os.rmdir, path)) - -def force_remove_tree (directory, verbose=0, dry_run=0): - """Recursively remove an entire directory tree. Any errors are ignored - (apart from being reported to stdout if 'verbose' is true). - """ - import distutils - from distutils.util import grok_environment_error - _path_created = distutils.dir_util._path_created - - if verbose: - print "removing '%s' (and everything under it)" % directory - if dry_run: - return - cmdtuples = [] - _build_cmdtuple(directory, cmdtuples) - for cmd in cmdtuples: - try: - cmd[0](cmd[1]) - # remove dir from cache if it's already there - abspath = os.path.abspath(cmd[1]) - if _path_created.has_key(abspath): - del _path_created[abspath] - except (IOError, OSError), exc: - if verbose: - print grok_environment_error( - exc, "error removing %s: " % directory) +# Changes: +# +# can now specify 'zipfile = None', in this case the Python module +# library archive is appended to the exe. + +# Todo: +# +# Make 'unbuffered' a per-target option + +from distutils.core import Command +from distutils.spawn import spawn +from distutils.errors import * +import sys +import os +import imp +import types +import stat +import marshal +import zipfile +import sets +import tempfile +import struct +import re + +is_win64 = struct.calcsize("P") == 8 + + +def _is_debug_build(): + for ext, _, _ in imp.get_suffixes(): + if ext == "_d.pyd": + return True + return False + + +is_debug_build = _is_debug_build() + +if is_debug_build: + python_dll = "python%d%d_d.dll" % sys.version_info[:2] +else: + python_dll = "python%d%d.dll" % sys.version_info[:2] + +# resource constants +RT_BITMAP = 2 +RT_MANIFEST = 24 + +# Pattern for modifying the 'requestedExecutionLevel' in the manifest. Groups +# are setup so all text *except* for the values is matched. +pat_manifest_uac = re.compile( + r'(^.*", path + mod = imp.load_dynamic(__name__, path) +## mod.frozen = 1 +__load() +del __load +""" + +# A very loosely defined "target". We assume either a "script" or "modules" +# attribute. Some attributes will be target specific. + + +class Target: + # A custom requestedExecutionLevel for the User Access Control portion + # of the manifest for the target. May be a string, which will be used for + # the 'requestedExecutionLevel' portion and False for 'uiAccess', or a tuple + # of (string, bool) which specifies both values. If specified and the + # target's 'template' executable has no manifest (ie, python 2.5 and + # earlier), then a default manifest is created, otherwise the manifest from + # the template is copied then updated. + uac_info = None + + def __init__(self, **kw): + self.__dict__.update(kw) + # If modules is a simple string, assume they meant list + m = self.__dict__.get("modules") + if m and type(m) in (str,): + self.modules = [m] + + def get_dest_base(self): + dest_base = getattr(self, "dest_base", None) + if dest_base: + return dest_base + script = getattr(self, "script", None) + if script: + return os.path.basename(os.path.splitext(script)[0]) + modules = getattr(self, "modules", None) + assert modules, "no script, modules or dest_base specified" + return modules[0].split(".")[-1] + + def validate(self): + resources = getattr(self, "bitmap_resources", []) + \ + getattr(self, "icon_resources", []) + for r_id, r_filename in resources: + if type(r_id) != type(0): + raise DistutilsOptionError("Resource ID must be an integer") + if not os.path.isfile(r_filename): + raise DistutilsOptionError( + "Resource filename '%s' does not exist" % r_filename) + + +def FixupTargets(targets, default_attribute): + if not targets: + return targets + ret = [] + for target_def in targets: + if type(target_def) in (str,): + # Create a default target object, with the string as the attribute + target = Target(**{default_attribute: target_def}) + else: + d = getattr(target_def, "__dict__", target_def) + if default_attribute not in d: + raise DistutilsOptionError( + "This target class requires an attribute '%s'" % default_attribute) + target = Target(**d) + target.validate() + ret.append(target) + return ret + + +class py2exe(Command): + description = "" + # List of option tuples: long name, short name (None if no short + # name), and help string. + user_options = [ + ('optimize=', 'O', + "optimization level: -O1 for \"python -O\", " + "-O2 for \"python -OO\", and -O0 to disable [default: -O0]"), + ('dist-dir=', 'd', + "directory to put final built distributions in (default is dist)"), + + ("excludes=", 'e', + "comma-separated list of modules to exclude"), + ("dll-excludes=", None, + "comma-separated list of DLLs to exclude"), + ("ignores=", None, + "comma-separated list of modules to ignore if they are not found"), + ("includes=", 'i', + "comma-separated list of modules to include"), + ("packages=", 'p', + "comma-separated list of packages to include"), + ("skip-scan=", None, + "comma-separated list of modules not to scan for imported modules"), + + ("compressed", 'c', + "create a compressed zipfile"), + + ("xref", 'x', + "create and show a module cross reference"), + + ("bundle-files=", 'b', + "bundle dlls in the zipfile or the exe. Valid levels are 1, 2, or 3 (default)"), + + ("skip-archive", None, + "do not place Python bytecode files in an archive, put them directly in the file system"), + + ("ascii", 'a', + "do not automatically include encodings and codecs"), + + ('custom-boot-script=', None, + "Python file that will be run when setting up the runtime environment"), + ] + + boolean_options = ["compressed", "xref", "ascii", "skip-archive"] + + def initialize_options(self): + self.xref = 0 + self.compressed = 0 + self.unbuffered = 0 + self.optimize = 0 + self.includes = None + self.excludes = None + self.skip_scan = None + self.ignores = None + self.packages = None + self.dist_dir = None + self.dll_excludes = None + self.typelibs = None + self.bundle_files = 3 + self.skip_archive = 0 + self.ascii = 0 + self.custom_boot_script = None + + def finalize_options(self): + self.optimize = int(self.optimize) + self.excludes = fancy_split(self.excludes) + self.includes = fancy_split(self.includes) + self.skip_scan = fancy_split(self.skip_scan) + self.ignores = fancy_split(self.ignores) + self.bundle_files = int(self.bundle_files) + if self.bundle_files < 1 or self.bundle_files > 3: + raise DistutilsOptionError( + "bundle-files must be 1, 2, or 3, not %s" % self.bundle_files) + if is_win64 and self.bundle_files < 3: + raise DistutilsOptionError( + "bundle-files %d not yet supported on win64" % self.bundle_files) + if self.skip_archive: + if self.compressed: + raise DistutilsOptionError( + "can't compress when skipping archive") + if self.distribution.zipfile is None: + raise DistutilsOptionError( + "zipfile cannot be None when skipping archive") + # includes is stronger than excludes + for m in self.includes: + if m in self.excludes: + self.excludes.remove(m) + self.packages = fancy_split(self.packages) + self.set_undefined_options('bdist', + ('dist_dir', 'dist_dir')) + self.dll_excludes = [x.lower() for x in fancy_split(self.dll_excludes)] + + def run(self): + build = self.reinitialize_command('build') + build.run() + sys_old_path = sys.path[:] + if build.build_platlib is not None: + sys.path.insert(0, build.build_platlib) + if build.build_lib is not None: + sys.path.insert(0, build.build_lib) + try: + self._run() + finally: + sys.path = sys_old_path + + def _run(self): + self.create_directories() + self.plat_prepare() + self.fixup_distribution() + + dist = self.distribution + + # all of these contain module names + required_modules = [] + for target in dist.com_server + dist.service + dist.ctypes_com_server: + required_modules.extend(target.modules) + # and these contains file names + required_files = [target.script + for target in dist.windows + dist.console] + + mf = self.create_modulefinder() + + # These are the name of a script, but used as a module! + for f in dist.isapi: + mf.load_file(f.script) + + if self.typelibs: + print("*** generate typelib stubs ***") + from distutils.dir_util import mkpath + genpy_temp = os.path.join(self.temp_dir, "win32com", "gen_py") + mkpath(genpy_temp) + num_stubs = collect_win32com_genpy(genpy_temp, + self.typelibs, + verbose=self.verbose, + dry_run=self.dry_run) + print(("collected %d stubs from %d type libraries" + % (num_stubs, len(self.typelibs)))) + mf.load_package("win32com.gen_py", genpy_temp) + self.packages.append("win32com.gen_py") + + # monkey patching the compile builtin. + # The idea is to include the filename in the error message + orig_compile = compile + import builtins + + def my_compile(source, filename, *args): + try: + result = orig_compile(source, filename, *args) + except Exception as details: + raise DistutilsError("compiling '%s' failed\n %s: %s" % + (filename, details.__class__.__name__, details)) + return result + builtins.compile = my_compile + + print("*** searching for required modules ***") + self.find_needed_modules(mf, required_files, required_modules) + + print("*** parsing results ***") + py_files, extensions, builtins = self.parse_mf_results(mf) + + if self.xref: + mf.create_xref() + + print("*** finding dlls needed ***") + dlls = self.find_dlls(extensions) + self.plat_finalize(mf.modules, py_files, extensions, dlls) + dlls = [item for item in dlls + if os.path.basename(item).lower() not in self.dll_excludes] + # should we filter self.other_depends in the same way? + + print("*** create binaries ***") + self.create_binaries(py_files, extensions, dlls) + + self.fix_badmodules(mf) + + if mf.any_missing(): + print("The following modules appear to be missing") + print((mf.any_missing())) + + if self.other_depends: + print() + print("*** binary dependencies ***") + print("Your executable(s) also depend on these dlls which are not included,") + print("you may or may not need to distribute them.") + print() + print("Make sure you have the license if you distribute any of them, and") + print( + "make sure you don't distribute files belonging to the operating system.") + print() + for fnm in self.other_depends: + print((" ", os.path.basename(fnm), "-", fnm.strip())) + + def create_modulefinder(self): + from modulefinder import ReplacePackage + from py2exe.mf import ModuleFinder + ReplacePackage("_xmlplus", "xml") + return ModuleFinder(excludes=self.excludes, skip_scan=self.skip_scan) + + def fix_badmodules(self, mf): + # This dictionary maps additional builtin module names to the + # module that creates them. + # For example, 'wxPython.misc' creates a builtin module named + # 'miscc'. + builtins = {"clip_dndc": "wxPython.clip_dnd", + "cmndlgsc": "wxPython.cmndlgs", + "controls2c": "wxPython.controls2", + "controlsc": "wxPython.controls", + "eventsc": "wxPython.events", + "filesysc": "wxPython.filesys", + "fontsc": "wxPython.fonts", + "framesc": "wxPython.frames", + "gdic": "wxPython.gdi", + "imagec": "wxPython.image", + "mdic": "wxPython.mdi", + "misc2c": "wxPython.misc2", + "miscc": "wxPython.misc", + "printfwc": "wxPython.printfw", + "sizersc": "wxPython.sizers", + "stattoolc": "wxPython.stattool", + "streamsc": "wxPython.streams", + "utilsc": "wxPython.utils", + "windows2c": "wxPython.windows2", + "windows3c": "wxPython.windows3", + "windowsc": "wxPython.windows", + } + + # Somewhat hackish: change modulefinder's badmodules dictionary in place. + bad = mf.badmodules + # mf.badmodules is a dictionary mapping unfound module names + # to another dictionary, the keys of this are the module names + # importing the unknown module. For the 'miscc' module + # mentioned above, it looks like this: + # mf.badmodules["miscc"] = { "wxPython.miscc": 1 } + for name in mf.any_missing(): + if name in self.ignores: + del bad[name] + continue + mod = builtins.get(name, None) + if mod is not None: + if mod in bad[name] and bad[name] == {mod: 1}: + del bad[name] + + def find_dlls(self, extensions): + dlls = [item.__file__ for item in extensions] +# extra_path = ["."] # XXX + extra_path = [] + dlls, unfriendly_dlls, other_depends = \ + self.find_dependend_dlls(dlls, + extra_path + sys.path, + self.dll_excludes) + self.other_depends = other_depends + # dlls contains the path names of all dlls we need. + # If a dll uses a function PyImport_ImportModule (or what was it?), + # it's name is additionally in unfriendly_dlls. + for item in extensions: + if item.__file__ in dlls: + dlls.remove(item.__file__) + return dlls + + def create_directories(self): + bdist_base = self.get_finalized_command('bdist').bdist_base + self.bdist_dir = os.path.join(bdist_base, 'winexe') + + collect_name = "collect-%d.%d" % sys.version_info[:2] + self.collect_dir = os.path.abspath( + os.path.join(self.bdist_dir, collect_name)) + self.mkpath(self.collect_dir) + + bundle_name = "bundle-%d.%d" % sys.version_info[:2] + self.bundle_dir = os.path.abspath( + os.path.join(self.bdist_dir, bundle_name)) + self.mkpath(self.bundle_dir) + + self.temp_dir = os.path.abspath(os.path.join(self.bdist_dir, "temp")) + self.mkpath(self.temp_dir) + + self.dist_dir = os.path.abspath(self.dist_dir) + self.mkpath(self.dist_dir) + + if self.distribution.zipfile is None: + self.lib_dir = self.dist_dir + else: + self.lib_dir = os.path.join(self.dist_dir, + os.path.dirname(self.distribution.zipfile)) + self.mkpath(self.lib_dir) + + def copy_extensions(self, extensions): + print("*** copy extensions ***") + # copy the extensions to the target directory + for item in extensions: + src = item.__file__ + if self.bundle_files > 2: # don't bundle pyds and dlls + dst = os.path.join(self.lib_dir, (item.__pydfile__)) + self.copy_file(src, dst, preserve_mode=0) + self.lib_files.append(dst) + else: + # we have to preserve the packages + package = "\\".join(item.__name__.split(".")[:-1]) + if package: + dst = os.path.join(package, os.path.basename(src)) + else: + dst = os.path.basename(src) + self.copy_file(src, os.path.join( + self.collect_dir, dst), preserve_mode=0) + self.compiled_files.append(dst) + + def copy_dlls(self, dlls): + # copy needed dlls where they belong. + print("*** copy dlls ***") + if self.bundle_files < 3: + self.copy_dlls_bundle_files(dlls) + return + # dlls belong into the lib_dir, except those listed in dlls_in_exedir, + # which have to go into exe_dir (pythonxy.dll, w9xpopen.exe). + for dll in dlls: + base = os.path.basename(dll) + if base.lower() in self.dlls_in_exedir: + # These special dlls cannot be in the lib directory, + # they must go into the exe directory. + dst = os.path.join(self.exe_dir, base) + else: + dst = os.path.join(self.lib_dir, base) + _, copied = self.copy_file(dll, dst, preserve_mode=0) + if not self.dry_run and copied and base.lower() == python_dll.lower(): + # If we actually copied pythonxy.dll, we have to patch it. + # + # Previously, the code did it every time, but this + # breaks if, for example, someone runs UPX over the + # dist directory. Patching an UPX'd dll seems to work + # (no error is detected when patching), but the + # resulting dll does not work anymore. + # + # The function restores the file times so + # dependencies still work correctly. + self.patch_python_dll_winver(dst) + + self.lib_files.append(dst) + + def copy_dlls_bundle_files(self, dlls): + # If dlls have to be bundled, they are copied into the + # collect_dir and will be added to the list of files to + # include in the zip archive 'self.compiled_files'. + # + # dlls listed in dlls_in_exedir have to be treated differently: + # + for dll in dlls: + base = os.path.basename(dll) + if base.lower() in self.dlls_in_exedir: + # pythonXY.dll must be bundled as resource. + # w9xpopen.exe must be copied to self.exe_dir. + if base.lower() == python_dll.lower() and self.bundle_files < 2: + dst = os.path.join(self.bundle_dir, base) + else: + dst = os.path.join(self.exe_dir, base) + _, copied = self.copy_file(dll, dst, preserve_mode=0) + if not self.dry_run and copied and base.lower() == python_dll.lower(): + # If we actually copied pythonxy.dll, we have to + # patch it. Well, since it's impossible to load + # resources from the bundled dlls it probably + # doesn't matter. + self.patch_python_dll_winver(dst) + self.lib_files.append(dst) + continue + + dst = os.path.join(self.collect_dir, os.path.basename(dll)) + self.copy_file(dll, dst, preserve_mode=0) + # Make sure they will be included into the zipfile. + self.compiled_files.append(os.path.basename(dst)) + + def create_binaries(self, py_files, extensions, dlls): + dist = self.distribution + + # byte compile the python modules into the target directory + print("*** byte compile python files ***") + self.compiled_files = byte_compile(py_files, + target_dir=self.collect_dir, + optimize=self.optimize, + force=0, + verbose=self.verbose, + dry_run=self.dry_run) + + self.lib_files = [] + self.console_exe_files = [] + self.windows_exe_files = [] + self.service_exe_files = [] + self.comserver_files = [] + + self.copy_extensions(extensions) + self.copy_dlls(dlls) + + # create the shared zipfile containing all Python modules + if dist.zipfile is None: + fd, archive_name = tempfile.mkstemp() + os.close(fd) + else: + archive_name = os.path.join(self.lib_dir, + os.path.basename(dist.zipfile)) + + arcname = self.make_lib_archive(archive_name, + base_dir=self.collect_dir, + files=self.compiled_files, + verbose=self.verbose, + dry_run=self.dry_run) + if dist.zipfile is not None: + self.lib_files.append(arcname) + + for target in self.distribution.isapi: + print("*** copy isapi support DLL ***") + # Locate the support DLL, and copy as "_script.dll", just like + # isapi itself + import isapi + src_name = is_debug_build and "PyISAPI_loader_d.dll" or \ + "PyISAPI_loader.dll" + src = os.path.join(isapi.__path__[0], src_name) + # destination name is "_{module_name}.dll" just like pyisapi does. + script_base = os.path.splitext(os.path.basename(target.script))[0] + dst = os.path.join(self.exe_dir, "_" + script_base + ".dll") + self.copy_file(src, dst, preserve_mode=0) + + if self.distribution.has_data_files(): + print("*** copy data files ***") + install_data = self.reinitialize_command('install_data') + install_data.install_dir = self.dist_dir + install_data.ensure_finalized() + install_data.run() + + self.lib_files.extend(install_data.get_outputs()) + + # build the executables + for target in dist.console: + dst = self.build_executable(target, self.get_console_template(), + arcname, target.script) + self.console_exe_files.append(dst) + for target in dist.windows: + dst = self.build_executable(target, self.get_windows_template(), + arcname, target.script) + self.windows_exe_files.append(dst) + for target in dist.service: + dst = self.build_service(target, self.get_service_template(), + arcname) + self.service_exe_files.append(dst) + + for target in dist.isapi: + dst = self.build_isapi(target, self.get_isapi_template(), arcname) + + for target in dist.com_server: + if getattr(target, "create_exe", True): + dst = self.build_comserver(target, self.get_comexe_template(), + arcname) + self.comserver_files.append(dst) + if getattr(target, "create_dll", True): + dst = self.build_comserver(target, self.get_comdll_template(), + arcname) + self.comserver_files.append(dst) + + for target in dist.ctypes_com_server: + dst = self.build_comserver(target, self.get_ctypes_comdll_template(), + arcname, boot_script="ctypes_com_server") + self.comserver_files.append(dst) + + if dist.zipfile is None: + os.unlink(arcname) + else: + if self.bundle_files < 3 or self.compressed: + arcbytes = open(arcname, "rb").read() + arcfile = open(arcname, "wb") + + if self.bundle_files < 2: # bundle pythonxy.dll also + print(("Adding %s to %s" % (python_dll, arcname))) + arcfile.write("") + bytes = open(os.path.join( + self.bundle_dir, python_dll), "rb").read() + arcfile.write(struct.pack("i", len(bytes))) + arcfile.write(bytes) # python dll + + if self.compressed: + # prepend zlib.pyd also + zlib_file = imp.find_module("zlib")[0] + if zlib_file: + print(("Adding zlib%s.pyd to %s" % + (is_debug_build and "_d" or "", arcname))) + arcfile.write("") + bytes = zlib_file.read() + arcfile.write(struct.pack("i", len(bytes))) + arcfile.write(bytes) # zlib.pyd + + arcfile.write(arcbytes) + +# if self.bundle_files < 2: +# remove python dll from the exe_dir, since it is now bundled. +#### os.remove(os.path.join(self.exe_dir, python_dll)) + + # for user convenience, let subclasses override the templates to use + + def get_console_template(self): + return is_debug_build and "run_d.exe" or "run.exe" + + def get_windows_template(self): + return is_debug_build and "run_w_d.exe" or "run_w.exe" + + def get_service_template(self): + return is_debug_build and "run_d.exe" or "run.exe" + + def get_isapi_template(self): + return is_debug_build and "run_isapi_d.dll" or "run_isapi.dll" + + def get_comexe_template(self): + return is_debug_build and "run_w_d.exe" or "run_w.exe" + + def get_comdll_template(self): + return is_debug_build and "run_dll_d.dll" or "run_dll.dll" + + def get_ctypes_comdll_template(self): + return is_debug_build and "run_ctypes_dll_d.dll" or "run_ctypes_dll.dll" + + def fixup_distribution(self): + dist = self.distribution + + # Convert our args into target objects. + dist.com_server = FixupTargets(dist.com_server, "modules") + dist.ctypes_com_server = FixupTargets( + dist.ctypes_com_server, "modules") + dist.service = FixupTargets(dist.service, "modules") + dist.windows = FixupTargets(dist.windows, "script") + dist.console = FixupTargets(dist.console, "script") + dist.isapi = FixupTargets(dist.isapi, "script") + + # make sure all targets use the same directory, this is + # also the directory where the pythonXX.dll must reside + paths = sets.Set() + for target in dist.com_server + dist.service \ + + dist.windows + dist.console + dist.isapi: + paths.add(os.path.dirname(target.get_dest_base())) + + if len(paths) > 1: + raise DistutilsOptionError("all targets must use the same directory: %s" % + [p for p in paths]) + if paths: + exe_dir = paths.pop() # the only element + if os.path.isabs(exe_dir): + raise DistutilsOptionError( + "exe directory must be relative: %s" % exe_dir) + self.exe_dir = os.path.join(self.dist_dir, exe_dir) + self.mkpath(self.exe_dir) + else: + # Do we allow to specify no targets? + # We can at least build a zipfile... + self.exe_dir = self.lib_dir + + def get_boot_script(self, boot_type): + # return the filename of the script to use for com servers. + thisfile = sys.modules['py2exe.build_exe'].__file__ + return os.path.join(os.path.dirname(thisfile), + "boot_" + boot_type + ".py") + + def build_comserver(self, target, template, arcname, boot_script="com_servers"): + # Build a dll and an exe executable hosting all the com + # objects listed in module_names. + # The basename of the dll/exe is the last part of the first module. + # Do we need a way to specify the name of the files to be built? + + # Setup the variables our boot script needs. + vars = {"com_module_names": target.modules} + boot = self.get_boot_script(boot_script) + # and build it + return self.build_executable(target, template, arcname, boot, vars) + + def get_service_names(self, module_name): + # import the script with every side effect :) + __import__(module_name) + mod = sys.modules[module_name] + for name, klass in list(mod.__dict__.items()): + if hasattr(klass, "_svc_name_"): + break + else: + raise RuntimeError("No services in module") + deps = () + if hasattr(klass, "_svc_deps_"): + deps = klass._svc_deps_ + return klass.__name__, klass._svc_name_, klass._svc_display_name_, deps + + def build_service(self, target, template, arcname): + # It should be possible to host many modules in a single service - + # but this is yet to be tested. + assert len(target.modules) == 1, "We only support one service module" + + cmdline_style = getattr(target, "cmdline_style", "py2exe") + if cmdline_style not in ["py2exe", "pywin32", "custom"]: + raise RuntimeError("cmdline_handler invalid") + + vars = {"service_module_names": target.modules, + "cmdline_style": cmdline_style, + } + boot = self.get_boot_script("service") + return self.build_executable(target, template, arcname, boot, vars) + + def build_isapi(self, target, template, arcname): + target_module = os.path.splitext(os.path.basename(target.script))[0] + vars = {"isapi_module_name": target_module, + } + return self.build_executable(target, template, arcname, None, vars) + + def build_manifest(self, target, template): + # Optionally return a manifest to be included in the target executable. + # Note for Python 2.6 and later, its *necessary* to include a manifest + # which correctly references the CRT. For earlier versions, a manifest + # is optional, and only necessary to customize things like + # Vista's User Access Control 'requestedExecutionLevel' setting, etc. + default_manifest = """ + + + + + + + + + + """ + from py2exe_util import load_resource + if os.path.splitext(template)[1] == ".exe": + rid = 1 + else: + rid = 2 + try: + # Manfiests have resource type of 24, and ID of either 1 or 2. + mfest = load_resource(ensure_unicode(template), RT_MANIFEST, rid) + # we consider the manifest 'changed' as we know we clobber all + # resources including the existing manifest - so this manifest must + # get written even if we make no other changes. + changed = True + except RuntimeError: + mfest = default_manifest + # in this case the template had no existing manifest, so its + # not considered 'changed' unless we make further changes later. + changed = False + # update the manifest according to our options. + # for now a regex will do. + if target.uac_info: + changed = True + if isinstance(target.uac_info, tuple): + exec_level, ui = target.uac_info + else: + exec_level = target.uac_info + ui = False + new_lines = [] + for line in mfest.splitlines(): + repl = r'\1%s\3%s\5' % (exec_level, ui) + new_lines.append(re.sub(pat_manifest_uac, repl, line)) + mfest = "".join(new_lines) + if not changed: + return None, None + return mfest, rid + + def build_executable(self, target, template, arcname, script, vars={}): + # Build an executable for the target + # template is the exe-stub to use, and arcname is the zipfile + # containing the python modules. + from py2exe_util import add_resource, add_icon + ext = os.path.splitext(template)[1] + exe_base = target.get_dest_base() + exe_path = os.path.join(self.dist_dir, exe_base + ext) + # The user may specify a sub-directory for the exe - that's fine, we + # just specify the parent directory for the .zip + parent_levels = len(os.path.normpath(exe_base).split(os.sep))-1 + lib_leaf = self.lib_dir[len(self.dist_dir)+1:] + relative_arcname = ((".." + os.sep) * parent_levels) + if lib_leaf: + relative_arcname += lib_leaf + os.sep + relative_arcname += os.path.basename(arcname) + + src = os.path.join(os.path.dirname(__file__), template) + # We want to force the creation of this file, as otherwise distutils + # will see the earlier time of our 'template' file versus the later + # time of our modified template file, and consider our old file OK. + old_force = self.force + self.force = True + self.copy_file(src, exe_path, preserve_mode=0) + self.force = old_force + + # Make sure the file is writeable... + os.chmod(exe_path, stat.S_IREAD | stat.S_IWRITE) + try: + f = open(exe_path, "a+b") + f.close() + except IOError as why: + print(("WARNING: File %s could not be opened - %s" % (exe_path, why))) + + # We create a list of code objects, and write it as a marshaled + # stream. The framework code then just exec's these in order. + # First is our common boot script. + boot = self.get_boot_script("common") + boot_code = compile(file(boot, "U").read(), + os.path.abspath(boot), "exec") + code_objects = [boot_code] + if self.bundle_files < 3: + code_objects.append( + compile("import zipextimporter; zipextimporter.install()", + "", "exec")) + for var_name, var_val in list(vars.items()): + code_objects.append( + compile("%s=%r\n" % (var_name, var_val), var_name, "exec") + ) + if self.custom_boot_script: + code_object = compile(file(self.custom_boot_script, "U").read() + "\n", + os.path.abspath(self.custom_boot_script), "exec") + code_objects.append(code_object) + if script: + code_object = compile(open(script, "U").read() + "\n", + os.path.basename(script), "exec") + code_objects.append(code_object) + code_bytes = marshal.dumps(code_objects) + + if self.distribution.zipfile is None: + relative_arcname = "" + + si = struct.pack("iiii", + 0x78563412, # a magic value, + self.optimize, + self.unbuffered, + len(code_bytes), + ) + relative_arcname + "\000" + + script_bytes = si + code_bytes + '\000\000' + self.announce("add script resource, %d bytes" % len(script_bytes)) + if not self.dry_run: + add_resource(ensure_unicode(exe_path), + script_bytes, "PYTHONSCRIPT", 1, True) + + # add the pythondll as resource, and delete in self.exe_dir + if self.bundle_files < 2 and self.distribution.zipfile is None: + # bundle pythonxy.dll + dll_path = os.path.join(self.bundle_dir, python_dll) + bytes = open(dll_path, "rb").read() + # image, bytes, lpName, lpType + + print(("Adding %s as resource to %s" % (python_dll, exe_path))) + add_resource(ensure_unicode(exe_path), bytes, + # for some reason, the 3. argument MUST BE UPPER CASE, + # otherwise the resource will not be found. + ensure_unicode(python_dll).upper(), 1, False) + + if self.compressed and self.bundle_files < 3 and self.distribution.zipfile is None: + zlib_file = imp.find_module("zlib")[0] + if zlib_file: + print(("Adding zlib.pyd as resource to %s" % exe_path)) + zlib_bytes = zlib_file.read() + add_resource(ensure_unicode(exe_path), zlib_bytes, + # for some reason, the 3. argument MUST BE UPPER CASE, + # otherwise the resource will not be found. + "ZLIB.PYD", 1, False) + + # Handle all resources specified by the target + bitmap_resources = getattr(target, "bitmap_resources", []) + for bmp_id, bmp_filename in bitmap_resources: + bmp_data = open(bmp_filename, "rb").read() + # skip the 14 byte bitmap header. + if not self.dry_run: + add_resource(ensure_unicode(exe_path), + bmp_data[14:], RT_BITMAP, bmp_id, False) + icon_resources = getattr(target, "icon_resources", []) + for ico_id, ico_filename in icon_resources: + if not self.dry_run: + add_icon(ensure_unicode(exe_path), + ensure_unicode(ico_filename), ico_id) + + # a manifest + mfest, mfest_id = self.build_manifest(target, src) + if mfest: + self.announce("add manifest, %d bytes" % len(mfest)) + if not self.dry_run: + add_resource(ensure_unicode(exe_path), mfest, + RT_MANIFEST, mfest_id, False) + + for res_type, res_id, data in getattr(target, "other_resources", []): + if not self.dry_run: + if isinstance(res_type, str): + res_type = ensure_unicode(res_type) + add_resource(ensure_unicode(exe_path), + data, res_type, res_id, False) + + typelib = getattr(target, "typelib", None) + if typelib is not None: + data = open(typelib, "rb").read() + add_resource(ensure_unicode(exe_path), data, "TYPELIB", 1, False) + + self.add_versioninfo(target, exe_path) + + # Hm, this doesn't make sense with normal executables, which are + # already small (around 20 kB). + # + # But it would make sense with static build pythons, but not + # if the zipfile is appended to the exe - it will be too slow + # then (although it is a wonder it works at all in this case). + # + # Maybe it would be faster to use the frozen modules machanism + # instead of the zip-import? +# if self.compressed: +## import gc +# gc.collect() # to close all open files! +## os.system("upx -9 %s" % exe_path) + + if self.distribution.zipfile is None: + zip_data = open(arcname, "rb").read() + open(exe_path, "a+b").write(zip_data) + + return exe_path + + def add_versioninfo(self, target, exe_path): + # Try to build and add a versioninfo resource + + def get(name, md=self.distribution.metadata): + # Try to get an attribute from the target, if not defined + # there, from the distribution's metadata, or None. Note + # that only *some* attributes are allowed by distutils on + # the distribution's metadata: version, description, and + # name. + return getattr(target, name, getattr(md, name, None)) + + version = get("version") + if version is None: + return + + from py2exe.resources.VersionInfo import Version, RT_VERSION, VersionError + version = Version(version, + file_description=get("description"), + comments=get("comments"), + company_name=get("company_name"), + legal_copyright=get("copyright"), + legal_trademarks=get("trademarks"), + original_filename=os.path.basename(exe_path), + product_name=get("name"), + product_version=get("product_version") or version) + try: + bytes = version.resource_bytes() + except VersionError as detail: + self.warn("Version Info will not be included:\n %s" % detail) + return + + from py2exe_util import add_resource + add_resource(ensure_unicode(exe_path), bytes, RT_VERSION, 1, False) + + def patch_python_dll_winver(self, dll_name, new_winver=None): + from py2exe.resources.StringTables import StringTable, RT_STRING + from py2exe_util import add_resource, load_resource + from py2exe.resources.VersionInfo import RT_VERSION + + new_winver = new_winver or self.distribution.metadata.name or "py2exe" + if self.verbose: + print(("setting sys.winver for '%s' to '%s'" % + (dll_name, new_winver))) + if self.dry_run: + return + + # We preserve the times on the file, so the dependency tracker works. + st = os.stat(dll_name) + # and as the resource functions silently fail if the open fails, + # check it explicitly. + os.chmod(dll_name, stat.S_IREAD | stat.S_IWRITE) + try: + f = open(dll_name, "a+b") + f.close() + except IOError as why: + print(("WARNING: File %s could not be opened - %s" % (dll_name, why))) + # We aren't smart enough to update string resources in place, so we need + # to persist other resources we care about. + unicode_name = ensure_unicode(dll_name) + + # Preserve existing version info (all versions should have this) + ver_info = load_resource(unicode_name, RT_VERSION, 1) + # Preserve an existing manifest (only python26.dll+ will have this) + try: + # Manfiests have resource type of 24, and ID of either 1 or 2. + mfest = load_resource(unicode_name, RT_MANIFEST, 2) + except RuntimeError: + mfest = None + + # Start putting the resources back, passing 'delete=True' for the first. + add_resource(unicode_name, ver_info, RT_VERSION, 1, True) + if mfest is not None: + add_resource(unicode_name, mfest, RT_MANIFEST, 2, False) + + # OK - do the strings. + s = StringTable() + # 1000 is the resource ID Python loads for its winver. + s.add_string(1000, new_winver) + for id, data in s.binary(): + add_resource(ensure_unicode(dll_name), data, RT_STRING, id, False) + + # restore the time. + os.utime(dll_name, (st[stat.ST_ATIME], st[stat.ST_MTIME])) + + def find_dependend_dlls(self, dlls, pypath, dll_excludes): + import py2exe_util + sysdir = py2exe_util.get_sysdir() + windir = py2exe_util.get_windir() + # This is the tail of the path windows uses when looking for dlls + # XXX On Windows NT, the SYSTEM directory is also searched + exedir = os.path.dirname(sys.executable) + syspath = os.environ['PATH'] + loadpath = ';'.join([exedir, sysdir, windir, syspath]) + + # Found by Duncan Booth: + # It may be possible that bin_depends needs extension modules, + # so the loadpath must be extended by our python path. + loadpath = loadpath + ';' + ';'.join(pypath) + + templates = sets.Set() + if self.distribution.console: + templates.add(self.get_console_template()) + if self.distribution.windows: + templates.add(self.get_windows_template()) + if self.distribution.service: + templates.add(self.get_service_template()) + for target in self.distribution.com_server: + if getattr(target, "create_exe", True): + templates.add(self.get_comexe_template()) + if getattr(target, "create_dll", True): + templates.add(self.get_comdll_template()) + + templates = [os.path.join(os.path.dirname(__file__), t) + for t in templates] + + # We use Python.exe to track the dependencies of our run stubs ... + images = dlls + templates + + self.announce("Resolving binary dependencies:") + excludes_use = dll_excludes[:] + # The MSVCRT modules are never found when using VS2008+ + if sys.version_info > (2, 6): + excludes_use.append("msvcr90.dll") + + # we add python.exe (aka sys.executable) to the list of images + # to scan for dependencies, but remove it later again from the + # results list. In this way pythonXY.dll is collected, and + # also the libraries it depends on. + alldlls, warnings, other_depends = \ + bin_depends(loadpath, images + [sys.executable], excludes_use) + alldlls.remove(sys.executable) + for dll in alldlls: + self.announce(" %s" % dll) + # ... but we don't need the exe stubs run_xxx.exe + for t in templates: + alldlls.remove(t) + + return alldlls, warnings, other_depends + # find_dependend_dlls() + + def get_hidden_imports(self): + # imports done from builtin modules in C code (untrackable by py2exe) + return {"time": ["_strptime"], + # "datetime": ["time"], + "cPickle": ["copy_reg"], + "parser": ["copy_reg"], + "codecs": ["encodings"], + + "cStringIO": ["copy_reg"], + "_sre": ["copy", "string", "sre"], + } + + def parse_mf_results(self, mf): + for name, imports in list(self.get_hidden_imports().items()): + if name in list(mf.modules.keys()): + for mod in imports: + mf.import_hook(mod) + + tcl_src_dir = tcl_dst_dir = None + if "Tkinter" in list(mf.modules.keys()): + import tkinter + import _tkinter + tk = _tkinter.create() + tcl_dir = tk.call("info", "library") + tcl_src_dir = os.path.split(tcl_dir)[0] + tcl_dst_dir = os.path.join(self.lib_dir, "tcl") + + self.announce("Copying TCL files from %s..." % tcl_src_dir) + self.copy_tree(os.path.join(tcl_src_dir, "tcl%s" % _tkinter.TCL_VERSION), + os.path.join(tcl_dst_dir, "tcl%s" % _tkinter.TCL_VERSION)) + self.copy_tree(os.path.join(tcl_src_dir, "tk%s" % _tkinter.TK_VERSION), + os.path.join(tcl_dst_dir, "tk%s" % _tkinter.TK_VERSION)) + del tk, _tkinter, Tkinter + + # Retrieve modules from modulefinder + py_files = [] + extensions = [] + builtins = [] + + for item in list(mf.modules.values()): + # There may be __main__ modules (from mf.run_script), but + # we don't need them in the zipfile we build. + if item.__name__ == "__main__": + continue + if self.bundle_files < 3 and item.__name__ in ("pythoncom", "pywintypes"): + # these are handled specially in zipextimporter. + continue + src = item.__file__ + if src: + base, ext = os.path.splitext(src) + suffix = ext + if sys.platform.startswith("win") and ext in [".dll", ".pyd"] \ + and base.endswith("_d"): + suffix = "_d" + ext + + if suffix in _py_suffixes: + py_files.append(item) + elif suffix in _c_suffixes: + extensions.append(item) + if not self.bundle_files < 3: + loader = self.create_loader(item) + if loader: + py_files.append(loader) + else: + raise RuntimeError( + "Don't know how to handle '%s'" % repr(src)) + else: + builtins.append(item.__name__) + + # sort on the file names, the output is nicer to read + py_files.sort(lambda a, b: cmp(a.__file__, b.__file__)) + extensions.sort(lambda a, b: cmp(a.__file__, b.__file__)) + builtins.sort() + return py_files, extensions, builtins + + def plat_finalize(self, modules, py_files, extensions, dlls): + # platform specific code for final adjustments to the file + # lists + if sys.platform == "win32": + # pythoncom and pywintypes are imported via LoadLibrary calls, + # help py2exe to include the dlls: + if "pythoncom" in list(modules.keys()): + import pythoncom + dlls.add(pythoncom.__file__) + if "pywintypes" in list(modules.keys()): + import pywintypes + dlls.add(pywintypes.__file__) + self.copy_w9xpopen(modules, dlls) + else: + raise DistutilsError( + "Platform %s not yet implemented" % sys.platform) + + def copy_w9xpopen(self, modules, dlls): + # Using popen requires (on Win9X) the w9xpopen.exe helper executable. + if "os" in list(modules.keys()) or "popen2" in list(modules.keys()): + if is_debug_build: + fname = os.path.join(os.path.dirname( + sys.executable), "w9xpopen_d.exe") + else: + fname = os.path.join(os.path.dirname( + sys.executable), "w9xpopen.exe") + # Don't copy w9xpopen.exe if it doesn't exist (64-bit + # Python build, for example) + if os.path.exists(fname): + dlls.add(fname) + + def create_loader(self, item): + # Hm, how to avoid needless recreation of this file? + pathname = os.path.join(self.temp_dir, "%s.py" % item.__name__) + if self.bundle_files > 2: # don't bundle pyds and dlls + # all dlls are copied into the same directory, so modify + # names to include the package name to avoid name + # conflicts and tuck it away for future reference + fname = item.__name__ + os.path.splitext(item.__file__)[1] + item.__pydfile__ = fname + else: + fname = os.path.basename(item.__file__) + + # and what about dry_run? + if self.verbose: + print(("creating python loader for extension '%s' (%s -> %s)" % + (item.__name__, item.__file__, fname))) + + source = LOADER % fname + if not self.dry_run: + open(pathname, "w").write(source) + else: + return None + from modulefinder import Module + return Module(item.__name__, pathname) + + def plat_prepare(self): + self.includes.append("warnings") # needed by Python itself + if not self.ascii: + self.packages.append("encodings") + self.includes.append("codecs") + if self.bundle_files < 3: + self.includes.append("zipextimporter") + # builtin in run_*.exe and run_*.dll + self.excludes.append("_memimporter") + if self.compressed: + self.includes.append("zlib") + + # os.path will never be found ;-) + self.ignores.append('os.path') + + # update the self.ignores list to ignore platform specific + # modules. + if sys.platform == "win32": + self.ignores += ['AL', + 'Audio_mac', + 'Carbon.File', + 'Carbon.Folder', + 'Carbon.Folders', + 'EasyDialogs', + 'MacOS', + 'Mailman', + 'SOCKS', + 'SUNAUDIODEV', + '_dummy_threading', + '_emx_link', + '_xmlplus', + '_xmlrpclib', + 'al', + 'bundlebuilder', + 'ce', + 'cl', + 'dbm', + 'dos', + 'fcntl', + 'gestalt', + 'grp', + 'ic', + 'java.lang', + 'mac', + 'macfs', + 'macostools', + 'mkcwproject', + 'org.python.core', + 'os.path', + 'os2', + 'poll', + 'posix', + 'pwd', + 'readline', + 'riscos', + 'riscosenviron', + 'riscospath', + 'rourl2path', + 'sgi', + 'sgmlop', + 'sunaudiodev', + 'termios', + 'vms_lib'] + # special dlls which must be copied to the exe_dir, not the lib_dir + self.dlls_in_exedir = [python_dll, + "w9xpopen%s.exe" % ( + is_debug_build and "_d" or ""), + "msvcr71%s.dll" % (is_debug_build and "d" or "")] + else: + raise DistutilsError( + "Platform %s not yet implemented" % sys.platform) + + def find_needed_modules(self, mf, files, modules): + # feed Modulefinder with everything, and return it. + for mod in modules: + mf.import_hook(mod) + + for path in files: + mf.run_script(path) + + mf.run_script(self.get_boot_script("common")) + + if self.distribution.com_server: + mf.run_script(self.get_boot_script("com_servers")) + + if self.distribution.ctypes_com_server: + mf.run_script(self.get_boot_script("ctypes_com_server")) + + if self.distribution.service: + mf.run_script(self.get_boot_script("service")) + + if self.custom_boot_script: + mf.run_script(self.custom_boot_script) + + for mod in self.includes: + if mod[-2:] == '.*': + mf.import_hook(mod[:-2], None, ['*']) + else: + mf.import_hook(mod) + + for f in self.packages: + def visit(arg, dirname, names): + if '__init__.py' in names: + arg.append(dirname) + + # Try to find the package using ModuleFinders's method to + # allow for modulefinder.AddPackagePath interactions + mf.import_hook(f) + + # If modulefinder has seen a reference to the package, then + # we prefer to believe that (imp_find_module doesn't seem to locate + # sub-packages) + if f in mf.modules: + module = mf.modules[f] + if module.__path__ is None: + # it's a module, not a package, so paths contains just the + # file entry + paths = [module.__file__] + else: + # it is a package because __path__ is available. __path__ + # is actually a list of paths that are searched to import + # sub-modules and sub-packages + paths = module.__path__ + else: + # Find path of package + try: + paths = [imp_find_module(f)[1]] + except ImportError: + self.warn("No package named %s" % f) + continue + + packages = [] + for path in paths: + # walk the path to find subdirs containing __init__.py files + os.path.walk(path, visit, packages) + + # scan the results (directory of __init__.py files) + # first trim the path (of the head package), + # then convert directory name in package name, + # finally push into modulefinder. + for p in packages: + if p.startswith(path): + package = f + '.' + p[len(path)+1:].replace('\\', '.') + mf.import_hook(package, None, ["*"]) + + return mf + + def make_lib_archive(self, zip_filename, base_dir, files, + verbose=0, dry_run=0): + from distutils.dir_util import mkpath + if not self.skip_archive: + # Like distutils "make_archive", but we can specify the files + # to include, and the compression to use - default is + # ZIP_STORED to keep the runtime performance up. Also, we + # don't append '.zip' to the filename. + mkpath(os.path.dirname(zip_filename), dry_run=dry_run) + + if self.compressed: + compression = zipfile.ZIP_DEFLATED + else: + compression = zipfile.ZIP_STORED + + if not dry_run: + z = zipfile.ZipFile(zip_filename, "w", + compression=compression) + for f in files: + z.write(os.path.join(base_dir, f), f) + z.close() + + return zip_filename + else: + # Don't really produce an archive, just copy the files. + from distutils.file_util import copy_file + + destFolder = os.path.dirname(zip_filename) + + for f in files: + d = os.path.dirname(f) + if d: + mkpath(os.path.join(destFolder, d), + verbose=verbose, dry_run=dry_run) + copy_file( + os.path.join(base_dir, f), + os.path.join(destFolder, f), + preserve_mode=0, + verbose=verbose, + dry_run=dry_run + ) + return '.' + + +################################################################ + +class FileSet: + # A case insensitive but case preserving set of files + def __init__(self, iterable=None): + self._dict = {} + if iterable is not None: + for arg in iterable: + self.add(arg) + + def __repr__(self): + return "" % (list(self._dict.values()), id(self)) + + def add(self, fname): + self._dict[fname.upper()] = fname + + def remove(self, fname): + del self._dict[fname.upper()] + + def __contains__(self, fname): + return fname.upper() in list(self._dict.keys()) + + def __getitem__(self, index): + key = list(self._dict.keys())[index] + return self._dict[key] + + def __len__(self): + return len(self._dict) + + def copy(self): + res = FileSet() + res._dict.update(self._dict) + return res + +# class FileSet() + + +def bin_depends(path, images, excluded_dlls): + import py2exe_util + warnings = FileSet() + images = FileSet(images) + dependents = FileSet() + others = FileSet() + while images: + for image in images.copy(): + images.remove(image) + if not image in dependents: + dependents.add(image) + abs_image = os.path.abspath(image) + loadpath = os.path.dirname(abs_image) + ';' + path + for result in list(py2exe_util.depends(image, loadpath).items()): + dll, uses_import_module = result + if os.path.basename(dll).lower() not in excluded_dlls: + if isSystemDLL(dll): + others.add(dll) + continue + if dll not in images and dll not in dependents: + images.add(dll) + if uses_import_module: + warnings.add(dll) + return dependents, warnings, others + + +# DLLs to be excluded +# XXX This list is NOT complete (it cannot be) +# Note: ALL ENTRIES MUST BE IN LOWER CASE! +EXCLUDED_DLLS = ( + "advapi32.dll", + "comctl32.dll", + "comdlg32.dll", + "crtdll.dll", + "gdi32.dll", + "glu32.dll", + "opengl32.dll", + "imm32.dll", + "kernel32.dll", + "mfc42.dll", + "msvcirt.dll", + "msvcrt.dll", + "msvcrtd.dll", + "ntdll.dll", + "odbc32.dll", + "ole32.dll", + "oleaut32.dll", + "rpcrt4.dll", + "shell32.dll", + "shlwapi.dll", + "user32.dll", + "version.dll", + "winmm.dll", + "winspool.drv", + "ws2_32.dll", + "ws2help.dll", + "wsock32.dll", + "netapi32.dll", + + "gdiplus.dll", +) + +# XXX Perhaps it would be better to assume dlls from the systemdir are system dlls, +# and make some exceptions for known dlls, like msvcr71, pythonXY.dll, and so on? + + +def isSystemDLL(pathname): + if os.path.basename(pathname).lower() in ("msvcr71.dll", "msvcr71d.dll"): + return 0 + if os.path.basename(pathname).lower() in EXCLUDED_DLLS: + return 1 + # How can we determine whether a dll is a 'SYSTEM DLL'? + # Is it sufficient to use the Image Load Address? + import struct + file = open(pathname, "rb") + if file.read(2) != "MZ": + raise Exception("Seems not to be an exe-file") + file.seek(0x3C) + pe_ofs = struct.unpack("i", file.read(4))[0] + file.seek(pe_ofs) + if file.read(4) != "PE\000\000": + raise Exception("Seems not to be an exe-file", pathname) + # COFF File Header, offset of ImageBase in Optional Header + file.read(20 + 28) + imagebase = struct.unpack("I", file.read(4))[0] + return not (imagebase < 0x70000000) + + +def byte_compile(py_files, optimize=0, force=0, + target_dir=None, verbose=1, dry_run=0, + direct=None): + + if direct is None: + direct = (__debug__ and optimize == 0) + + # "Indirect" byte-compilation: write a temporary script and then + # run it with the appropriate flags. + if not direct: + from tempfile import mktemp + from distutils.util import execute + script_name = mktemp(".py") + if verbose: + print(("writing byte-compilation script '%s'" % script_name)) + if not dry_run: + script = open(script_name, "w") + script.write("""\ +from py2exe.build_exe import byte_compile +from modulefinder import Module +files = [ +""") + + for f in py_files: + script.write("Module(%s, %s, %s),\n" % + (repr(f.__name__), repr(f.__file__), repr(f.__path__))) + script.write("]\n") + script.write(""" +byte_compile(files, optimize=%s, force=%s, + target_dir=%s, + verbose=%s, dry_run=0, + direct=1) +""" % (repr(optimize), repr(force), repr(target_dir), repr(verbose))) + + script.close() + + cmd = [sys.executable, script_name] + if optimize == 1: + cmd.insert(1, "-O") + elif optimize == 2: + cmd.insert(1, "-OO") + spawn(cmd, verbose=verbose, dry_run=dry_run) + execute(os.remove, (script_name,), "removing %s" % script_name, + verbose=verbose, dry_run=dry_run) + + else: + from py_compile import compile + from distutils.dir_util import mkpath + from distutils.dep_util import newer + from distutils.file_util import copy_file + + for file in py_files: + # Terminology from the py_compile module: + # cfile - byte-compiled file + # dfile - purported source filename (same as 'file' by default) + cfile = file.__name__.replace('.', '\\') + + if file.__path__: + dfile = cfile + '\\__init__.py' + (__debug__ and 'c' or 'o') + else: + dfile = cfile + '.py' + (__debug__ and 'c' or 'o') + if target_dir: + cfile = os.path.join(target_dir, dfile) + + if force or newer(file.__file__, cfile): + if verbose: + print(("byte-compiling %s to %s" % (file.__file__, dfile))) + if not dry_run: + mkpath(os.path.dirname(cfile)) + suffix = os.path.splitext(file.__file__)[1] + if suffix in (".py", ".pyw"): + compile(file.__file__, cfile, dfile) + elif suffix in _py_suffixes: + # Minor problem: This will happily copy a file + # .pyo to .pyc or .pyc to + # .pyo, but it does seem to work. + copy_file(file.__file__, cfile, preserve_mode=0) + else: + raise RuntimeError( + "Don't know how to handle %r" % file.__file__) + else: + if verbose: + print(("skipping byte-compilation of %s to %s" % + (file.__file__, dfile))) + compiled_files = [] + for file in py_files: + cfile = file.__name__.replace('.', '\\') + + if file.__path__: + dfile = cfile + '\\__init__.py' + (optimize and 'o' or 'c') + else: + dfile = cfile + '.py' + (optimize and 'o' or 'c') + compiled_files.append(dfile) + return compiled_files + +# byte_compile() + +# win32com makepy helper. + + +def collect_win32com_genpy(path, typelibs, verbose=0, dry_run=0): + import win32com + from win32com.client import gencache, makepy + from distutils.file_util import copy_file + + old_gen_path = win32com.__gen_path__ + num = 0 + try: + win32com.__gen_path__ = path + win32com.gen_py.__path__ = [path] + gencache.__init__() + for info in typelibs: + guid, lcid, major, minor = info[:4] + # They may provide an input filename in the tuple - in which case + # they will have pre-generated it on a machine with the typelibs + # installed, and just want us to include it. + fname_in = None + if len(info) > 4: + fname_in = info[4] + if fname_in is not None: + base = gencache.GetGeneratedFileName(guid, lcid, major, minor) + fname_out = os.path.join(path, base) + ".py" + copy_file(fname_in, fname_out, + verbose=verbose, dry_run=dry_run) + num += 1 + # That's all we gotta do! + continue + + # It seems bForDemand=True generates code which is missing + # at least sometimes an import of DispatchBaseClass. + # Until this is resolved, set it to false. + # What's the purpose of bForDemand=True? Thomas + # bForDemand is supposed to only generate stubs when each + # individual object is referenced. A side-effect of that is + # that each object gets its own source file. The intent of + # this code was to set bForDemand=True, meaning we get the + # 'file per object' behaviour, but then explicitly walk all + # children forcing them to be built - so the entire object model + # is included, but not in a huge .pyc. + # I'm not sure why its not working :) I'll debug later. + # bForDemand=False isn't really important here - the overhead for + # monolithic typelib stubs is in the compilation, not the loading + # of an existing .pyc. Mark. +## makepy.GenerateFromTypeLibSpec(info, bForDemand = True) + tlb_info = (guid, lcid, major, minor) + makepy.GenerateFromTypeLibSpec(tlb_info, bForDemand=False) + # Now get the module, and build all sub-modules. + mod = gencache.GetModuleForTypelib(*tlb_info) + for clsid, name in list(mod.CLSIDToPackageMap.items()): + try: + gencache.GetModuleForCLSID(clsid) + num += 1 + # print "", name + except ImportError: + pass + return num + finally: + # restore win32com, just in case. + win32com.__gen_path__ = old_gen_path + win32com.gen_py.__path__ = [old_gen_path] + gencache.__init__() + +# utilities hacked from distutils.dir_util + + +def _chmod(file): + os.chmod(file, 0o777) + +# Helper for force_remove_tree() + + +def _build_cmdtuple(path, cmdtuples): + for f in os.listdir(path): + real_f = os.path.join(path, f) + if os.path.isdir(real_f) and not os.path.islink(real_f): + _build_cmdtuple(real_f, cmdtuples) + else: + cmdtuples.append((_chmod, real_f)) + cmdtuples.append((os.remove, real_f)) + cmdtuples.append((os.rmdir, path)) + + +def force_remove_tree(directory, verbose=0, dry_run=0): + """Recursively remove an entire directory tree. Any errors are ignored + (apart from being reported to stdout if 'verbose' is true). + """ + import distutils + from distutils.util import grok_environment_error + _path_created = distutils.dir_util._path_created + + if verbose: + print(("removing '%s' (and everything under it)" % directory)) + if dry_run: + return + cmdtuples = [] + _build_cmdtuple(directory, cmdtuples) + for cmd in cmdtuples: + try: + cmd[0](cmd[1]) + # remove dir from cache if it's already there + abspath = os.path.abspath(cmd[1]) + if abspath in _path_created: + del _path_created[abspath] + except (IOError, OSError) as exc: + if verbose: + print((grok_environment_error( + exc, "error removing %s: " % directory))) diff --git a/patch/mf.py b/patch/mf.py index acfa33e..2ce540d 100644 --- a/patch/mf.py +++ b/patch/mf.py @@ -1,812 +1,832 @@ -"""Find modules used by a script, using introspection.""" -# This module should be kept compatible with Python 2.2, see PEP 291. - -from __future__ import generators -import dis -import imp -import marshal -import os -import sys -import types -import struct - -if hasattr(sys.__stdout__, "newlines"): - READ_MODE = "U" # universal line endings -else: - # remain compatible with Python < 2.3 - READ_MODE = "r" - -LOAD_CONST = chr(dis.opname.index('LOAD_CONST')) -IMPORT_NAME = chr(dis.opname.index('IMPORT_NAME')) -STORE_NAME = chr(dis.opname.index('STORE_NAME')) -STORE_GLOBAL = chr(dis.opname.index('STORE_GLOBAL')) -STORE_OPS = [STORE_NAME, STORE_GLOBAL] -HAVE_ARGUMENT = chr(dis.HAVE_ARGUMENT) - -# !!! NOTE BEFORE INCLUDING IN PYTHON DISTRIBUTION !!! -# To clear up issues caused by the duplication of data structures between -# the real Python modulefinder and this duplicate version, packagePathMap -# and replacePackageMap are imported from the actual modulefinder. This -# should be changed back to the assigments that are commented out below. -# There are also py2exe specific pieces at the bottom of this file. - - -# Modulefinder does a good job at simulating Python's, but it can not -# handle __path__ modifications packages make at runtime. Therefore there -# is a mechanism whereby you can register extra paths in this map for a -# package, and it will be honored. - -# Note this is a mapping is lists of paths. -#~ packagePathMap = {} -from modulefinder import packagePathMap - -# A Public interface -def AddPackagePath(packagename, path): - paths = packagePathMap.get(packagename, []) - paths.append(path) - packagePathMap[packagename] = paths - -#~ replacePackageMap = {} -from modulefinder import replacePackageMap - -# This ReplacePackage mechanism allows modulefinder to work around the -# way the _xmlplus package injects itself under the name "xml" into -# sys.modules at runtime by calling ReplacePackage("_xmlplus", "xml") -# before running ModuleFinder. - -def ReplacePackage(oldname, newname): - replacePackageMap[oldname] = newname - - -class Module: - - def __init__(self, name, file=None, path=None): - self.__name__ = name - self.__file__ = file - self.__path__ = path - self.__code__ = None - # The set of global names that are assigned to in the module. - # This includes those names imported through starimports of - # Python modules. - self.globalnames = {} - # The set of starimports this module did that could not be - # resolved, ie. a starimport from a non-Python module. - self.starimports = {} - - def __repr__(self): - s = "Module(%r" % (self.__name__,) - if self.__file__ is not None: - s = s + ", %r" % (self.__file__,) - if self.__path__ is not None: - s = s + ", %r" % (self.__path__,) - s = s + ")" - return s - -class ModuleFinder: - - def __init__(self, path=None, debug=0, excludes=[], replace_paths=[], skip_scan=[]): - if path is None: - path = sys.path - self.path = path - self.modules = {} - self.badmodules = {} - self.debug = debug - self.indent = 0 - self.excludes = excludes - self.replace_paths = replace_paths - self.skip_scan = skip_scan - self.processed_paths = [] # Used in debugging only - - def msg(self, level, str, *args): - if level <= self.debug: - for i in range(self.indent): - print " ", - print str, - for arg in args: - print repr(arg), - print - - def msgin(self, *args): - level = args[0] - if level <= self.debug: - self.indent = self.indent + 1 - self.msg(*args) - - def msgout(self, *args): - level = args[0] - if level <= self.debug: - self.indent = self.indent - 1 - self.msg(*args) - - def run_script(self, pathname): - self.msg(2, "run_script", pathname) - fp = open(pathname, READ_MODE) - stuff = ("", "r", imp.PY_SOURCE) - self.load_module('__main__', fp, pathname, stuff) - - def load_file(self, pathname): - dir, name = os.path.split(pathname) - name, ext = os.path.splitext(name) - fp = open(pathname, READ_MODE) - stuff = (ext, "r", imp.PY_SOURCE) - self.load_module(name, fp, pathname, stuff) - - def import_hook(self, name, caller=None, fromlist=None, level=-1): - self.msg(3, "import_hook", name, caller, fromlist, level) - parent = self.determine_parent(caller, level=level) - q, tail = self.find_head_package(parent, name) - m = self.load_tail(q, tail) - if not fromlist: - return q - if m.__path__: - self.ensure_fromlist(m, fromlist) - return None - - def determine_parent(self, caller, level=-1): - self.msgin(4, "determine_parent", caller, level) - if not caller or level == 0: - self.msgout(4, "determine_parent -> None") - return None - pname = caller.__name__ - if level >= 1: # relative import - if caller.__path__: - level -= 1 - if level == 0: - parent = self.modules[pname] - assert parent is caller - self.msgout(4, "determine_parent ->", parent) - return parent - if pname.count(".") < level: - raise ImportError, "relative importpath too deep" - pname = ".".join(pname.split(".")[:-level]) - parent = self.modules[pname] - self.msgout(4, "determine_parent ->", parent) - return parent - if caller.__path__: - parent = self.modules[pname] - assert caller is parent - self.msgout(4, "determine_parent ->", parent) - return parent - if '.' in pname: - i = pname.rfind('.') - pname = pname[:i] - parent = self.modules[pname] - assert parent.__name__ == pname - self.msgout(4, "determine_parent ->", parent) - return parent - self.msgout(4, "determine_parent -> None") - return None - - def find_head_package(self, parent, name): - self.msgin(4, "find_head_package", parent, name) - if '.' in name: - i = name.find('.') - head = name[:i] - tail = name[i+1:] - else: - head = name - tail = "" - if parent: - qname = "%s.%s" % (parent.__name__, head) - else: - qname = head - q = self.import_module(head, qname, parent) - if q: - self.msgout(4, "find_head_package ->", (q, tail)) - return q, tail - if parent: - qname = head - parent = None - q = self.import_module(head, qname, parent) - if q: - self.msgout(4, "find_head_package ->", (q, tail)) - return q, tail - self.msgout(4, "raise ImportError: No module named", qname) - raise ImportError, "No module named " + qname - - def load_tail(self, q, tail): - self.msgin(4, "load_tail", q, tail) - m = q - while tail: - i = tail.find('.') - if i < 0: i = len(tail) - head, tail = tail[:i], tail[i+1:] - mname = "%s.%s" % (m.__name__, head) - m = self.import_module(head, mname, m) - if not m: - self.msgout(4, "raise ImportError: No module named", mname) - raise ImportError, "No module named " + mname - self.msgout(4, "load_tail ->", m) - return m - - def ensure_fromlist(self, m, fromlist, recursive=0): - self.msg(4, "ensure_fromlist", m, fromlist, recursive) - for sub in fromlist: - if sub == "*": - if not recursive: - all = self.find_all_submodules(m) - if all: - self.ensure_fromlist(m, all, 1) - elif not hasattr(m, sub): - subname = "%s.%s" % (m.__name__, sub) - submod = self.import_module(sub, subname, m) - if not submod: - raise ImportError, "No module named " + subname - - def find_all_submodules(self, m): - if not m.__path__: - return - modules = {} - # 'suffixes' used to be a list hardcoded to [".py", ".pyc", ".pyo"]. - # But we must also collect Python extension modules - although - # we cannot separate normal dlls from Python extensions. - suffixes = [] - for triple in imp.get_suffixes(): - suffixes.append(triple[0]) - for dir in m.__path__: - try: - names = os.listdir(dir) - except os.error: - self.msg(2, "can't list directory", dir) - continue - for name in names: - mod = None - for suff in suffixes: - n = len(suff) - if name[-n:] == suff: - mod = name[:-n] - break - if mod and mod != "__init__": - modules[mod] = mod - return modules.keys() - - def import_module(self, partname, fqname, parent): - self.msgin(3, "import_module", partname, fqname, parent) - try: - m = self.modules[fqname] - except KeyError: - pass - else: - self.msgout(3, "import_module ->", m) - return m - if self.badmodules.has_key(fqname): - self.msgout(3, "import_module -> None") - return None - if parent and parent.__path__ is None: - self.msgout(3, "import_module -> None") - return None - try: - fp, pathname, stuff = self.find_module(partname, - parent and parent.__path__, parent) - except ImportError: - self.msgout(3, "import_module ->", None) - return None - try: - m = self.load_module(fqname, fp, pathname, stuff) - finally: - if fp: fp.close() - if parent: - setattr(parent, partname, m) - self.msgout(3, "import_module ->", m) - return m - - def load_module(self, fqname, fp, pathname, (suffix, mode, type)): - self.msgin(2, "load_module", fqname, fp and "fp", pathname) - if type == imp.PKG_DIRECTORY: - m = self.load_package(fqname, pathname) - self.msgout(2, "load_module ->", m) - return m - if type == imp.PY_SOURCE: - co = compile(fp.read()+'\n', pathname, 'exec') - elif type == imp.PY_COMPILED: - if fp.read(4) != imp.get_magic(): - self.msgout(2, "raise ImportError: Bad magic number", pathname) - raise ImportError, "Bad magic number in %s" % pathname - fp.read(4) - co = marshal.load(fp) - else: - co = None - m = self.add_module(fqname) - m.__file__ = pathname - if co: - if self.replace_paths: - co = self.replace_paths_in_code(co) - m.__code__ = co - self.scan_code(co, m) - self.msgout(2, "load_module ->", m) - return m - - def _add_badmodule(self, name, caller): - if name not in self.badmodules: - self.badmodules[name] = {} - if caller: - self.badmodules[name][caller.__name__] = 1 - else: - self.badmodules[name]["-"] = 1 - - def _safe_import_hook(self, name, caller, fromlist, level=-1): - # wrapper for self.import_hook() that won't raise ImportError - if name in self.badmodules: - self._add_badmodule(name, caller) - return - try: - self.import_hook(name, caller, level=level) - except ImportError, msg: - self.msg(2, "ImportError:", str(msg)) - self._add_badmodule(name, caller) - else: - if fromlist: - for sub in fromlist: - if sub in self.badmodules: - self._add_badmodule(sub, caller) - continue - try: - self.import_hook(name, caller, [sub], level=level) - except ImportError, msg: - self.msg(2, "ImportError:", str(msg)) - fullname = name + "." + sub - self._add_badmodule(fullname, caller) - - def scan_opcodes(self, co, - unpack = struct.unpack): - # Scan the code, and yield 'interesting' opcode combinations - # Version for Python 2.4 and older - code = co.co_code - names = co.co_names - consts = co.co_consts - while code: - c = code[0] - if c in STORE_OPS: - oparg, = unpack('= HAVE_ARGUMENT: - code = code[3:] - else: - code = code[1:] - - def scan_opcodes_25(self, co, - unpack = struct.unpack): - # Scan the code, and yield 'interesting' opcode combinations - # Python 2.5 version (has absolute and relative imports) - code = co.co_code - names = co.co_names - consts = co.co_consts - LOAD_LOAD_AND_IMPORT = LOAD_CONST + LOAD_CONST + IMPORT_NAME - while code: - c = code[0] - if c in STORE_OPS: - oparg, = unpack('= HAVE_ARGUMENT: - code = code[3:] - else: - code = code[1:] - - def scan_code(self, co, m): - if m.__name__ in self.skip_scan: - return - code = co.co_code - if sys.version_info >= (2, 5): - scanner = self.scan_opcodes_25 - else: - scanner = self.scan_opcodes - for what, args in scanner(co): - if what == "store": - name, = args - m.globalnames[name] = 1 - elif what in ("import", "absolute_import"): - fromlist, name = args - have_star = 0 - if fromlist is not None: - if "*" in fromlist: - have_star = 1 - fromlist = [f for f in fromlist if f != "*"] - if what == "absolute_import": level = 0 - else: level = -1 - self._safe_import_hook(name, m, fromlist, level=level) - if have_star: - # We've encountered an "import *". If it is a Python module, - # the code has already been parsed and we can suck out the - # global names. - mm = None - if m.__path__: - # At this point we don't know whether 'name' is a - # submodule of 'm' or a global module. Let's just try - # the full name first. - mm = self.modules.get(m.__name__ + "." + name) - if mm is None: - mm = self.modules.get(name) - if mm is not None: - m.globalnames.update(mm.globalnames) - m.starimports.update(mm.starimports) - if mm.__code__ is None: - m.starimports[name] = 1 - else: - m.starimports[name] = 1 - elif what == "relative_import": - level, fromlist, name = args - if name: - self._safe_import_hook(name, m, fromlist, level=level) - else: - parent = self.determine_parent(m, level=level) - self._safe_import_hook(parent.__name__, None, fromlist, level=0) - else: - # We don't expect anything else from the generator. - raise RuntimeError(what) - - for c in co.co_consts: - if isinstance(c, type(co)): - self.scan_code(c, m) - - def load_package(self, fqname, pathname): - self.msgin(2, "load_package", fqname, pathname) - newname = replacePackageMap.get(fqname) - if newname: - fqname = newname - m = self.add_module(fqname) - m.__file__ = pathname - m.__path__ = [pathname] - - # As per comment at top of file, simulate runtime __path__ additions. - m.__path__ = m.__path__ + packagePathMap.get(fqname, []) - - fp, buf, stuff = self.find_module("__init__", m.__path__) - self.load_module(fqname, fp, buf, stuff) - self.msgout(2, "load_package ->", m) - return m - - def add_module(self, fqname): - if self.modules.has_key(fqname): - return self.modules[fqname] - self.modules[fqname] = m = Module(fqname) - return m - - def find_module(self, name, path, parent=None): - if parent is not None: - # assert path is not None - fullname = parent.__name__+'.'+name - else: - fullname = name - if fullname in self.excludes: - self.msgout(3, "find_module -> Excluded", fullname) - raise ImportError, name - - if path is None: - if name in sys.builtin_module_names: - return (None, None, ("", "", imp.C_BUILTIN)) - - path = self.path - return imp.find_module(name, path) - - def report(self): - """Print a report to stdout, listing the found modules with their - paths, as well as modules that are missing, or seem to be missing. - """ - print - print " %-25s %s" % ("Name", "File") - print " %-25s %s" % ("----", "----") - # Print modules found - keys = self.modules.keys() - keys.sort() - for key in keys: - m = self.modules[key] - if m.__path__: - print "P", - else: - print "m", - print "%-25s" % key, m.__file__ or "" - - # Print missing modules - missing, maybe = self.any_missing_maybe() - if missing: - print - print "Missing modules:" - for name in missing: - mods = self.badmodules[name].keys() - mods.sort() - print "?", name, "imported from", ', '.join(mods) - # Print modules that may be missing, but then again, maybe not... - if maybe: - print - print "Submodules thay appear to be missing, but could also be", - print "global names in the parent package:" - for name in maybe: - mods = self.badmodules[name].keys() - mods.sort() - print "?", name, "imported from", ', '.join(mods) - - def any_missing(self): - """Return a list of modules that appear to be missing. Use - any_missing_maybe() if you want to know which modules are - certain to be missing, and which *may* be missing. - """ - missing, maybe = self.any_missing_maybe() - return missing + maybe - - def any_missing_maybe(self): - """Return two lists, one with modules that are certainly missing - and one with modules that *may* be missing. The latter names could - either be submodules *or* just global names in the package. - - The reason it can't always be determined is that it's impossible to - tell which names are imported when "from module import *" is done - with an extension module, short of actually importing it. - """ - missing = [] - maybe = [] - for name in self.badmodules: - if name in self.excludes: - continue - i = name.rfind(".") - if i < 0: - missing.append(name) - continue - subname = name[i+1:] - pkgname = name[:i] - pkg = self.modules.get(pkgname) - if pkg is not None: - if pkgname in self.badmodules[name]: - # The package tried to import this module itself and - # failed. It's definitely missing. - missing.append(name) - elif subname in pkg.globalnames: - # It's a global in the package: definitely not missing. - pass - elif pkg.starimports: - # It could be missing, but the package did an "import *" - # from a non-Python module, so we simply can't be sure. - maybe.append(name) - else: - # It's not a global in the package, the package didn't - # do funny star imports, it's very likely to be missing. - # The symbol could be inserted into the package from the - # outside, but since that's not good style we simply list - # it missing. - missing.append(name) - else: - missing.append(name) - missing.sort() - maybe.sort() - return missing, maybe - - def replace_paths_in_code(self, co): - new_filename = original_filename = os.path.normpath(co.co_filename) - for f, r in self.replace_paths: - if original_filename.startswith(f): - new_filename = r + original_filename[len(f):] - break - - if self.debug and original_filename not in self.processed_paths: - if new_filename != original_filename: - self.msgout(2, "co_filename %r changed to %r" \ - % (original_filename,new_filename,)) - else: - self.msgout(2, "co_filename %r remains unchanged" \ - % (original_filename,)) - self.processed_paths.append(original_filename) - - consts = list(co.co_consts) - for i in range(len(consts)): - if isinstance(consts[i], type(co)): - consts[i] = self.replace_paths_in_code(consts[i]) - - return types.CodeType(co.co_argcount, co.co_nlocals, co.co_stacksize, - co.co_flags, co.co_code, tuple(consts), co.co_names, - co.co_varnames, new_filename, co.co_name, - co.co_firstlineno, co.co_lnotab, - co.co_freevars, co.co_cellvars) - - -def test(): - # Parse command line - import getopt - try: - opts, args = getopt.getopt(sys.argv[1:], "dmp:qx:") - except getopt.error, msg: - print msg - return - - # Process options - debug = 1 - domods = 0 - addpath = [] - exclude = [] - for o, a in opts: - if o == '-d': - debug = debug + 1 - if o == '-m': - domods = 1 - if o == '-p': - addpath = addpath + a.split(os.pathsep) - if o == '-q': - debug = 0 - if o == '-x': - exclude.append(a) - - # Provide default arguments - if not args: - script = "hello.py" - else: - script = args[0] - - # Set the path based on sys.path and the script directory - path = sys.path[:] - path[0] = os.path.dirname(script) - path = addpath + path - if debug > 1: - print "path:" - for item in path: - print " ", repr(item) - - # Create the module finder and turn its crank - mf = ModuleFinder(path, debug, exclude) - for arg in args[1:]: - if arg == '-m': - domods = 1 - continue - if domods: - if arg[-2:] == '.*': - mf.import_hook(arg[:-2], None, ["*"]) - else: - mf.import_hook(arg) - else: - mf.load_file(arg) - mf.run_script(script) - mf.report() - return mf # for -i debugging - - -if __name__ == '__main__': - try: - mf = test() - except KeyboardInterrupt: - print "\n[interrupt]" - - - -# py2exe specific portion - this should be removed before inclusion in the -# Python distribution - -import tempfile -import urllib - -try: - set -except NameError: - from sets import Set as set - -Base = ModuleFinder -del ModuleFinder - -# Much inspired by Toby Dickenson's code: -# http://www.tarind.com/depgraph.html -class ModuleFinder(Base): - def __init__(self, *args, **kw): - self._depgraph = {} - self._types = {} - self._last_caller = None - self._scripts = set() - Base.__init__(self, *args, **kw) - - def run_script(self, pathname): - # Scripts always end in the __main__ module, but we possibly - # have more than one script in py2exe, so we want to keep - # *all* the pathnames. - self._scripts.add(pathname) - Base.run_script(self, pathname) - - def import_hook(self, name, caller=None, fromlist=None, level=-1): - old_last_caller = self._last_caller - try: - self._last_caller = caller - return Base.import_hook(self,name,caller,fromlist,level) - finally: - self._last_caller = old_last_caller - - def import_module(self,partnam,fqname,parent): - r = Base.import_module(self,partnam,fqname,parent) - if r is not None and self._last_caller: - self._depgraph.setdefault(self._last_caller.__name__, set()).add(r.__name__) - return r - - def load_module(self, fqname, fp, pathname, (suffix, mode, typ)): - r = Base.load_module(self, fqname, fp, pathname, (suffix, mode, typ)) - if r is not None: - self._types[r.__name__] = typ - return r - - def create_xref(self): - # this code probably needs cleanup - depgraph = {} - importedby = {} - for name, value in self._depgraph.items(): - depgraph[name] = list(value) - for needs in value: - importedby.setdefault(needs, set()).add(name) - - names = self._types.keys() - names.sort() - - fd, htmlfile = tempfile.mkstemp(".html") - ofi = open(htmlfile, "w") - os.close(fd) - print >> ofi, "py2exe cross reference for %s" % sys.argv[0] - - print >> ofi, "

py2exe cross reference for %s

" % sys.argv[0] - - for name in names: - if self._types[name] in (imp.PY_SOURCE, imp.PKG_DIRECTORY): - print >> ofi, '%s' % (name, name) - if name == "__main__": - for fname in self._scripts: - path = urllib.pathname2url(os.path.abspath(fname)) - print >> ofi, '%s ' \ - % (path, fname) - print >> ofi, '
imports:' - else: - fname = urllib.pathname2url(self.modules[name].__file__) - print >> ofi, '%s
imports:' \ - % (fname, self.modules[name].__file__) - else: - fname = self.modules[name].__file__ - if fname: - print >> ofi, '%s %s
imports:' \ - % (name, name, fname) - else: - print >> ofi, '%s %s
imports:' \ - % (name, name, TYPES[self._types[name]]) - - if name in depgraph: - needs = depgraph[name] - for n in needs: - print >> ofi, '%s ' % (n, n) - print >> ofi, "
\n" - - print >> ofi, 'imported by:' - if name in importedby: - for i in importedby[name]: - print >> ofi, '%s ' % (i, i) - - print >> ofi, "
\n" - - print >> ofi, "
\n" - - print >> ofi, "" - ofi.close() - os.startfile(htmlfile) - # how long does it take to start the browser? - import threading - threading.Timer(5, os.remove, args=[htmlfile]) - - -TYPES = {imp.C_BUILTIN: "(builtin module)", - imp.C_EXTENSION: "extension module", - imp.IMP_HOOK: "IMP_HOOK", - imp.PKG_DIRECTORY: "package directory", - imp.PY_CODERESOURCE: "PY_CODERESOURCE", - imp.PY_COMPILED: "compiled python module", - imp.PY_FROZEN: "frozen module", - imp.PY_RESOURCE: "PY_RESOURCE", - imp.PY_SOURCE: "python module", - imp.SEARCH_ERROR: "SEARCH_ERROR" - } +"""Find modules used by a script, using introspection.""" +# This module should be kept compatible with Python 2.2, see PEP 291. + + +import urllib.error +import urllib.parse +import urllib.request +import tempfile +from modulefinder import replacePackageMap +from modulefinder import packagePathMap +import dis +import imp +import marshal +import os +import sys +import types +import struct + +if hasattr(sys.__stdout__, "newlines"): + READ_MODE = "U" # universal line endings +else: + # remain compatible with Python < 2.3 + READ_MODE = "r" + +LOAD_CONST = chr(dis.opname.index('LOAD_CONST')) +IMPORT_NAME = chr(dis.opname.index('IMPORT_NAME')) +STORE_NAME = chr(dis.opname.index('STORE_NAME')) +STORE_GLOBAL = chr(dis.opname.index('STORE_GLOBAL')) +STORE_OPS = [STORE_NAME, STORE_GLOBAL] +HAVE_ARGUMENT = chr(dis.HAVE_ARGUMENT) + +# !!! NOTE BEFORE INCLUDING IN PYTHON DISTRIBUTION !!! +# To clear up issues caused by the duplication of data structures between +# the real Python modulefinder and this duplicate version, packagePathMap +# and replacePackageMap are imported from the actual modulefinder. This +# should be changed back to the assigments that are commented out below. +# There are also py2exe specific pieces at the bottom of this file. + + +# Modulefinder does a good job at simulating Python's, but it can not +# handle __path__ modifications packages make at runtime. Therefore there +# is a mechanism whereby you can register extra paths in this map for a +# package, and it will be honored. + +# Note this is a mapping is lists of paths. +# ~ packagePathMap = {} + +# A Public interface + +def AddPackagePath(packagename, path): + paths = packagePathMap.get(packagename, []) + paths.append(path) + packagePathMap[packagename] = paths + + +# ~ replacePackageMap = {} + +# This ReplacePackage mechanism allows modulefinder to work around the +# way the _xmlplus package injects itself under the name "xml" into +# sys.modules at runtime by calling ReplacePackage("_xmlplus", "xml") +# before running ModuleFinder. + + +def ReplacePackage(oldname, newname): + replacePackageMap[oldname] = newname + + +class Module: + + def __init__(self, name, file=None, path=None): + self.__name__ = name + self.__file__ = file + self.__path__ = path + self.__code__ = None + # The set of global names that are assigned to in the module. + # This includes those names imported through starimports of + # Python modules. + self.globalnames = {} + # The set of starimports this module did that could not be + # resolved, ie. a starimport from a non-Python module. + self.starimports = {} + + def __repr__(self): + s = "Module(%r" % (self.__name__,) + if self.__file__ is not None: + s = s + ", %r" % (self.__file__,) + if self.__path__ is not None: + s = s + ", %r" % (self.__path__,) + s = s + ")" + return s + + +class ModuleFinder: + + def __init__(self, path=None, debug=0, excludes=[], replace_paths=[], skip_scan=[]): + if path is None: + path = sys.path + self.path = path + self.modules = {} + self.badmodules = {} + self.debug = debug + self.indent = 0 + self.excludes = excludes + self.replace_paths = replace_paths + self.skip_scan = skip_scan + self.processed_paths = [] # Used in debugging only + + def msg(self, level, str, *args): + if level <= self.debug: + for i in range(self.indent): + print(" ", end=' ') + print(str, end=' ') + for arg in args: + print(repr(arg), end=' ') + print() + + def msgin(self, *args): + level = args[0] + if level <= self.debug: + self.indent = self.indent + 1 + self.msg(*args) + + def msgout(self, *args): + level = args[0] + if level <= self.debug: + self.indent = self.indent - 1 + self.msg(*args) + + def run_script(self, pathname): + self.msg(2, "run_script", pathname) + fp = open(pathname, READ_MODE) + stuff = ("", "r", imp.PY_SOURCE) + self.load_module('__main__', fp, pathname, stuff) + + def load_file(self, pathname): + dir, name = os.path.split(pathname) + name, ext = os.path.splitext(name) + fp = open(pathname, READ_MODE) + stuff = (ext, "r", imp.PY_SOURCE) + self.load_module(name, fp, pathname, stuff) + + def import_hook(self, name, caller=None, fromlist=None, level=-1): + self.msg(3, "import_hook", name, caller, fromlist, level) + parent = self.determine_parent(caller, level=level) + q, tail = self.find_head_package(parent, name) + m = self.load_tail(q, tail) + if not fromlist: + return q + if m.__path__: + self.ensure_fromlist(m, fromlist) + return None + + def determine_parent(self, caller, level=-1): + self.msgin(4, "determine_parent", caller, level) + if not caller or level == 0: + self.msgout(4, "determine_parent -> None") + return None + pname = caller.__name__ + if level >= 1: # relative import + if caller.__path__: + level -= 1 + if level == 0: + parent = self.modules[pname] + assert parent is caller + self.msgout(4, "determine_parent ->", parent) + return parent + if pname.count(".") < level: + raise ImportError("relative importpath too deep") + pname = ".".join(pname.split(".")[:-level]) + parent = self.modules[pname] + self.msgout(4, "determine_parent ->", parent) + return parent + if caller.__path__: + parent = self.modules[pname] + assert caller is parent + self.msgout(4, "determine_parent ->", parent) + return parent + if '.' in pname: + i = pname.rfind('.') + pname = pname[:i] + parent = self.modules[pname] + assert parent.__name__ == pname + self.msgout(4, "determine_parent ->", parent) + return parent + self.msgout(4, "determine_parent -> None") + return None + + def find_head_package(self, parent, name): + self.msgin(4, "find_head_package", parent, name) + if '.' in name: + i = name.find('.') + head = name[:i] + tail = name[i+1:] + else: + head = name + tail = "" + if parent: + qname = "%s.%s" % (parent.__name__, head) + else: + qname = head + q = self.import_module(head, qname, parent) + if q: + self.msgout(4, "find_head_package ->", (q, tail)) + return q, tail + if parent: + qname = head + parent = None + q = self.import_module(head, qname, parent) + if q: + self.msgout(4, "find_head_package ->", (q, tail)) + return q, tail + self.msgout(4, "raise ImportError: No module named", qname) + raise ImportError("No module named " + qname) + + def load_tail(self, q, tail): + self.msgin(4, "load_tail", q, tail) + m = q + while tail: + i = tail.find('.') + if i < 0: + i = len(tail) + head, tail = tail[:i], tail[i+1:] + mname = "%s.%s" % (m.__name__, head) + m = self.import_module(head, mname, m) + if not m: + self.msgout(4, "raise ImportError: No module named", mname) + raise ImportError("No module named " + mname) + self.msgout(4, "load_tail ->", m) + return m + + def ensure_fromlist(self, m, fromlist, recursive=0): + self.msg(4, "ensure_fromlist", m, fromlist, recursive) + for sub in fromlist: + if sub == "*": + if not recursive: + all = self.find_all_submodules(m) + if all: + self.ensure_fromlist(m, all, 1) + elif not hasattr(m, sub): + subname = "%s.%s" % (m.__name__, sub) + submod = self.import_module(sub, subname, m) + if not submod: + raise ImportError("No module named " + subname) + + def find_all_submodules(self, m): + if not m.__path__: + return + modules = {} + # 'suffixes' used to be a list hardcoded to [".py", ".pyc", ".pyo"]. + # But we must also collect Python extension modules - although + # we cannot separate normal dlls from Python extensions. + suffixes = [] + for triple in imp.get_suffixes(): + suffixes.append(triple[0]) + for dir in m.__path__: + try: + names = os.listdir(dir) + except os.error: + self.msg(2, "can't list directory", dir) + continue + for name in names: + mod = None + for suff in suffixes: + n = len(suff) + if name[-n:] == suff: + mod = name[:-n] + break + if mod and mod != "__init__": + modules[mod] = mod + return list(modules.keys()) + + def import_module(self, partname, fqname, parent): + self.msgin(3, "import_module", partname, fqname, parent) + try: + m = self.modules[fqname] + except KeyError: + pass + else: + self.msgout(3, "import_module ->", m) + return m + if fqname in self.badmodules: + self.msgout(3, "import_module -> None") + return None + if parent and parent.__path__ is None: + self.msgout(3, "import_module -> None") + return None + try: + fp, pathname, stuff = self.find_module(partname, + parent and parent.__path__, parent) + except ImportError: + self.msgout(3, "import_module ->", None) + return None + try: + m = self.load_module(fqname, fp, pathname, stuff) + finally: + if fp: + fp.close() + if parent: + setattr(parent, partname, m) + self.msgout(3, "import_module ->", m) + return m + + def load_module(self, fqname, fp, pathname, xxx_todo_changeme): + (suffix, mode, type) = xxx_todo_changeme + self.msgin(2, "load_module", fqname, fp and "fp", pathname) + if type == imp.PKG_DIRECTORY: + m = self.load_package(fqname, pathname) + self.msgout(2, "load_module ->", m) + return m + if type == imp.PY_SOURCE: + co = compile(fp.read()+'\n', pathname, 'exec') + elif type == imp.PY_COMPILED: + if fp.read(4) != imp.get_magic(): + self.msgout(2, "raise ImportError: Bad magic number", pathname) + raise ImportError("Bad magic number in %s" % pathname) + fp.read(4) + co = marshal.load(fp) + else: + co = None + m = self.add_module(fqname) + m.__file__ = pathname + if co: + if self.replace_paths: + co = self.replace_paths_in_code(co) + m.__code__ = co + self.scan_code(co, m) + self.msgout(2, "load_module ->", m) + return m + + def _add_badmodule(self, name, caller): + if name not in self.badmodules: + self.badmodules[name] = {} + if caller: + self.badmodules[name][caller.__name__] = 1 + else: + self.badmodules[name]["-"] = 1 + + def _safe_import_hook(self, name, caller, fromlist, level=-1): + # wrapper for self.import_hook() that won't raise ImportError + if name in self.badmodules: + self._add_badmodule(name, caller) + return + try: + self.import_hook(name, caller, level=level) + except ImportError as msg: + self.msg(2, "ImportError:", str(msg)) + self._add_badmodule(name, caller) + else: + if fromlist: + for sub in fromlist: + if sub in self.badmodules: + self._add_badmodule(sub, caller) + continue + try: + self.import_hook(name, caller, [sub], level=level) + except ImportError as msg: + self.msg(2, "ImportError:", str(msg)) + fullname = name + "." + sub + self._add_badmodule(fullname, caller) + + def scan_opcodes(self, co, + unpack=struct.unpack): + # Scan the code, and yield 'interesting' opcode combinations + # Version for Python 2.4 and older + code = co.co_code + names = co.co_names + consts = co.co_consts + while code: + c = code[0] + if c in STORE_OPS: + oparg, = unpack('= HAVE_ARGUMENT: + code = code[3:] + else: + code = code[1:] + + def scan_opcodes_25(self, co, + unpack=struct.unpack): + # Scan the code, and yield 'interesting' opcode combinations + # Python 2.5 version (has absolute and relative imports) + code = co.co_code + names = co.co_names + consts = co.co_consts + LOAD_LOAD_AND_IMPORT = LOAD_CONST + LOAD_CONST + IMPORT_NAME + while code: + c = code[0] + if c in STORE_OPS: + oparg, = unpack('= HAVE_ARGUMENT: + code = code[3:] + else: + code = code[1:] + + def scan_code(self, co, m): + if m.__name__ in self.skip_scan: + return + code = co.co_code + if sys.version_info >= (2, 5): + scanner = self.scan_opcodes_25 + else: + scanner = self.scan_opcodes + for what, args in scanner(co): + if what == "store": + name, = args + m.globalnames[name] = 1 + elif what in ("import", "absolute_import"): + fromlist, name = args + have_star = 0 + if fromlist is not None: + if "*" in fromlist: + have_star = 1 + fromlist = [f for f in fromlist if f != "*"] + if what == "absolute_import": + level = 0 + else: + level = -1 + self._safe_import_hook(name, m, fromlist, level=level) + if have_star: + # We've encountered an "import *". If it is a Python module, + # the code has already been parsed and we can suck out the + # global names. + mm = None + if m.__path__: + # At this point we don't know whether 'name' is a + # submodule of 'm' or a global module. Let's just try + # the full name first. + mm = self.modules.get(m.__name__ + "." + name) + if mm is None: + mm = self.modules.get(name) + if mm is not None: + m.globalnames.update(mm.globalnames) + m.starimports.update(mm.starimports) + if mm.__code__ is None: + m.starimports[name] = 1 + else: + m.starimports[name] = 1 + elif what == "relative_import": + level, fromlist, name = args + if name: + self._safe_import_hook(name, m, fromlist, level=level) + else: + parent = self.determine_parent(m, level=level) + self._safe_import_hook( + parent.__name__, None, fromlist, level=0) + else: + # We don't expect anything else from the generator. + raise RuntimeError(what) + + for c in co.co_consts: + if isinstance(c, type(co)): + self.scan_code(c, m) + + def load_package(self, fqname, pathname): + self.msgin(2, "load_package", fqname, pathname) + newname = replacePackageMap.get(fqname) + if newname: + fqname = newname + m = self.add_module(fqname) + m.__file__ = pathname + m.__path__ = [pathname] + + # As per comment at top of file, simulate runtime __path__ additions. + m.__path__ = m.__path__ + packagePathMap.get(fqname, []) + + fp, buf, stuff = self.find_module("__init__", m.__path__) + self.load_module(fqname, fp, buf, stuff) + self.msgout(2, "load_package ->", m) + return m + + def add_module(self, fqname): + if fqname in self.modules: + return self.modules[fqname] + self.modules[fqname] = m = Module(fqname) + return m + + def find_module(self, name, path, parent=None): + if parent is not None: + # assert path is not None + fullname = parent.__name__+'.'+name + else: + fullname = name + if fullname in self.excludes: + self.msgout(3, "find_module -> Excluded", fullname) + raise ImportError(name) + + if path is None: + if name in sys.builtin_module_names: + return (None, None, ("", "", imp.C_BUILTIN)) + + path = self.path + return imp.find_module(name, path) + + def report(self): + """Print a report to stdout, listing the found modules with their + paths, as well as modules that are missing, or seem to be missing. + """ + print() + print(" %-25s %s" % ("Name", "File")) + print(" %-25s %s" % ("----", "----")) + # Print modules found + keys = list(self.modules.keys()) + keys.sort() + for key in keys: + m = self.modules[key] + if m.__path__: + print("P", end=' ') + else: + print("m", end=' ') + print("%-25s" % key, m.__file__ or "") + + # Print missing modules + missing, maybe = self.any_missing_maybe() + if missing: + print() + print("Missing modules:") + for name in missing: + mods = list(self.badmodules[name].keys()) + mods.sort() + print("?", name, "imported from", ', '.join(mods)) + # Print modules that may be missing, but then again, maybe not... + if maybe: + print() + print("Submodules thay appear to be missing, but could also be", end=' ') + print("global names in the parent package:") + for name in maybe: + mods = list(self.badmodules[name].keys()) + mods.sort() + print("?", name, "imported from", ', '.join(mods)) + + def any_missing(self): + """Return a list of modules that appear to be missing. Use + any_missing_maybe() if you want to know which modules are + certain to be missing, and which *may* be missing. + """ + missing, maybe = self.any_missing_maybe() + return missing + maybe + + def any_missing_maybe(self): + """Return two lists, one with modules that are certainly missing + and one with modules that *may* be missing. The latter names could + either be submodules *or* just global names in the package. + + The reason it can't always be determined is that it's impossible to + tell which names are imported when "from module import *" is done + with an extension module, short of actually importing it. + """ + missing = [] + maybe = [] + for name in self.badmodules: + if name in self.excludes: + continue + i = name.rfind(".") + if i < 0: + missing.append(name) + continue + subname = name[i+1:] + pkgname = name[:i] + pkg = self.modules.get(pkgname) + if pkg is not None: + if pkgname in self.badmodules[name]: + # The package tried to import this module itself and + # failed. It's definitely missing. + missing.append(name) + elif subname in pkg.globalnames: + # It's a global in the package: definitely not missing. + pass + elif pkg.starimports: + # It could be missing, but the package did an "import *" + # from a non-Python module, so we simply can't be sure. + maybe.append(name) + else: + # It's not a global in the package, the package didn't + # do funny star imports, it's very likely to be missing. + # The symbol could be inserted into the package from the + # outside, but since that's not good style we simply list + # it missing. + missing.append(name) + else: + missing.append(name) + missing.sort() + maybe.sort() + return missing, maybe + + def replace_paths_in_code(self, co): + new_filename = original_filename = os.path.normpath(co.co_filename) + for f, r in self.replace_paths: + if original_filename.startswith(f): + new_filename = r + original_filename[len(f):] + break + + if self.debug and original_filename not in self.processed_paths: + if new_filename != original_filename: + self.msgout(2, "co_filename %r changed to %r" + % (original_filename, new_filename,)) + else: + self.msgout(2, "co_filename %r remains unchanged" + % (original_filename,)) + self.processed_paths.append(original_filename) + + consts = list(co.co_consts) + for i in range(len(consts)): + if isinstance(consts[i], type(co)): + consts[i] = self.replace_paths_in_code(consts[i]) + + return types.CodeType(co.co_argcount, co.co_nlocals, co.co_stacksize, + co.co_flags, co.co_code, tuple( + consts), co.co_names, + co.co_varnames, new_filename, co.co_name, + co.co_firstlineno, co.co_lnotab, + co.co_freevars, co.co_cellvars) + + +def test(): + # Parse command line + import getopt + try: + opts, args = getopt.getopt(sys.argv[1:], "dmp:qx:") + except getopt.error as msg: + print(msg) + return + + # Process options + debug = 1 + domods = 0 + addpath = [] + exclude = [] + for o, a in opts: + if o == '-d': + debug = debug + 1 + if o == '-m': + domods = 1 + if o == '-p': + addpath = addpath + a.split(os.pathsep) + if o == '-q': + debug = 0 + if o == '-x': + exclude.append(a) + + # Provide default arguments + if not args: + script = "hello.py" + else: + script = args[0] + + # Set the path based on sys.path and the script directory + path = sys.path[:] + path[0] = os.path.dirname(script) + path = addpath + path + if debug > 1: + print("path:") + for item in path: + print(" ", repr(item)) + + # Create the module finder and turn its crank + mf = ModuleFinder(path, debug, exclude) + for arg in args[1:]: + if arg == '-m': + domods = 1 + continue + if domods: + if arg[-2:] == '.*': + mf.import_hook(arg[:-2], None, ["*"]) + else: + mf.import_hook(arg) + else: + mf.load_file(arg) + mf.run_script(script) + mf.report() + return mf # for -i debugging + + +if __name__ == '__main__': + try: + mf = test() + except KeyboardInterrupt: + print("\n[interrupt]") + + +# py2exe specific portion - this should be removed before inclusion in the +# Python distribution + + +try: + set +except NameError: + from sets import Set as set + +Base = ModuleFinder +del ModuleFinder + +# Much inspired by Toby Dickenson's code: +# http://www.tarind.com/depgraph.html + + +class ModuleFinder(Base): + def __init__(self, *args, **kw): + self._depgraph = {} + self._types = {} + self._last_caller = None + self._scripts = set() + Base.__init__(self, *args, **kw) + + def run_script(self, pathname): + # Scripts always end in the __main__ module, but we possibly + # have more than one script in py2exe, so we want to keep + # *all* the pathnames. + self._scripts.add(pathname) + Base.run_script(self, pathname) + + def import_hook(self, name, caller=None, fromlist=None, level=-1): + old_last_caller = self._last_caller + try: + self._last_caller = caller + return Base.import_hook(self, name, caller, fromlist, level) + finally: + self._last_caller = old_last_caller + + def import_module(self, partnam, fqname, parent): + r = Base.import_module(self, partnam, fqname, parent) + if r is not None and self._last_caller: + self._depgraph.setdefault( + self._last_caller.__name__, set()).add(r.__name__) + return r + + def load_module(self, fqname, fp, pathname, xxx_todo_changeme1): + (suffix, mode, typ) = xxx_todo_changeme1 + r = Base.load_module(self, fqname, fp, pathname, (suffix, mode, typ)) + if r is not None: + self._types[r.__name__] = typ + return r + + def create_xref(self): + # this code probably needs cleanup + depgraph = {} + importedby = {} + for name, value in list(self._depgraph.items()): + depgraph[name] = list(value) + for needs in value: + importedby.setdefault(needs, set()).add(name) + + names = list(self._types.keys()) + names.sort() + + fd, htmlfile = tempfile.mkstemp(".html") + ofi = open(htmlfile, "w") + os.close(fd) + print("py2exe cross reference for %s" % + sys.argv[0], file=ofi) + + print("

py2exe cross reference for %s

" % sys.argv[0], file=ofi) + + for name in names: + if self._types[name] in (imp.PY_SOURCE, imp.PKG_DIRECTORY): + print('%s' % + (name, name), file=ofi) + if name == "__main__": + for fname in self._scripts: + path = urllib.request.pathname2url( + os.path.abspath(fname)) + print('%s ' + % (path, fname), file=ofi) + print('
imports:', file=ofi) + else: + fname = urllib.request.pathname2url( + self.modules[name].__file__) + print('%s
imports:' + % (fname, self.modules[name].__file__), file=ofi) + else: + fname = self.modules[name].__file__ + if fname: + print('%s %s
imports:' + % (name, name, fname), file=ofi) + else: + print('%s %s
imports:' + % (name, name, TYPES[self._types[name]]), file=ofi) + + if name in depgraph: + needs = depgraph[name] + for n in needs: + print('%s ' % (n, n), file=ofi) + print("
\n", file=ofi) + + print('imported by:', file=ofi) + if name in importedby: + for i in importedby[name]: + print('%s ' % (i, i), file=ofi) + + print("
\n", file=ofi) + + print("
\n", file=ofi) + + print("", file=ofi) + ofi.close() + os.startfile(htmlfile) + # how long does it take to start the browser? + import threading + threading.Timer(5, os.remove, args=[htmlfile]) + + +TYPES = {imp.C_BUILTIN: "(builtin module)", + imp.C_EXTENSION: "extension module", + imp.IMP_HOOK: "IMP_HOOK", + imp.PKG_DIRECTORY: "package directory", + imp.PY_CODERESOURCE: "PY_CODERESOURCE", + imp.PY_COMPILED: "compiled python module", + imp.PY_FROZEN: "frozen module", + imp.PY_RESOURCE: "PY_RESOURCE", + imp.PY_SOURCE: "python module", + imp.SEARCH_ERROR: "SEARCH_ERROR" + } diff --git a/popups.py b/popups.py index 5a55858..6c15d19 100644 --- a/popups.py +++ b/popups.py @@ -1,225 +1,410 @@ -import wx -import webbrowser -from settings import settings - -BLANK = 'about:blank' -COMMAND_CLOSE = 'http://close/' -COMMAND_NEXT = 'http://next/' -COMMAND_PREVIOUS = 'http://previous/' -COMMAND_FIRST = 'http://first/' -COMMAND_LAST = 'http://last/' -COMMAND_PLAY = 'http://play/' -COMMAND_PAUSE = 'http://pause/' - -def position_window(window): - index = settings.POPUP_DISPLAY - if index >= wx.Display_GetCount(): - index = 0 - display = wx.Display(index) - x, y, w, h = display.GetClientArea() - cw, ch = window.GetSize() - pad = 10 - x1 = x + pad - y1 = y + pad - x2 = x + w - cw - pad - y2 = y + h - ch - pad - x3 = x + w / 2 - cw / 2 - y3 = y + h / 2 - ch / 2 - lookup = { - (-1, -1): (x1, y1), - (1, -1): (x2, y1), - (-1, 1): (x1, y2), - (1, 1): (x2, y2), - (0, 0): (x3, y3), - } - window.SetPosition(lookup[settings.POPUP_POSITION]) - -class Event(wx.PyEvent): - def __init__(self, event_object, type): - super(Event, self).__init__() - self.SetEventType(type.typeId) - self.SetEventObject(event_object) - -EVT_LINK = wx.PyEventBinder(wx.NewEventType()) -EVT_POPUP_CLOSE = wx.PyEventBinder(wx.NewEventType()) -EVT_POPUP_ENTER = wx.PyEventBinder(wx.NewEventType()) -EVT_POPUP_LEAVE = wx.PyEventBinder(wx.NewEventType()) - -class PopupManager(wx.EvtHandler): - def __init__(self): - super(PopupManager, self).__init__() - self.timer = None - self.auto = settings.POPUP_AUTO_PLAY - self.cache = {} - self.hover_count = 0 - def set_items(self, items, index=0, focus=False): - self.items = list(items) - self.index = index - self.count = len(self.items) - self.clear_cache(keep_current_item=True) - self.update(focus) - self.set_timer() - def update(self, focus=False): - item = self.items[self.index] - if item in self.cache: - self.show_frame(focus) - self.update_cache() - else: - self.update_cache(True) - self.show_frame(focus) - self.update_cache() - def update_cache(self, current_only=False): - indexes = set() - indexes.add(self.index) - if not current_only: - indexes.add(self.index - 1) - indexes.add(self.index + 1) - #indexes.add(0) - #indexes.add(self.count - 1) - items = set(self.items[index] for index in indexes if index >= 0 and index < self.count) - for item in items: - if item in self.cache: - continue - frame = self.create_frame(item) - self.cache[item] = frame - for item, frame in self.cache.items(): - if item not in items: - frame.Close() - del self.cache[item] - def clear_cache(self, keep_current_item=False): - current_item = self.items[self.index] - for item, frame in self.cache.items(): - if keep_current_item and item == current_item: - continue - frame.Close() - del self.cache[item] - def show_frame(self, focus=False): - current_item = self.items[self.index] - current_item.read = True - for item, frame in self.cache.items(): - if item == current_item: - if focus: - frame.Show() - else: - frame.Disable() - frame.Show() - frame.Enable() - frame.Update() - if settings.POPUP_TRANSPARENCY < 255: - frame.SetTransparent(settings.POPUP_TRANSPARENCY) - for item, frame in self.cache.items(): - if item != current_item: - frame.Hide() - def create_frame(self, item): - if True:#settings.POPUP_THEME == 'default': - import theme_default - context = self.create_context(item) - frame = theme_default.Frame(item, context) - frame.Bind(EVT_LINK, self.on_link) - frame.Bind(EVT_POPUP_ENTER, self.on_enter) - frame.Bind(EVT_POPUP_LEAVE, self.on_leave) - position_window(frame) - if settings.POPUP_TRANSPARENCY < 255: - frame.SetTransparent(0) - return frame - def create_context(self, item): - context = {} - count = str(self.count) - index = str(self.items.index(item) + 1) - index = '%s%s' % ('0' * (len(count) - len(index)), index) - context['item_index'] = index - context['item_count'] = count - context['is_playing'] = self.auto - context['is_paused'] = not self.auto - context['POPUP_WIDTH'] = settings.POPUP_WIDTH - context['COMMAND_CLOSE'] = COMMAND_CLOSE - context['COMMAND_NEXT'] = COMMAND_NEXT - context['COMMAND_PREVIOUS'] = COMMAND_PREVIOUS - context['COMMAND_FIRST'] = COMMAND_FIRST - context['COMMAND_LAST'] = COMMAND_LAST - context['COMMAND_PLAY'] = COMMAND_PLAY - context['COMMAND_PAUSE'] = COMMAND_PAUSE - return context - def set_timer(self): - if self.timer and self.timer.IsRunning(): - return - duration = settings.POPUP_DURATION * 1000 - self.timer = wx.CallLater(duration, self.on_timer) - def stop_timer(self): - if self.timer and self.timer.IsRunning(): - self.timer.Stop() - self.timer = None - def on_enter(self, event): - event.Skip() - self.hover_count += 1 - def on_leave(self, event): - event.Skip() - self.hover_count -= 1 - def on_link(self, event): - link = event.link - # track the click - item = self.items[self.index] - feed = item.feed - if link == item.link or link == feed.link: - feed.clicks += 1 - # handle the click - if link == BLANK: - event.Skip() - elif link == COMMAND_CLOSE: - self.on_close() - elif link == COMMAND_FIRST: - self.auto = False - self.on_first() - elif link == COMMAND_LAST: - self.auto = False - self.on_last() - elif link == COMMAND_NEXT: - self.auto = False - self.on_next() - elif link == COMMAND_PREVIOUS: - self.auto = False - self.on_previous() - elif link == COMMAND_PLAY: - if not self.auto: - self.auto = True - self.stop_timer() - self.on_timer() - elif link == COMMAND_PAUSE: - self.auto = False - else: - webbrowser.open(link) - def on_first(self): - self.index = 0 - self.update(True) - def on_last(self): - self.index = self.count - 1 - self.update(True) - def on_next(self, focus=True): - if self.index < self.count - 1: - self.index += 1 - self.update(focus) - else: - self.on_close() - def on_previous(self): - if self.index > 0: - self.index -= 1 - self.update(True) - def on_close(self): - self.stop_timer() - self.clear_cache() - event = Event(self, EVT_POPUP_CLOSE) - wx.PostEvent(self, event) - def on_timer(self): - self.timer = None - set_timer = False - if self.hover_count and settings.POPUP_WAIT_ON_HOVER: - set_timer = True - elif self.auto: - if self.index == self.count - 1: - self.on_close() - else: - self.on_next(False) - set_timer = True - if set_timer: - self.set_timer() - \ No newline at end of file +# -*- coding: utf-8 -*- + +"""[summary] + +Returns: + [type] -- [description] +""" + + +import webbrowser + +import wx + +import theme_default + +from settings import settings + +BLANK = 'about:blank' +COMMAND_CLOSE = 'http://close/' +COMMAND_NEXT = 'http://next/' +COMMAND_PREVIOUS = 'http://previous/' +COMMAND_FIRST = 'http://first/' +COMMAND_LAST = 'http://last/' +COMMAND_PLAY = 'http://play/' +COMMAND_PAUSE = 'http://pause/' + + +def position_window(window): + """[summary] + + Arguments: + window {[type]} -- [description] + """ + + index = settings.POPUP_DISPLAY + + if index >= wx.Display_GetCount(): + index = 0 + + display = wx.Display(index) + x, y, w, h = display.GetClientArea() + cw, ch = window.GetSize() + pad = 10 + x1 = x + pad + y1 = y + pad + x2 = x + w - cw - pad + y2 = y + h - ch - pad + x3 = x + w / 2 - cw / 2 + y3 = y + h / 2 - ch / 2 + + lookup = { + (-1, -1): (x1, y1), + (1, -1): (x2, y1), + (-1, 1): (x1, y2), + (1, 1): (x2, y2), + (0, 0): (x3, y3), + } + + window.SetPosition(lookup[settings.POPUP_POSITION]) + + +class Event(wx.PyEvent): + """[summary] + + Arguments: + wx {[type]} -- [description] + """ + + def __init__(self, event_object, type): + """[summary] + + Arguments: + event_object {[type]} -- [description] + type {[type]} -- [description] + """ + + super(Event, self).__init__() + + self.SetEventType(type.typeId) + self.SetEventObject(event_object) + + +EVT_LINK = wx.PyEventBinder(wx.NewEventType()) +EVT_POPUP_CLOSE = wx.PyEventBinder(wx.NewEventType()) +EVT_POPUP_ENTER = wx.PyEventBinder(wx.NewEventType()) +EVT_POPUP_LEAVE = wx.PyEventBinder(wx.NewEventType()) + + +class PopupManager(wx.EvtHandler): + """[summary] + + Arguments: + wx {[type]} -- [description] + """ + + def __init__(self): + """[summary] + """ + + super(PopupManager, self).__init__() + + self.timer = None + self.auto = settings.POPUP_AUTO_PLAY + self.cache = {} + self.hover_count = 0 + + def set_items(self, items, index=0, focus=False): + """[summary] + + Arguments: + items {[type]} -- [description] + + Keyword Arguments: + index {int} -- [description] (default: {0}) + focus {bool} -- [description] (default: {False}) + """ + + self.items = list(items) + self.index = index + self.count = len(self.items) + self.clear_cache(keep_current_item=True) + self.update(focus) + self.set_timer() + + def update(self, focus=False): + """[summary] + + Keyword Arguments: + focus {bool} -- [description] (default: {False}) + """ + + item = self.items[self.index] + + if item in self.cache: + self.show_frame(focus) + self.update_cache() + else: + self.update_cache(True) + self.show_frame(focus) + self.update_cache() + + def update_cache(self, current_only=False): + """[summary] + + Keyword Arguments: + current_only {bool} -- [description] (default: {False}) + """ + + indexes = set() + indexes.add(self.index) + + if not current_only: + indexes.add(self.index - 1) + indexes.add(self.index + 1) + # indexes.add(0) + # indexes.add(self.count - 1) + + items = set(self.items[index] + for index in indexes if index >= 0 and index < self.count) + + for item in items: + if item in self.cache: + continue + frame = self.create_frame(item) + self.cache[item] = frame + + for item, frame in list(self.cache.items()): + if item not in items: + frame.Close() + del self.cache[item] + + def clear_cache(self, keep_current_item=False): + """[summary] + + Keyword Arguments: + keep_current_item {bool} -- [description] (default: {False}) + """ + + current_item = self.items[self.index] + + for item, frame in list(self.cache.items()): + if keep_current_item and item == current_item: + continue + frame.Close() + del self.cache[item] + + def show_frame(self, focus=False): + """[summary] + + Keyword Arguments: + focus {bool} -- [description] (default: {False}) + """ + + current_item = self.items[self.index] + current_item.read = True + + for item, frame in list(self.cache.items()): + if item == current_item: + if focus: + frame.Show() + else: + frame.Disable() + frame.Show() + frame.Enable() + frame.Update() + if settings.POPUP_TRANSPARENCY < 255: + frame.SetTransparent(settings.POPUP_TRANSPARENCY) + + for item, frame in list(self.cache.items()): + if item != current_item: + frame.Hide() + + def create_frame(self, item): + """[summary] + + Arguments: + item {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + if True: # settings.POPUP_THEME == 'default': + # import theme_default # FIXME: delete this + context = self.create_context(item) + frame = theme_default.Frame(item, context) + frame.Bind(EVT_LINK, self.on_link) + frame.Bind(EVT_POPUP_ENTER, self.on_enter) + frame.Bind(EVT_POPUP_LEAVE, self.on_leave) + + position_window(frame) + + if settings.POPUP_TRANSPARENCY < 255: + frame.SetTransparent(0) + + return frame + + def create_context(self, item): + """[summary] + + Arguments: + item {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + context = {} + count = str(self.count) + index = str(self.items.index(item) + 1) + index = '%s%s' % ('0' * (len(count) - len(index)), index) + context['item_index'] = index + context['item_count'] = count + context['is_playing'] = self.auto + context['is_paused'] = not self.auto + context['POPUP_WIDTH'] = settings.POPUP_WIDTH + context['COMMAND_CLOSE'] = COMMAND_CLOSE + context['COMMAND_NEXT'] = COMMAND_NEXT + context['COMMAND_PREVIOUS'] = COMMAND_PREVIOUS + context['COMMAND_FIRST'] = COMMAND_FIRST + context['COMMAND_LAST'] = COMMAND_LAST + context['COMMAND_PLAY'] = COMMAND_PLAY + context['COMMAND_PAUSE'] = COMMAND_PAUSE + + return context + + def set_timer(self): + """[summary] + """ + + if self.timer and self.timer.IsRunning(): + return + duration = settings.POPUP_DURATION * 1000 + self.timer = wx.CallLater(duration, self.on_timer) + + def stop_timer(self): + """[summary] + """ + + if self.timer and self.timer.IsRunning(): + self.timer.Stop() + self.timer = None + + def on_enter(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + event.Skip() + self.hover_count += 1 + + def on_leave(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + event.Skip() + self.hover_count -= 1 + + def on_link(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + link = event.link + # track the click + item = self.items[self.index] + feed = item.feed + + if link == item.link or link == feed.link: + feed.clicks += 1 + + # handle the click + if link == BLANK: + event.Skip() + elif link == COMMAND_CLOSE: + self.on_close() + elif link == COMMAND_FIRST: + self.auto = False + self.on_first() + elif link == COMMAND_LAST: + self.auto = False + self.on_last() + elif link == COMMAND_NEXT: + self.auto = False + self.on_next() + elif link == COMMAND_PREVIOUS: + self.auto = False + self.on_previous() + elif link == COMMAND_PLAY: + if not self.auto: + self.auto = True + self.stop_timer() + self.on_timer() + elif link == COMMAND_PAUSE: + self.auto = False + else: + webbrowser.open(link) + + def on_first(self): + """[summary] + """ + + self.index = 0 + self.update(True) + + def on_last(self): + """[summary] + """ + + self.index = self.count - 1 + self.update(True) + + def on_next(self, focus=True): + """[summary] + + Keyword Arguments: + focus {bool} -- [description] (default: {True}) + """ + + if self.index < self.count - 1: + self.index += 1 + self.update(focus) + else: + self.on_close() + + def on_previous(self): + """[summary] + """ + + if self.index > 0: + self.index -= 1 + self.update(True) + + def on_close(self): + """[summary] + """ + + self.stop_timer() + self.clear_cache() + + event = Event(self, EVT_POPUP_CLOSE) + wx.PostEvent(self, event) + + def on_timer(self): + """[summary] + """ + + self.timer = None + set_timer = False + + if self.hover_count and settings.POPUP_WAIT_ON_HOVER: + set_timer = True + elif self.auto: + if self.index == self.count - 1: + self.on_close() + else: + self.on_next(False) + set_timer = True + + if set_timer: + self.set_timer() + +# EOF diff --git a/safe_pickle.py b/safe_pickle.py index c1fb01d..03ff919 100644 --- a/safe_pickle.py +++ b/safe_pickle.py @@ -1,37 +1,75 @@ -import os -import cPickle as pickle - -def load(path): - tmp_path = '%s.tmp' % path - bak_path = '%s.bak' % path - for p in (path, bak_path, tmp_path): - try: - with open(p, 'rb') as file: - return pickle.load(file) - except Exception: - pass - raise Exception('Unable to load: %s' % path) - -def save(path, data): - tmp_path = '%s.tmp' % path - bak_path = '%s.bak' % path - # Write tmp file - with open(tmp_path, 'wb') as file: - pickle.dump(data, file, -1) - # Copy existing file to bak file - try: - os.remove(bak_path) - except Exception: - pass - try: - os.rename(path, bak_path) - except Exception: - pass - # Rename tmp file to actual file - os.rename(tmp_path, path) - # Remove bak file - try: - os.remove(bak_path) - except Exception: - pass - \ No newline at end of file +# -*- coding: utf-8 -*- + +"""[summary] + +Returns: + [type] -- [description] +""" + + +import os +import pickle as pickle + + +def load(path): + """[summary] + + Arguments: + path {[type]} -- [description] + + Raises: + Exception: [description] + + Returns: + [type] -- [description] + """ + + tmp_path = '%s.tmp' % path + bak_path = '%s.bak' % path + + for p in (path, bak_path, tmp_path): + try: + with open(p, 'rb') as file: + return pickle.load(file) + except Exception: + pass + + raise Exception('Unable to load: %s' % path) + + +def save(path, data): + """[summary] + + Arguments: + path {[type]} -- [description] + data {[type]} -- [description] + """ + + tmp_path = '%s.tmp' % path + bak_path = '%s.bak' % path + + # Write tmp file + with open(tmp_path, 'wb') as file: + pickle.dump(data, file, -1) + + # Copy existing file to bak file + try: + os.remove(bak_path) + except Exception: + pass + + try: + os.rename(path, bak_path) + except Exception: + pass + + # Rename tmp file to actual file + os.rename(tmp_path, path) + + # Remove bak file + try: + os.remove(bak_path) + except Exception: + pass + +# EOF diff --git a/settings.py b/settings.py index 302c1da..39fb93d 100644 --- a/settings.py +++ b/settings.py @@ -1,76 +1,266 @@ -import safe_pickle - -class InvalidSettingError(Exception): - pass - -class NOT_SET(object): - pass - -class Settings(object): - def __init__(self, parent): - self._parent = parent - def __getattr__(self, name): - if name.startswith('_'): - return super(Settings, self).__getattr__(name) - value = self.get(name) - if value != NOT_SET: - return value - if self._parent: - return getattr(self._parent, name) - raise InvalidSettingError, 'Invalid setting: %s' % name - def __setattr__(self, name, value): - if name.startswith('_'): - super(Settings, self).__setattr__(name, value) - return - if self.set(name, value): - return - if self._parent: - setattr(self._parent, name, value) - return - raise InvalidSettingError, 'Invalid setting: %s' % name - def get(self, name): - raise NotImplementedError, 'Settings subclasses must implement the get() method.' - def set(self, name, value): - raise NotImplementedError, 'Settings subclasses must implement the set() method.' - -class ModuleSettings(Settings): - def __init__(self, parent, module): - super(ModuleSettings, self).__init__(parent) - self._module = module - def get(self, name): - module = self._module - if hasattr(module, name): - return getattr(module, name) - return NOT_SET - def set(self, name, value): - return False - -class FileSettings(Settings): - def __init__(self, parent, file): - super(FileSettings, self).__init__(parent) - self._file = file - self.load() - def load(self): - try: - self._settings = safe_pickle.load(self._file) - except Exception: - self._settings = {} - def save(self): - safe_pickle.save(self._file, self._settings) - def get(self, name): - if name in self._settings: - return self._settings[name] - return NOT_SET - def set(self, name, value): - if value != getattr(self, name): - self._settings[name] = value - self.save() - return True - -def create_chain(): - import defaults - settings = ModuleSettings(None, defaults) - settings = FileSettings(settings, 'settings.dat') - return settings - -settings = create_chain() +# -*- coding: utf-8 -*- + +"""[summary] + +Returns: + [type] -- [description] +""" + + +import defaults +import logging +import safe_pickle + + +class InvalidSettingError(Exception): + """[summary] + + Arguments: + Exception {[type]} -- [description] + """ + + logging.debug(f'Initializing class InvalidSettingError()') + pass + logging.debug(f'Initialized class InvalidSettingError()') + + +class NOT_SET(object): + """[summary] + + Arguments: + object {[type]} -- [description] + """ + + logging.debug(f'Initializing class NOT_SET()') + pass + logging.debug(f'Initialized class NOT_SET()') + + +class Settings(object): + """[summary] + + Arguments: + object {[type]} -- [description] + """ + + def __init__(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + """ + + self._parent = parent + + def __getattr__(self, name): + """[summary] + + Arguments: + name {[type]} -- [description] + + Raises: + InvalidSettingError: [description] + + Returns: + [type] -- [description] + """ + + if name.startswith('_'): + return super(Settings, self).__getattr__(name) + + value = self.get(name) + + if value != NOT_SET: + return value + + if self._parent: + return getattr(self._parent, name) + + raise InvalidSettingError('Invalid setting: %s' % name) + + def __setattr__(self, name, value): + """[summary] + + Arguments: + name {[type]} -- [description] + value {[type]} -- [description] + + Raises: + InvalidSettingError: [description] + """ + + if name.startswith('_'): + super(Settings, self).__setattr__(name, value) + return + + if self.set(name, value): + return + + if self._parent: + setattr(self._parent, name, value) + return + + raise InvalidSettingError('Invalid setting: %s' % name) + + def get(self, name): + """[summary] + + Arguments: + name {[type]} -- [description] + + Raises: + NotImplementedError: [description] + """ + + raise NotImplementedError( + 'Settings subclasses must implement the get() method.') + + def set(self, name, value): + """[summary] + + Arguments: + name {[type]} -- [description] + value {[type]} -- [description] + + Raises: + NotImplementedError: [description] + """ + + raise NotImplementedError( + 'Settings subclasses must implement the set() method.') + + +class ModuleSettings(Settings): + """[summary] + + Arguments: + Settings {[type]} -- [description] + """ + + def __init__(self, parent, module): + """[summary] + + Arguments: + parent {[type]} -- [description] + module {[type]} -- [description] + """ + + super(ModuleSettings, self).__init__(parent) + self._module = module + + def get(self, name): + """[summary] + + Arguments: + name {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + module = self._module + + if hasattr(module, name): + return getattr(module, name) + + return NOT_SET + + def set(self, name, value): + """[summary] + + Arguments: + name {[type]} -- [description] + value {[type]} -- [description] + + Returns: + [type] -- [description] + """ + return False + + +class FileSettings(Settings): + """[summary] + + Arguments: + Settings {[type]} -- [description] + """ + + def __init__(self, parent, file): + """[summary] + + Arguments: + parent {[type]} -- [description] + file {[type]} -- [description] + """ + + super(FileSettings, self).__init__(parent) + + self._file = file + self.load() + + def load(self): + """[summary] + """ + + try: + self._settings = safe_pickle.load(self._file) + except Exception: + self._settings = {} + + def save(self): + """[summary] + """ + + safe_pickle.save(self._file, self._settings) + + def get(self, name): + """[summary] + + Arguments: + name {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + if name in self._settings: + return self._settings[name] + + return NOT_SET + + def set(self, name, value): + """[summary] + + Arguments: + name {[type]} -- [description] + value {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + if value != getattr(self, name): + self._settings[name] = value + self.save() + + return True + + +def create_chain(): + """[summary] + + Returns: + [type] -- [description] + """ + + # import defaults # FIXME: delete this + + settings = ModuleSettings(None, defaults) + settings = FileSettings(settings, 'settings.dat') + + return settings + + +settings = create_chain() + +# EOF diff --git a/setup.py b/setup.py index 53866ed..e0d309b 100644 --- a/setup.py +++ b/setup.py @@ -1,117 +1,191 @@ -import os -import py2exe -import sys -from distutils.core import setup - -manifest = ''' - - - Feed Notifier 2.6 - - - - - - - - - - - - - - - - - - -''' - -# Don't require the command line argument. -sys.argv.append('py2exe') - -# Include these data files. -def get_data_files(): - def filter_files(files): - def match(file): - extensions = ['.dat'] - for extension in extensions: - if file.endswith(extension): - return True - return False - return tuple(file for file in files if not match(file)) - def tree(src): - return [(root, map(lambda f: os.path.join(root, f), filter_files(files))) for (root, dirs, files) in os.walk(os.path.normpath(src))] - def include(src): - result = tree(src) - result = [('.', item[1]) for item in result] - return result - data_files = [] - data_files += tree('./icons') - data_files += tree('./sounds') - data_files += tree('./Microsoft.VC90.CRT') - return data_files - -# Build the distribution. -setup( - options = {"py2exe":{ - "compressed": 1, - "optimize": 1, - "bundle_files": 1, - "includes": ['parsetab'], - "dll_excludes": [ - 'msvcp90.dll', - 'mswsock.dll', - 'API-MS-Win-Core-LocalRegistry-L1-1-0.dll', - 'API-MS-Win-Core-ProcessThreads-L1-1-0.dll', - 'API-MS-Win-Security-Base-L1-1-0.dll', - 'POWRPROF.dll', - 'Secur32.dll', - 'SHFOLDER.dll', - ], - }}, - windows = [{ - "script": "main.py", - "dest_base": "notifier", - "icon_resources": [(1, "icons/feed.ico")], - "other_resources": [(24, 1, manifest)], - }], - data_files = get_data_files(), -) - -# Build Information -def get_revision(): - import time - return int(time.time()) - -def save_build_info(): - revision = get_revision() - path = 'dist/revision.txt' - with open(path, 'w') as file: - file.write(str(revision)) - print - print 'Saved build revision %d to %s' % (revision, path) - -save_build_info() +# -*- coding: utf-8 -*- + +"""[summary] + +Returns: + [type] -- [description] +""" + + +import os +import sys +import time +from distutils.core import setup + +if sys.platform.startswith('win32'): + import py2exe + + +manifest = ''' + + + Feed Notifier 2.6 + + + + + + + + + + + + + + + + + + +''' + +# Don't require the command line argument. +sys.argv.append('py2exe') + +# Include these data files. + + +def get_data_files(): + """[summary] + """ + + def filter_files(files): + """[summary] + """ + + def match(file): + """[summary] + + Arguments: + file {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + extensions = ['.dat'] + + for extension in extensions: + if file.endswith(extension): + return True + return False + + return tuple(file for file in files if not match(file)) + + def tree(src): + """[summary] + + Arguments: + src {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + return [(root, [os.path.join(root, f) for f in filter_files(files)]) + for (root, dirs, files) in os.walk(os.path.normpath(src))] + + def include(src): + """[summary] + + Arguments: + src {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + result = tree(src) + result = [('.', item[1]) for item in result] + + return result + + data_files = [] + data_files += tree('./icons') + data_files += tree('./sounds') + data_files += tree('./Microsoft.VC90.CRT') + + return data_files + + +# Build the distribution. +setup( + options={"py2exe": { + "compressed": 1, + "optimize": 1, + "bundle_files": 1, + "includes": ['parsetab'], + "dll_excludes": [ + 'msvcp90.dll', + 'mswsock.dll', + 'API-MS-Win-Core-LocalRegistry-L1-1-0.dll', + 'API-MS-Win-Core-ProcessThreads-L1-1-0.dll', + 'API-MS-Win-Security-Base-L1-1-0.dll', + 'POWRPROF.dll', + 'Secur32.dll', + 'SHFOLDER.dll', + ], + }}, + windows=[{ + "script": "main.py", + "dest_base": "notifier", + "icon_resources": [(1, "icons/feed.ico")], + "other_resources": [(24, 1, manifest)], + }], + data_files=get_data_files(), +) + +# Build Information + + +def get_revision(): + """[summary] + + Returns: + [type] -- [description] + """ + + # import time # FIXME: delete this + return int(time.time()) + + +def save_build_info(): + """[summary] + """ + + revision = get_revision() + path = 'dist/revision.txt' + + with open(path, 'w') as file: + file.write(str(revision)) + + print() + print(('Saved build revision %d to %s' % (revision, path))) + + +save_build_info() + +# EOF diff --git a/theme_default.py b/theme_default.py index 6a50db7..592bc19 100644 --- a/theme_default.py +++ b/theme_default.py @@ -1,237 +1,425 @@ -import wx -import controls -import popups -import util -from settings import settings - -BACKGROUND = (230, 230, 230) - -class Frame(wx.Frame): - def __init__(self, item, context): - title = settings.APP_NAME - style = wx.FRAME_NO_TASKBAR | wx.BORDER_NONE - if settings.POPUP_STAY_ON_TOP: - style |= wx.STAY_ON_TOP - super(Frame, self).__init__(None, -1, title, style=style) - self.item = item - self.context = context - self.hover_count = 0 - container = self.create_container(self) - container.Bind(wx.EVT_MOUSEWHEEL, self.on_mousewheel) - container.Bind(wx.EVT_KEY_DOWN, self.on_key_down) - self.container = container - self.Fit() - def post_link(self, link): - event = popups.Event(self, popups.EVT_LINK) - event.link = link - wx.PostEvent(self, event) - def on_link(self, event): - self.post_link(event.link) - def on_left_down(self, event): - self.post_link(popups.COMMAND_NEXT) - def on_mousewheel(self, event): - if event.GetWheelRotation() < 0: - self.post_link(popups.COMMAND_NEXT) - else: - self.post_link(popups.COMMAND_PREVIOUS) - def on_focus(self, event): - if event.GetEventObject() != self.container: - self.container.SetFocusIgnoringChildren() - def on_key_down(self, event): - code = event.GetKeyCode() - if code == wx.WXK_ESCAPE: - self.post_link(popups.COMMAND_CLOSE) - elif code == wx.WXK_LEFT: - self.post_link(popups.COMMAND_PREVIOUS) - elif code == wx.WXK_RIGHT: - self.post_link(popups.COMMAND_NEXT) - elif code == wx.WXK_HOME: - self.post_link(popups.COMMAND_FIRST) - elif code == wx.WXK_END: - self.post_link(popups.COMMAND_LAST) - def on_enter(self, event): - event.Skip() - self.hover_count += 1 - if self.hover_count == 1: - wx.PostEvent(self, popups.Event(self, popups.EVT_POPUP_ENTER)) - def on_leave(self, event): - event.Skip() - self.hover_count -= 1 - if self.hover_count == 0: - wx.PostEvent(self, popups.Event(self, popups.EVT_POPUP_LEAVE)) - def bind_links(self, widgets): - for widget in widgets: - widget.Bind(controls.EVT_HYPERLINK, self.on_link) - widget.Bind(wx.EVT_SET_FOCUS, self.on_focus) - widget.Bind(wx.EVT_ENTER_WINDOW, self.on_enter) - widget.Bind(wx.EVT_LEAVE_WINDOW, self.on_leave) - def bind_widgets(self, widgets): - for widget in widgets: - widget.Bind(wx.EVT_LEFT_DOWN, self.on_left_down) - widget.Bind(wx.EVT_SET_FOCUS, self.on_focus) - widget.Bind(wx.EVT_ENTER_WINDOW, self.on_enter) - widget.Bind(wx.EVT_LEAVE_WINDOW, self.on_leave) - def create_container(self, parent): - color = self.item.feed.color or settings.POPUP_BORDER_COLOR - - panel1 = wx.Panel(parent, -1, style=wx.WANTS_CHARS) - panel1.SetBackgroundColour(wx.Colour(*color)) - panel1.SetForegroundColour(wx.Colour(*color)) - panel2 = wx.Panel(panel1, -1) - panel2.SetBackgroundColour(wx.BLACK) - panel2.SetForegroundColour(wx.BLACK) - panel3 = wx.Panel(panel2, -1) - panel3.SetBackgroundColour(wx.WHITE) - panel3.SetForegroundColour(wx.BLACK) - contents = self.create_contents(panel3) - - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(panel2, 1, wx.EXPAND|wx.ALL, settings.POPUP_BORDER_SIZE) - panel1.SetSizer(sizer) - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(panel3, 1, wx.EXPAND|wx.ALL, 1) - panel2.SetSizer(sizer) - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(contents, 1, wx.EXPAND|wx.ALL) - panel3.SetSizer(sizer) - - panel1.Fit() - self.bind_widgets([panel1, panel2, panel3]) - return panel1 - def create_contents(self, parent): - header = self.create_header(parent) - body = self.create_body(parent) - footer = self.create_footer(parent) - pen = wx.Pen(wx.BLACK, style=wx.USER_DASH) - pen.SetDashes([0, 2]) - line1 = controls.Line(parent, pen) - line2 = controls.Line(parent, pen) - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(header, 0, wx.EXPAND) - sizer.Add(line1, 0, wx.EXPAND) - sizer.Add(body, 1, wx.EXPAND) - sizer.Add(line2, 0, wx.EXPAND) - sizer.Add(footer, 0, wx.EXPAND) - self.bind_widgets([line1, line2]) - return sizer - def create_header(self, parent): - panel = wx.Panel(parent, -1) - panel.SetBackgroundColour(wx.Colour(*BACKGROUND)) - panel.SetForegroundColour(wx.BLACK) - feed = self.item.feed - paths = ['icons/feed.png'] - if feed.has_favicon: - paths.insert(0, feed.favicon_path) - for path in paths: - try: - bitmap = util.scale_bitmap(wx.Bitmap(path), 16, 16, wx.Colour(*BACKGROUND)) - break - except Exception: - pass - else: - bitmap = wx.EmptyBitmap(16, 16) - icon = controls.BitmapLink(panel, feed.link, bitmap) - icon.SetBackgroundColour(wx.Colour(*BACKGROUND)) - width, height = icon.GetSize() - feed = self.create_feed(panel, width) - button = controls.BitmapLink(panel, popups.COMMAND_CLOSE, wx.Bitmap('icons/cross.png'), wx.Bitmap('icons/cross_hover.png')) - button.SetBackgroundColour(wx.Colour(*BACKGROUND)) - sizer = wx.BoxSizer(wx.HORIZONTAL) - sizer.Add(icon, 0, wx.ALIGN_CENTER|wx.ALL, 10) - sizer.Add(feed, 1, wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM, 5) - sizer.Add(button, 0, wx.ALIGN_CENTER|wx.ALL, 10) - panel.SetSizer(sizer) - self.bind_links([icon, button]) - self.bind_widgets([panel]) - return panel - def create_feed(self, parent, icon_width): - width = settings.POPUP_WIDTH - 64 - icon_width - if self.item.feed.link: - link = controls.Link(parent, width, self.item.feed.link, self.item.feed.title) - else: - link = controls.Text(parent, width, self.item.feed.title) - link.SetBackgroundColour(wx.Colour(*BACKGROUND)) - font = link.GetFont() - font.SetWeight(wx.BOLD) - link.SetFont(font) - if self.item.author: - info = '%s ago by %s' % (self.item.time_since, self.item.author) - else: - info = '%s ago' % self.item.time_since - info = controls.Text(parent, width, info) - info.SetBackgroundColour(wx.Colour(*BACKGROUND)) - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(link, 0, wx.EXPAND) - sizer.Add(info, 0, wx.EXPAND) - self.bind_links([link]) - self.bind_widgets([info]) - return sizer - def create_body(self, parent): - width = settings.POPUP_WIDTH - 28 - if self.item.link: - link = controls.Link(parent, width, self.item.link, self.item.title) - else: - link = controls.Text(parent, width, self.item.title) - link.SetBackgroundColour(wx.WHITE) - font = link.GetFont() - font.SetWeight(wx.BOLD) - font.SetPointSize(12) - link.SetFont(font) - text = controls.Text(parent, width, self.item.description) - text.SetBackgroundColour(wx.WHITE) - font = text.GetFont() - font.SetPointSize(10) - text.SetFont(font) - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.AddSpacer(5) - sizer.Add(link, 0, wx.EXPAND|wx.LEFT|wx.RIGHT, 10) - sizer.AddSpacer(5) - sizer.Add(text, 0, wx.EXPAND|wx.LEFT|wx.RIGHT, 10) - sizer.AddSpacer(10) - self.bind_links([link]) - self.bind_widgets([text]) - return sizer - def create_footer(self, parent): - panel = wx.Panel(parent, -1) - panel.SetBackgroundColour(wx.Colour(*BACKGROUND)) - panel.SetForegroundColour(wx.BLACK) - first = controls.BitmapLink(panel, popups.COMMAND_FIRST, wx.Bitmap('icons/control_start.png'), wx.Bitmap('icons/control_start_blue.png')) - previous = controls.BitmapLink(panel, popups.COMMAND_PREVIOUS, wx.Bitmap('icons/control_rewind.png'), wx.Bitmap('icons/control_rewind_blue.png')) - text = '%s of %s' % (self.context['item_index'], self.context['item_count']) - text = controls.Text(panel, 0, text) - text.SetBackgroundColour(wx.Colour(*BACKGROUND)) - text.fit_no_wrap() - next = controls.BitmapLink(panel, popups.COMMAND_NEXT, wx.Bitmap('icons/control_fastforward.png'), wx.Bitmap('icons/control_fastforward_blue.png')) - last = controls.BitmapLink(panel, popups.COMMAND_LAST, wx.Bitmap('icons/control_end.png'), wx.Bitmap('icons/control_end_blue.png')) - play = controls.BitmapLink(panel, popups.COMMAND_PLAY, wx.Bitmap('icons/control_play.png'), wx.Bitmap('icons/control_play_blue.png')) - pause = controls.BitmapLink(panel, popups.COMMAND_PAUSE, wx.Bitmap('icons/control_pause.png'), wx.Bitmap('icons/control_pause_blue.png')) - widgets = [first, previous, next, last, play, pause] - self.bind_links(widgets) - for widget in widgets: - widget.SetBackgroundColour(wx.Colour(*BACKGROUND)) - sizer = wx.BoxSizer(wx.HORIZONTAL) - sizer.AddSpacer(10) - sizer.Add(first, 0, wx.TOP|wx.BOTTOM, 5) - sizer.AddSpacer(8) - sizer.Add(previous, 0, wx.TOP|wx.BOTTOM, 5) - sizer.AddSpacer(8) - sizer.Add(text, 0, wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM, 5) - sizer.AddSpacer(8) - sizer.Add(next, 0, wx.TOP|wx.BOTTOM, 5) - sizer.AddSpacer(8) - sizer.Add(last, 0, wx.TOP|wx.BOTTOM, 5) - sizer.AddStretchSpacer(1) - sizer.Add(play, 0, wx.TOP|wx.BOTTOM, 5) - sizer.AddSpacer(8) - sizer.Add(pause, 0, wx.TOP|wx.BOTTOM, 5) - sizer.AddSpacer(10) - panel.SetSizer(sizer) - self.bind_widgets([panel, text]) - return panel - -if __name__ == '__main__': - app = wx.PySimpleApp() - frame = Frame() - frame.Show() - app.MainLoop() - \ No newline at end of file +# -*- coding: utf-8 -*- + +"""[summary] + +Returns: + [type] -- [description] +""" + +import wx + +import controls +import popups +import util +from settings import settings + +BACKGROUND = (230, 230, 230) + + +class Frame(wx.Frame): + """[summary] + + Arguments: + wx {[type]} -- [description] + """ + + def __init__(self, item, context): + """[summary] + + Arguments: + item {[type]} -- [description] + context {[type]} -- [description] + """ + + title = settings.APP_NAME + style = wx.FRAME_NO_TASKBAR | wx.BORDER_NONE + + if settings.POPUP_STAY_ON_TOP: + style |= wx.STAY_ON_TOP + + super(Frame, self).__init__(None, -1, title, style=style) + + self.item = item + self.context = context + self.hover_count = 0 + + container = self.create_container(self) + container.Bind(wx.EVT_MOUSEWHEEL, self.on_mousewheel) + container.Bind(wx.EVT_KEY_DOWN, self.on_key_down) + + self.container = container + self.Fit() + + def post_link(self, link): + """[summary] + + Arguments: + link {[type]} -- [description] + """ + + event = popups.Event(self, popups.EVT_LINK) + event.link = link + wx.PostEvent(self, event) + + def on_link(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.post_link(event.link) + + def on_left_down(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.post_link(popups.COMMAND_NEXT) + + def on_mousewheel(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + if event.GetWheelRotation() < 0: + self.post_link(popups.COMMAND_NEXT) + else: + self.post_link(popups.COMMAND_PREVIOUS) + + def on_focus(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + if event.GetEventObject() != self.container: + self.container.SetFocusIgnoringChildren() + + def on_key_down(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + code = event.GetKeyCode() + + if code == wx.WXK_ESCAPE: + self.post_link(popups.COMMAND_CLOSE) + elif code == wx.WXK_LEFT: + self.post_link(popups.COMMAND_PREVIOUS) + elif code == wx.WXK_RIGHT: + self.post_link(popups.COMMAND_NEXT) + elif code == wx.WXK_HOME: + self.post_link(popups.COMMAND_FIRST) + elif code == wx.WXK_END: + self.post_link(popups.COMMAND_LAST) + + def on_enter(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + event.Skip() + self.hover_count += 1 + if self.hover_count == 1: + wx.PostEvent(self, popups.Event(self, popups.EVT_POPUP_ENTER)) + + def on_leave(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + event.Skip() + self.hover_count -= 1 + if self.hover_count == 0: + wx.PostEvent(self, popups.Event(self, popups.EVT_POPUP_LEAVE)) + + def bind_links(self, widgets): + """[summary] + + Arguments: + widgets {[type]} -- [description] + """ + + for widget in widgets: + widget.Bind(controls.EVT_HYPERLINK, self.on_link) + widget.Bind(wx.EVT_SET_FOCUS, self.on_focus) + widget.Bind(wx.EVT_ENTER_WINDOW, self.on_enter) + widget.Bind(wx.EVT_LEAVE_WINDOW, self.on_leave) + + def bind_widgets(self, widgets): + """[summary] + + Arguments: + widgets {[type]} -- [description] + """ + + for widget in widgets: + widget.Bind(wx.EVT_LEFT_DOWN, self.on_left_down) + widget.Bind(wx.EVT_SET_FOCUS, self.on_focus) + widget.Bind(wx.EVT_ENTER_WINDOW, self.on_enter) + widget.Bind(wx.EVT_LEAVE_WINDOW, self.on_leave) + + def create_container(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + color = self.item.feed.color or settings.POPUP_BORDER_COLOR + + panel1 = wx.Panel(parent, -1, style=wx.WANTS_CHARS) + panel1.SetBackgroundColour(wx.Colour(*color)) + panel1.SetForegroundColour(wx.Colour(*color)) + panel2 = wx.Panel(panel1, -1) + panel2.SetBackgroundColour(wx.BLACK) + panel2.SetForegroundColour(wx.BLACK) + panel3 = wx.Panel(panel2, -1) + panel3.SetBackgroundColour(wx.WHITE) + panel3.SetForegroundColour(wx.BLACK) + contents = self.create_contents(panel3) + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(panel2, 1, wx.EXPAND | wx.ALL, settings.POPUP_BORDER_SIZE) + panel1.SetSizer(sizer) + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(panel3, 1, wx.EXPAND | wx.ALL, 1) + panel2.SetSizer(sizer) + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(contents, 1, wx.EXPAND | wx.ALL) + panel3.SetSizer(sizer) + + panel1.Fit() + self.bind_widgets([panel1, panel2, panel3]) + + return panel1 + + def create_contents(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + header = self.create_header(parent) + body = self.create_body(parent) + footer = self.create_footer(parent) + pen = wx.Pen(wx.BLACK, style=wx.USER_DASH) + pen.SetDashes([0, 2]) + line1 = controls.Line(parent, pen) + line2 = controls.Line(parent, pen) + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(header, 0, wx.EXPAND) + sizer.Add(line1, 0, wx.EXPAND) + sizer.Add(body, 1, wx.EXPAND) + sizer.Add(line2, 0, wx.EXPAND) + sizer.Add(footer, 0, wx.EXPAND) + self.bind_widgets([line1, line2]) + + return sizer + + def create_header(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + panel = wx.Panel(parent, -1) + panel.SetBackgroundColour(wx.Colour(*BACKGROUND)) + panel.SetForegroundColour(wx.BLACK) + feed = self.item.feed + paths = ['icons/feed.png'] + + if feed.has_favicon: + paths.insert(0, feed.favicon_path) + + for path in paths: + try: + bitmap = util.scale_bitmap( + wx.Bitmap(path), 16, 16, wx.Colour(*BACKGROUND)) + break + except Exception: + pass + else: + bitmap = wx.EmptyBitmap(16, 16) + + icon = controls.BitmapLink(panel, feed.link, bitmap) + icon.SetBackgroundColour(wx.Colour(*BACKGROUND)) + width, height = icon.GetSize() + feed = self.create_feed(panel, width) + button = controls.BitmapLink(panel, popups.COMMAND_CLOSE, wx.Bitmap( + 'icons/cross.png'), wx.Bitmap('icons/cross_hover.png')) + button.SetBackgroundColour(wx.Colour(*BACKGROUND)) + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(icon, 0, wx.ALIGN_CENTER | wx.ALL, 10) + sizer.Add(feed, 1, wx.ALIGN_CENTER_VERTICAL | wx.TOP | wx.BOTTOM, 5) + sizer.Add(button, 0, wx.ALIGN_CENTER | wx.ALL, 10) + panel.SetSizer(sizer) + self.bind_links([icon, button]) + self.bind_widgets([panel]) + + return panel + + def create_feed(self, parent, icon_width): + """[summary] + + Arguments: + parent {[type]} -- [description] + icon_width {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + width = settings.POPUP_WIDTH - 64 - icon_width + + if self.item.feed.link: + link = controls.Link( + parent, width, self.item.feed.link, self.item.feed.title) + else: + link = controls.Text(parent, width, self.item.feed.title) + + link.SetBackgroundColour(wx.Colour(*BACKGROUND)) + font = link.GetFont() + font.SetWeight(wx.BOLD) + link.SetFont(font) + + if self.item.author: + info = '%s ago by %s' % (self.item.time_since, self.item.author) + else: + info = '%s ago' % self.item.time_since + + info = controls.Text(parent, width, info) + info.SetBackgroundColour(wx.Colour(*BACKGROUND)) + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(link, 0, wx.EXPAND) + sizer.Add(info, 0, wx.EXPAND) + self.bind_links([link]) + self.bind_widgets([info]) + + return sizer + + def create_body(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + width = settings.POPUP_WIDTH - 28 + + if self.item.link: + link = controls.Link( + parent, width, self.item.link, self.item.title) + else: + link = controls.Text(parent, width, self.item.title) + + link.SetBackgroundColour(wx.WHITE) + font = link.GetFont() + font.SetWeight(wx.BOLD) + font.SetPointSize(12) + link.SetFont(font) + text = controls.Text(parent, width, self.item.description) + text.SetBackgroundColour(wx.WHITE) + font = text.GetFont() + font.SetPointSize(10) + text.SetFont(font) + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.AddSpacer(5) + sizer.Add(link, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 10) + sizer.AddSpacer(5) + sizer.Add(text, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 10) + sizer.AddSpacer(10) + self.bind_links([link]) + self.bind_widgets([text]) + + return sizer + + def create_footer(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + panel = wx.Panel(parent, -1) + panel.SetBackgroundColour(wx.Colour(*BACKGROUND)) + panel.SetForegroundColour(wx.BLACK) + first = controls.BitmapLink(panel, popups.COMMAND_FIRST, wx.Bitmap( + 'icons/control_start.png'), wx.Bitmap('icons/control_start_blue.png')) + previous = controls.BitmapLink(panel, popups.COMMAND_PREVIOUS, wx.Bitmap( + 'icons/control_rewind.png'), wx.Bitmap('icons/control_rewind_blue.png')) + text = '%s of %s' % ( + self.context['item_index'], self.context['item_count']) + text = controls.Text(panel, 0, text) + text.SetBackgroundColour(wx.Colour(*BACKGROUND)) + text.fit_no_wrap() + next = controls.BitmapLink(panel, popups.COMMAND_NEXT, wx.Bitmap( + 'icons/control_fastforward.png'), wx.Bitmap('icons/control_fastforward_blue.png')) + last = controls.BitmapLink(panel, popups.COMMAND_LAST, wx.Bitmap( + 'icons/control_end.png'), wx.Bitmap('icons/control_end_blue.png')) + play = controls.BitmapLink(panel, popups.COMMAND_PLAY, wx.Bitmap( + 'icons/control_play.png'), wx.Bitmap('icons/control_play_blue.png')) + pause = controls.BitmapLink(panel, popups.COMMAND_PAUSE, wx.Bitmap( + 'icons/control_pause.png'), wx.Bitmap('icons/control_pause_blue.png')) + widgets = [first, previous, next, last, play, pause] + self.bind_links(widgets) + for widget in widgets: + widget.SetBackgroundColour(wx.Colour(*BACKGROUND)) + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.AddSpacer(10) + sizer.Add(first, 0, wx.TOP | wx.BOTTOM, 5) + sizer.AddSpacer(8) + sizer.Add(previous, 0, wx.TOP | wx.BOTTOM, 5) + sizer.AddSpacer(8) + sizer.Add(text, 0, wx.ALIGN_CENTER_VERTICAL | wx.TOP | wx.BOTTOM, 5) + sizer.AddSpacer(8) + sizer.Add(next, 0, wx.TOP | wx.BOTTOM, 5) + sizer.AddSpacer(8) + sizer.Add(last, 0, wx.TOP | wx.BOTTOM, 5) + sizer.AddStretchSpacer(1) + sizer.Add(play, 0, wx.TOP | wx.BOTTOM, 5) + sizer.AddSpacer(8) + sizer.Add(pause, 0, wx.TOP | wx.BOTTOM, 5) + sizer.AddSpacer(10) + panel.SetSizer(sizer) + self.bind_widgets([panel, text]) + + return panel + + +if __name__ == '__main__': + app = wx.App() + frame = wx.Frame(None, -1, "Test") + frame.Show() + app.MainLoop() + +# EOF diff --git a/updater.py b/updater.py index d4cf068..70fe015 100644 --- a/updater.py +++ b/updater.py @@ -1,125 +1,287 @@ -import wx -import os -import time -import urllib -import tempfile -import util -from settings import settings - -class CancelException(Exception): - pass - -class DownloadDialog(wx.Dialog): - def __init__(self, parent): - super(DownloadDialog, self).__init__(parent, -1, 'Feed Notifier Update') - util.set_icon(self) - self.path = None - text = wx.StaticText(self, -1, 'Downloading update, please wait...') - self.gauge = wx.Gauge(self, -1, 100, size=(250, 16)) - cancel = wx.Button(self, wx.ID_CANCEL, 'Cancel') - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(text) - sizer.AddSpacer(8) - sizer.Add(self.gauge, 0, wx.EXPAND) - sizer.AddSpacer(8) - sizer.Add(cancel, 0, wx.ALIGN_RIGHT) - wrapper = wx.BoxSizer(wx.VERTICAL) - wrapper.Add(sizer, 1, wx.EXPAND|wx.ALL, 10) - self.SetSizerAndFit(wrapper) - self.start_download() - def start_download(self): - util.start_thread(self.download) - def download(self): - try: - self.path = download_installer(self.listener) - wx.CallAfter(self.EndModal, wx.ID_OK) - except CancelException: - pass - except Exception: - wx.CallAfter(self.on_fail) - def on_fail(self): - dialog = wx.MessageDialog(self, 'Failed to download updates. Nothing will be installed at this time.', 'Update Failed', wx.OK|wx.ICON_ERROR) - dialog.ShowModal() - dialog.Destroy() - self.EndModal(wx.ID_CANCEL) - def update(self, percent): - if self: - self.gauge.SetValue(percent) - def listener(self, blocks, block_size, total_size): - size = blocks * block_size - percent = size * 100 / total_size - if self: - wx.CallAfter(self.update, percent) - else: - raise CancelException - -def get_remote_revision(): - file = None - try: - file = urllib.urlopen(settings.REVISION_URL) - return int(file.read().strip()) - except Exception: - return -1 - finally: - if file: - file.close() - -def download_installer(listener): - fd, path = tempfile.mkstemp('.exe') - os.close(fd) - path, headers = urllib.urlretrieve(settings.INSTALLER_URL, path, listener) - return path - -def should_check(): - last_check = settings.UPDATE_TIMESTAMP - now = int(time.time()) - elapsed = now - last_check - return elapsed >= settings.UPDATE_INTERVAL - -def should_update(force): - if not force: - if not should_check(): - return False - now = int(time.time()) - settings.UPDATE_TIMESTAMP = now - local = settings.LOCAL_REVISION - remote = get_remote_revision() - if local < 0 or remote < 0: - return False - return remote > local - -def do_check(controller, force=False): - if should_update(force): - wx.CallAfter(do_ask, controller) - elif force: - wx.CallAfter(do_tell, controller) - -def do_ask(controller): - dialog = wx.MessageDialog(None, 'Feed Notifier software updates are available. Download and install now?', 'Update Feed Notifier?', wx.YES_NO|wx.YES_DEFAULT|wx.ICON_QUESTION) - if dialog.ShowModal() == wx.ID_YES: - do_download(controller) - dialog.Destroy() - -def do_tell(controller): - dialog = wx.MessageDialog(None, 'No software updates are available at this time.', 'No Updates', wx.OK|wx.ICON_INFORMATION) - dialog.ShowModal() - dialog.Destroy() - -def do_download(controller): - dialog = DownloadDialog(None) - dialog.Center() - result = dialog.ShowModal() - path = dialog.path - dialog.Destroy() - if result == wx.ID_OK: - do_install(controller, path) - -def do_install(controller, path): - controller.close() - time.sleep(1) - os.execvp(path, (path, '/sp-', '/silent', '/norestart')) - -def run(controller, force=False): - if force or settings.CHECK_FOR_UPDATES: - util.start_thread(do_check, controller, force) - \ No newline at end of file +# -*- coding: utf-8 -*- + +"""[summary] + +Returns: + [type] -- [description] +""" + +import os +import tempfile +import time +import urllib.error +import urllib.parse +import urllib.request + +import wx + +import util + +from settings import settings + + +class CancelException(Exception): + """[summary] + + Arguments: + Exception {[type]} -- [description] + """ + + pass + + +class DownloadDialog(wx.Dialog): + """[summary] + + Arguments: + wx {[type]} -- [description] + """ + + def __init__(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + """ + + super(DownloadDialog, self).__init__( + parent, -1, 'Feed Notifier Update') + util.set_icon(self) + self.path = None + text = wx.StaticText(self, -1, 'Downloading update, please wait...') + self.gauge = wx.Gauge(self, -1, 100, size=(250, 16)) + cancel = wx.Button(self, wx.ID_CANCEL, 'Cancel') + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(text) + sizer.AddSpacer(8) + sizer.Add(self.gauge, 0, wx.EXPAND) + sizer.AddSpacer(8) + sizer.Add(cancel, 0, wx.ALIGN_RIGHT) + wrapper = wx.BoxSizer(wx.VERTICAL) + wrapper.Add(sizer, 1, wx.EXPAND | wx.ALL, 10) + self.SetSizerAndFit(wrapper) + self.start_download() + + def start_download(self): + """[summary] + """ + + util.start_thread(self.download) + + def download(self): + """[summary] + """ + + try: + self.path = download_installer(self.listener) + wx.CallAfter(self.EndModal, wx.ID_OK) + except CancelException: + pass + except Exception: + wx.CallAfter(self.on_fail) + + def on_fail(self): + """[summary] + """ + + dialog = wx.MessageDialog( + self, 'Failed to download updates. Nothing will be installed at this time.', 'Update Failed', wx.OK | wx.ICON_ERROR) + dialog.ShowModal() + dialog.Destroy() + self.EndModal(wx.ID_CANCEL) + + def update(self, percent): + """[summary] + + Arguments: + percent {[type]} -- [description] + """ + + if self: + self.gauge.SetValue(percent) + + def listener(self, blocks, block_size, total_size): + """[summary] + + Arguments: + blocks {[type]} -- [description] + block_size {[type]} -- [description] + total_size {[type]} -- [description] + + Raises: + CancelException: [description] + """ + + size = blocks * block_size + percent = size * 100 / total_size + + if self: + wx.CallAfter(self.update, percent) + else: + raise CancelException + + +def get_remote_revision(): + """[summary] + + Returns: + [type] -- [description] + """ + + file = None + + try: + file = urllib.request.urlopen(settings.REVISION_URL) + return int(file.read().strip()) + except Exception: + return -1 + finally: + if file: + file.close() + + +def download_installer(listener): + """[summary] + + Arguments: + listener {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + fd, path = tempfile.mkstemp('.exe') + os.close(fd) + path, headers = urllib.request.urlretrieve( + settings.INSTALLER_URL, path, listener) + + return path + + +def should_check(): + """[summary] + + Returns: + [type] -- [description] + """ + + last_check = settings.UPDATE_TIMESTAMP + now = int(time.time()) + elapsed = now - last_check + + return elapsed >= settings.UPDATE_INTERVAL + + +def should_update(force): + """[summary] + + Arguments: + force {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + if not force: + if not should_check(): + return False + + now = int(time.time()) + settings.UPDATE_TIMESTAMP = now + local = settings.LOCAL_REVISION + remote = get_remote_revision() + + if local < 0 or remote < 0: + return False + + return remote > local + + +def do_check(controller, force=False): + """[summary] + + Arguments: + controller {[type]} -- [description] + + Keyword Arguments: + force {bool} -- [description] (default: {False}) + """ + + if should_update(force): + wx.CallAfter(do_ask, controller) + elif force: + wx.CallAfter(do_tell, controller) + + +def do_ask(controller): + """[summary] + + Arguments: + controller {[type]} -- [description] + """ + + dialog = wx.MessageDialog(None, 'Feed Notifier software updates are available. Download and install now?', + 'Update Feed Notifier?', wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION) + + if dialog.ShowModal() == wx.ID_YES: + do_download(controller) + + dialog.Destroy() + + +def do_tell(controller): + """[summary] + + Arguments: + controller {[type]} -- [description] + """ + + dialog = wx.MessageDialog( + None, 'No software updates are available at this time.', 'No Updates', wx.OK | wx.ICON_INFORMATION) + dialog.ShowModal() + dialog.Destroy() + + +def do_download(controller): + """[summary] + + Arguments: + controller {[type]} -- [description] + """ + + dialog = DownloadDialog(None) + dialog.Center() + result = dialog.ShowModal() + path = dialog.path + dialog.Destroy() + if result == wx.ID_OK: + do_install(controller, path) + + +def do_install(controller, path): + """[summary] + + Arguments: + controller {[type]} -- [description] + path {[type]} -- [description] + """ + + controller.close() + time.sleep(1) + os.execvp(path, (path, '/sp-', '/silent', '/norestart')) + + +def run(controller, force=False): + """[summary] + + Arguments: + controller {[type]} -- [description] + + Keyword Arguments: + force {bool} -- [description] (default: {False}) + """ + + if force or settings.CHECK_FOR_UPDATES: + util.start_thread(do_check, controller, force) + +# EOF \ No newline at end of file diff --git a/util.py b/util.py index 61ff2a5..2822102 100644 --- a/util.py +++ b/util.py @@ -1,263 +1,549 @@ -import wx -import os -import re -import time -import base64 -import calendar -import urllib2 -import urlparse -import threading -import feedparser -from htmlentitydefs import name2codepoint -from settings import settings - -def set_icon(window): - bundle = wx.IconBundle() - bundle.AddIcon(wx.Icon('icons/16.png', wx.BITMAP_TYPE_PNG)) - bundle.AddIcon(wx.Icon('icons/24.png', wx.BITMAP_TYPE_PNG)) - bundle.AddIcon(wx.Icon('icons/32.png', wx.BITMAP_TYPE_PNG)) - bundle.AddIcon(wx.Icon('icons/48.png', wx.BITMAP_TYPE_PNG)) - bundle.AddIcon(wx.Icon('icons/256.png', wx.BITMAP_TYPE_PNG)) - window.SetIcons(bundle) - -def start_thread(func, *args): - thread = threading.Thread(target=func, args=args) - thread.setDaemon(True) - thread.start() - return thread - -def scale_bitmap(bitmap, width, height, color): - bw, bh = bitmap.GetWidth(), bitmap.GetHeight() - if bw == width and bh == height: - return bitmap - if width < 0: - width = bw - if height < 0: - height = bh - buffer = wx.EmptyBitmap(bw, bh) - dc = wx.MemoryDC(buffer) - dc.SetBackground(wx.Brush(color)) - dc.Clear() - dc.DrawBitmap(bitmap, 0, 0, True) - image = wx.ImageFromBitmap(buffer) - image = image.Scale(width, height, wx.IMAGE_QUALITY_HIGH) - result = wx.BitmapFromImage(image) - return result - -def menu_item(menu, label, func, icon=None, kind=wx.ITEM_NORMAL): - item = wx.MenuItem(menu, -1, label, kind=kind) - if func: - menu.Bind(wx.EVT_MENU, func, id=item.GetId()) - if icon: - item.SetBitmap(wx.Bitmap(icon)) - menu.AppendItem(item) - return item - -def select_choice(choice, data): - for index in range(choice.GetCount()): - if choice.GetClientData(index) == data: - choice.Select(index) - return - choice.Select(wx.NOT_FOUND) - -def get_top_window(window): - result = None - while window: - result = window - window = window.GetParent() - return result - -def get(obj, key, default): - value = obj.get(key, None) - return value or default - -def abspath(path): - path = os.path.abspath(path) - path = 'file:///%s' % path.replace('\\', '/') - return path - -def parse(url, username=None, password=None, etag=None, modified=None): - agent = settings.USER_AGENT - handlers = [get_proxy()] - if username and password: - url = insert_credentials(url, username, password) - return feedparser.parse(url, etag=etag, modified=modified, agent=agent, handlers=handlers) - -def is_valid_feed(data): - entries = get(data, 'entries', []) - title = get(data.feed, 'title', '') - link = get(data.feed, 'link', '') - return entries or title or link - -def insert_credentials(url, username, password): - parts = urlparse.urlsplit(url) - netloc = parts.netloc - if '@' in netloc: - netloc = netloc[netloc.index('@')+1:] - netloc = '%s:%s@%s' % (username, password, netloc) - parts = list(parts) - parts[1] = netloc - return urlparse.urlunsplit(tuple(parts)) - -def encode_password(password): - return base64.b64encode(password) if password else None - -def decode_password(password): - try: - return base64.b64decode(password) if password else None - except Exception: - return None - -def get_proxy(): - if settings.USE_PROXY: - url = decode_password(settings.PROXY_URL) - if url: - # User-configured Proxy - map = { - 'http': url, - 'https': url, - } - proxy = urllib2.ProxyHandler(map) - else: - # Windows-configured Proxy - proxy = urllib2.ProxyHandler() - else: - # No Proxy - proxy = urllib2.ProxyHandler({}) - return proxy - -def find_themes(): - return ['default'] # TODO: more themes! - result = [] - names = os.listdir('themes') - for name in names: - if name.startswith('.'): - continue - path = os.path.join('themes', name) - if os.path.isdir(path): - result.append(name) - return result - -def guess_polling_interval(entries): - if len(entries) < 2: - return settings.DEFAULT_POLLING_INTERVAL - timestamps = [] - for entry in entries: - timestamp = calendar.timegm(get(entry, 'date_parsed', time.gmtime())) - timestamps.append(timestamp) - timestamps.sort() - durations = [b - a for a, b in zip(timestamps, timestamps[1:])] - mean = sum(durations) / len(durations) - choices = [ - 60, - 60*5, - 60*10, - 60*15, - 60*30, - 60*60, - 60*60*2, - 60*60*4, - 60*60*8, - 60*60*12, - 60*60*24, - ] - desired = mean / 2 - if desired == 0: - interval = settings.DEFAULT_POLLING_INTERVAL - elif desired < choices[0]: - interval = choices[0] - else: - interval = max(choice for choice in choices if choice <= desired) - return interval - -def time_since(t): - t = int(t) - now = int(time.time()) - seconds = max(now - t, 0) - if seconds == 1: - return '1 second' - if seconds < 60: - return '%d seconds' % seconds - minutes = seconds / 60 - if minutes == 1: - return '1 minute' - if minutes < 60: - return '%d minutes' % minutes - hours = minutes / 60 - if hours == 1: - return '1 hour' - if hours < 24: - return '%d hours' % hours - days = hours / 24 - if days == 1: - return '1 day' - return '%d days' % days - -def split_time(seconds): - if seconds < 60: - return seconds, 0 - minutes = seconds / 60 - if minutes < 60: - return minutes, 1 - hours = minutes / 60 - days = hours / 24 - if days and hours % 24 == 0: - return days, 3 - return hours, 2 - -def split_time_str(seconds): - interval, units = split_time(seconds) - strings = ['second', 'minute', 'hour', 'day'] - string = strings[units] - if interval != 1: - string += 's' - return '%d %s' % (interval, string) - -def pretty_name(name): - name = ' '.join(s.title() for s in name.split('_')) - last = '0' - result = '' - for c in name: - if c.isdigit() and not last.isdigit(): - result += ' ' - result += c - last = c - return result - -def replace_entities1(text): - entity = re.compile(r'&#(\d+);') - def func(match): - try: - return unichr(int(match.group(1))) - except Exception: - return match.group(0) - return entity.sub(func, text) - -def replace_entities2(text): - entity = re.compile(r'&([a-zA-Z]+);') - def func(match): - try: - return unichr(name2codepoint[match.group(1)]) - except Exception: - return match.group(0) - return entity.sub(func, text) - -def remove_markup(text): - html = re.compile(r'<[^>]+>') - return html.sub(' ', text) - -def format(text, max_length=400): - previous = '' - while text != previous: - previous = text - text = replace_entities1(text) - text = replace_entities2(text) - text = remove_markup(text) - text = ' '.join(text.split()) - if len(text) > max_length: - text = text[:max_length].strip() - text = text.split()[:-1] - text.append('[...]') - text = ' '.join(text) - return text - \ No newline at end of file +# -*- coding: utf-8 -*- + +"""[summary] + +Returns: + [type] -- [description] +""" + +import base64 +import calendar +import logging +import os +import re +import sys +import threading +import time +import urllib.error +import urllib.parse +import urllib.request +from html.entities import name2codepoint + +import wx + +from settings import settings + +try: + import feedparser +except ModuleNotFoundError: + sys.exit("\n\tpip install feeparser\n") + + + +def set_icon(window): + """[summary] + + Arguments: + window {[type]} -- [description] + """ + + logging.debug(f'initializing set_icon function.') + + bundle = wx.IconBundle() + bundle.AddIcon(wx.Icon('icons/16.png', wx.BITMAP_TYPE_PNG)) + bundle.AddIcon(wx.Icon('icons/24.png', wx.BITMAP_TYPE_PNG)) + bundle.AddIcon(wx.Icon('icons/32.png', wx.BITMAP_TYPE_PNG)) + bundle.AddIcon(wx.Icon('icons/48.png', wx.BITMAP_TYPE_PNG)) + bundle.AddIcon(wx.Icon('icons/256.png', wx.BITMAP_TYPE_PNG)) + window.SetIcons(bundle) + + logging.debug(f'initialized set_icon function.') + + +def start_thread(func, *args): + """[summary] + + Arguments: + func {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + logging.debug('util::start_thread - In') + logging.debug(f'func: {func}') + logging.debug(f'args: {args}') + thread = threading.Thread(target=func, args=args) + thread.setDaemon(True) + thread.start() + logging.debug('util::start_thread - Out') + return thread + + +def scale_bitmap(bitmap, width, height, color): + """[summary] + + Arguments: + bitmap {[type]} -- [description] + width {[type]} -- [description] + height {[type]} -- [description] + color {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + bw, bh = bitmap.GetWidth(), bitmap.GetHeight() + if bw == width and bh == height: + return bitmap + if width < 0: + width = bw + if height < 0: + height = bh + # buffer = wx.EmptyBitmap(bw, bh) # FIXME: deprecated + buffer = wx.Bitmap(bw, bh) + dc = wx.MemoryDC(buffer) + dc.SetBackground(wx.Brush(color)) + dc.Clear() + dc.DrawBitmap(bitmap, 0, 0, True) + # image = wx.ImageFromBitmap(buffer) # FIXME: deprecated + image = wx.Bitmap.ConvertToImage(buffer) + image = image.Scale(width, height, wx.IMAGE_QUALITY_HIGH) + # result = wx.BitmapFromImage(image) # FIXME: deprecated + result = wx.Bitmap(image) + return result + + +def menu_item(menu, label, func, icon=None, kind=wx.ITEM_NORMAL): + """[summary] + + Arguments: + menu {[type]} -- [description] + label {[type]} -- [description] + func {[type]} -- [description] + + Keyword Arguments: + icon {[type]} -- [description] (default: {None}) + kind {[type]} -- [description] (default: {wx.ITEM_NORMAL}) + + Returns: + [type] -- [description] + """ + + item = wx.MenuItem(menu, -1, label, kind=kind) + if func: + menu.Bind(wx.EVT_MENU, func, id=item.GetId()) + if icon: + item.SetBitmap(wx.Bitmap(icon)) + # menu.AppendItem(item) # FIXME: deprecated + menu.Append(item) + return item + + +def select_choice(choice, data): + """[summary] + + Arguments: + choice {[type]} -- [description] + data {[type]} -- [description] + """ + + for index in range(choice.GetCount()): + if choice.GetClientData(index) == data: + choice.Select(index) + return + choice.Select(wx.NOT_FOUND) + + +def get_top_window(window): + """[summary] + + Arguments: + window {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + result = None + while window: + result = window + window = window.GetParent() + return result + + +def get(obj, key, default): + """[summary] + + Arguments: + obj {[type]} -- [description] + key {[type]} -- [description] + default {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + value = obj.get(key, None) + return value or default + + +def abspath(path): + """[summary] + + Arguments: + path {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + path = os.path.abspath(path) + path = 'file:///%s' % path.replace('\\', '/') + return path + + +def parse(url, username=None, password=None, etag=None, modified=None): + """[summary] + + Arguments: + url {[type]} -- [description] + + Keyword Arguments: + username {[type]} -- [description] (default: {None}) + password {[type]} -- [description] (default: {None}) + etag {[type]} -- [description] (default: {None}) + modified {[type]} -- [description] (default: {None}) + + Returns: + [type] -- [description] + """ + + agent = settings.USER_AGENT + handlers = [get_proxy()] + if username and password: + url = insert_credentials(url, username, password) + return feedparser.parse(url, etag=etag, modified=modified, agent=agent, handlers=handlers) + + +def is_valid_feed(data): + """[summary] + + Arguments: + data {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + entries = get(data, 'entries', []) + title = get(data.feed, 'title', '') + link = get(data.feed, 'link', '') + return entries or title or link + + +def insert_credentials(url, username, password): + """[summary] + + Arguments: + url {[type]} -- [description] + username {[type]} -- [description] + password {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + parts = urllib.parse.urlsplit(url) + netloc = parts.netloc + if '@' in netloc: + netloc = netloc[netloc.index('@')+1:] + netloc = '%s:%s@%s' % (username, password, netloc) + parts = list(parts) + parts[1] = netloc + return urllib.parse.urlunsplit(tuple(parts)) + + +def encode_password(password): + """[summary] + + Arguments: + password {[type]} -- [description] + + Returns: + [type] -- [description] + """ + return base64.b64encode(password) if password else None + + +def decode_password(password): + """[summary] + + Arguments: + password {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + try: + return base64.b64decode(password) if password else None + except Exception: + return None + + +def get_proxy(): + """[summary] + + Returns: + [type] -- [description] + """ + + if settings.USE_PROXY: + url = decode_password(settings.PROXY_URL) + if url: + # User-configured Proxy + map = { + 'http': url, + 'https': url, + } + proxy = urllib.request.ProxyHandler(map) + else: + # Windows-configured Proxy + proxy = urllib.request.ProxyHandler() + else: + # No Proxy + proxy = urllib.request.ProxyHandler({}) + return proxy + + +def find_themes(): + """[summary] + + Returns: + [type] -- [description] + """ + + # return ['default'] # TODO: more themes! FIXME: unreachable code + result = [] + names = os.listdir('themes') + for name in names: + if name.startswith('.'): + continue + path = os.path.join('themes', name) + if os.path.isdir(path): + result.append(name) + return result + + +def guess_polling_interval(entries): + """[summary] + + Arguments: + entries {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + if len(entries) < 2: + return settings.DEFAULT_POLLING_INTERVAL + timestamps = [] + for entry in entries: + timestamp = calendar.timegm(get(entry, 'date_parsed', time.gmtime())) + timestamps.append(timestamp) + timestamps.sort() + durations = [b - a for a, b in zip(timestamps, timestamps[1:])] + mean = sum(durations) / len(durations) + choices = [ + 60, + 60*5, + 60*10, + 60*15, + 60*30, + 60*60, + 60*60*2, + 60*60*4, + 60*60*8, + 60*60*12, + 60*60*24, + ] + desired = mean / 2 + if desired == 0: + interval = settings.DEFAULT_POLLING_INTERVAL + elif desired < choices[0]: + interval = choices[0] + else: + interval = max(choice for choice in choices if choice <= desired) + return interval + + +def time_since(t): + """[summary] + + Arguments: + t {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + t = int(t) + now = int(time.time()) + seconds = max(now - t, 0) + if seconds == 1: + return '1 second' + if seconds < 60: + return '%d seconds' % seconds + minutes = seconds / 60 + if minutes == 1: + return '1 minute' + if minutes < 60: + return '%d minutes' % minutes + hours = minutes / 60 + if hours == 1: + return '1 hour' + if hours < 24: + return '%d hours' % hours + days = hours / 24 + if days == 1: + return '1 day' + return '%d days' % days + + +def split_time(seconds): + """[summary] + + Arguments: + seconds {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + if seconds < 60: + return seconds, 0 + minutes = seconds / 60 + if minutes < 60: + return minutes, 1 + hours = minutes / 60 + days = hours / 24 + if days and hours % 24 == 0: + return days, 3 + return hours, 2 + + +def split_time_str(seconds): + """[summary] + + Arguments: + seconds {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + interval, units = split_time(seconds) + strings = ['second', 'minute', 'hour', 'day'] + string = strings[units] + if interval != 1: + string += 's' + return '%d %s' % (interval, string) + + +def pretty_name(name): + """[summary] + + Arguments: + name {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + name = ' '.join(s.title() for s in name.split('_')) + last = '0' + result = '' + for c in name: + if c.isdigit() and not last.isdigit(): + result += ' ' + result += c + last = c + return result + + +def replace_entities1(text): + """[summary] + + Arguments: + text {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + entity = re.compile(r'&#(\d+);') + + def func(match): + try: + return chr(int(match.group(1))) + except Exception: + return match.group(0) + return entity.sub(func, text) + + +def replace_entities2(text): + """[summary] + + Arguments: + text {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + entity = re.compile(r'&([a-zA-Z]+);') + + def func(match): + try: + return chr(name2codepoint[match.group(1)]) + except Exception: + return match.group(0) + return entity.sub(func, text) + + +def remove_markup(text): + """[summary] + + Arguments: + text {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + html = re.compile(r'<[^>]+>') + return html.sub(' ', text) + + +def format(text, max_length=400): + """[summary] + + Arguments: + text {[type]} -- [description] + + Keyword Arguments: + max_length {int} -- [description] (default: {400}) + + Returns: + [type] -- [description] + """ + + previous = '' + + while text != previous: + previous = text + text = replace_entities1(text) + text = replace_entities2(text) + + text = remove_markup(text) + text = ' '.join(text.split()) + + if len(text) > max_length: + text = text[:max_length].strip() + text = text.split()[:-1] + text.append('[...]') + text = ' '.join(text) + return text + +# EOF \ No newline at end of file diff --git a/view.py b/view.py index 518164d..5446501 100644 --- a/view.py +++ b/view.py @@ -1,1462 +1,2704 @@ -import wx -import util -import feeds -import filters -from settings import settings - -INDEX_ENABLED = 0 -INDEX_URL = 1 -INDEX_TITLE = 2 -INDEX_INTERVAL = 3 -INDEX_ITEM_COUNT = 4 -INDEX_CLICKS = 5 - -INDEX_RULES = 1 -INDEX_FEEDS = 2 -INDEX_IN = 3 -INDEX_OUT = 4 - -class TaskBarIcon(wx.TaskBarIcon): - def __init__(self, controller): - super(TaskBarIcon, self).__init__() - self.controller = controller - self.set_icon('icons/feed.png') - self.Bind(wx.EVT_TASKBAR_LEFT_DOWN, self.on_left_down) - def CreatePopupMenu(self): - menu = wx.Menu() - util.menu_item(menu, 'Add Feed...', self.on_add_feed, 'icons/add.png') - util.menu_item(menu, 'Preferences...', self.on_settings, 'icons/cog.png') - menu.AppendSeparator() - if self.controller.enabled: - util.menu_item(menu, 'Disable Updates', self.on_disable, 'icons/delete.png') - util.menu_item(menu, 'Update Now', self.on_force_update, 'icons/transmit.png') - else: - util.menu_item(menu, 'Enable Updates', self.on_enable, 'icons/accept.png') - item = util.menu_item(menu, 'Update Now', self.on_force_update, 'icons/transmit.png') - item.Enable(False) - menu.AppendSeparator() - util.menu_item(menu, 'Exit', self.on_exit, 'icons/door_out.png') - return menu - def set_icon(self, path): - icon = wx.IconFromBitmap(wx.Bitmap(path)) - self.SetIcon(icon, settings.APP_NAME) - def on_exit(self, event): - self.controller.close() - def on_left_down(self, event): - self.controller.show_popup() - def on_force_update(self, event): - self.controller.force_poll() - def on_disable(self, event): - self.controller.disable() - def on_enable(self, event): - self.controller.enable() - def on_add_feed(self, event): - self.controller.add_feed() - def on_settings(self, event): - self.controller.edit_settings() - -class AddFeedDialog(wx.Dialog): - @staticmethod - def show_wizard(parent, url=''): - while True: - window = AddFeedDialog(parent, url) - window.Center() - result = window.ShowModal() - data = window.result - window.Destroy() - if result != wx.ID_OK: - return None - url = data.original_url - entries = util.get(data, 'entries', []) - feed = feeds.Feed(url) - feed.title = util.get(data.feed, 'title', '') - feed.link = util.get(data.feed, 'link', '') - feed.username = util.encode_password(data.username) - feed.password = util.encode_password(data.password) - feed.interval = util.guess_polling_interval(entries) - window = EditFeedDialog(parent, feed, True) - window.Center() - result = window.ShowModal() - window.Destroy() - if result == wx.ID_BACKWARD: - continue - if result == wx.ID_OK: - return feed - return None - def __init__(self, parent, initial_url=''): - super(AddFeedDialog, self).__init__(parent, -1, 'Add RSS/Atom Feed') - util.set_icon(self) - #self.SetIcon(wx.IconFromBitmap(wx.Bitmap('icons/feed.png'))) - self.initial_url = initial_url - self.result = None - panel = self.create_panel(self) - self.Fit() - self.validate() - def get_initial_url(self): - if self.initial_url: - return self.initial_url - if wx.TheClipboard.Open(): - object = wx.TextDataObject() - success = wx.TheClipboard.GetData(object) - wx.TheClipboard.Close() - if success: - url = object.GetText() - if url.startswith('http'): - return url - return '' - def create_panel(self, parent): - panel = wx.Panel(parent, -1) - sizer = wx.BoxSizer(wx.VERTICAL) - controls = self.create_controls(panel) - buttons = self.create_buttons(panel) - line = wx.StaticLine(panel, -1) - sizer.AddStretchSpacer(1) - sizer.Add(controls, 0, wx.ALIGN_CENTER_HORIZONTAL|wx.ALL, 25) - sizer.AddStretchSpacer(1) - sizer.Add(line, 0, wx.EXPAND) - sizer.Add(buttons, 0, wx.EXPAND|wx.ALL, 8) - panel.SetSizerAndFit(sizer) - return panel - def create_controls(self, parent): - sizer = wx.GridBagSizer(8, 8) - label = wx.StaticText(parent, -1, 'Feed URL') - font = label.GetFont() - font.SetWeight(wx.FONTWEIGHT_BOLD) - label.SetFont(font) - value = self.get_initial_url() - value = value.replace('feed:https://', 'https://') - value = value.replace('feed://', 'http://') - url = wx.TextCtrl(parent, -1, value, size=(300, -1)) - url.Bind(wx.EVT_TEXT, self.on_text) - status = wx.StaticText(parent, -1, '') - sizer.Add(label, (0, 0), flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT) - sizer.Add(url, (0, 1)) - sizer.Add(status, (1, 1)) - self.url = url - self.status = status - return sizer - def create_buttons(self, parent): - sizer = wx.BoxSizer(wx.HORIZONTAL) - back = wx.Button(parent, wx.ID_BACKWARD, '< Back') - next = wx.Button(parent, wx.ID_FORWARD, 'Next >') - cancel = wx.Button(parent, wx.ID_CANCEL, 'Cancel') - back.Disable() - next.SetDefault() - next.Bind(wx.EVT_BUTTON, self.on_next) - self.next = next - sizer.AddStretchSpacer(1) - sizer.Add(back) - sizer.AddSpacer(4) - sizer.Add(next) - sizer.AddSpacer(16) - sizer.Add(cancel) - return sizer - def validate(self): - if self.url.GetValue(): - self.next.Enable() - else: - self.next.Disable() - def on_text(self, event): - self.validate() - def on_next(self, event): - url = self.url.GetValue() - self.lock() - util.start_thread(self.check_feed, url) - def on_valid(self, result): - self.result = result - self.EndModal(wx.ID_OK) - def on_invalid(self): - dialog = wx.MessageDialog(self, 'The URL entered does not appear to be a valid RSS/Atom feed.', 'Invalid Feed', wx.OK|wx.ICON_ERROR) - dialog.Center() - dialog.ShowModal() - dialog.Destroy() - self.unlock() - def on_password(self, url, username, password): - dialog = PasswordDialog(self, username, password) - dialog.Center() - result = dialog.ShowModal() - username = dialog.username.GetValue() - password = dialog.password.GetValue() - dialog.Destroy() - if result == wx.ID_OK: - util.start_thread(self.check_feed, url, username, password) - else: - self.unlock() - def lock(self): - self.url.Disable() - self.next.Disable() - self.status.SetLabel('Checking feed, please wait...') - def unlock(self): - self.url.Enable() - self.next.Enable() - self.status.SetLabel('') - self.url.SelectAll() - self.url.SetFocus() - def check_feed(self, url, username=None, password=None): - d = util.parse(url, username, password) - if not self: # cancelled - return - status = util.get(d, 'status', 0) - if status == 401: # auth required - wx.CallAfter(self.on_password, url, username, password) - elif util.is_valid_feed(d): - d['original_url'] = url - d['username'] = username - d['password'] = password - wx.CallAfter(self.on_valid, d) - else: - wx.CallAfter(self.on_invalid) - -class PasswordDialog(wx.Dialog): - def __init__(self, parent, username=None, password=None): - super(PasswordDialog, self).__init__(parent, -1, 'Password Required') - util.set_icon(self) - panel = self.create_panel(self) - if username: - self.username.SetValue(username) - if password: - self.password.SetValue(password) - self.Fit() - self.validate() - def create_panel(self, parent): - panel = wx.Panel(parent, -1) - sizer = wx.BoxSizer(wx.VERTICAL) - controls = self.create_controls(panel) - buttons = self.create_buttons(panel) - sizer.AddStretchSpacer(1) - sizer.Add(controls, 0, wx.ALIGN_CENTER_HORIZONTAL|wx.ALL, 12) - sizer.AddStretchSpacer(1) - sizer.Add(buttons, 0, wx.EXPAND|wx.ALL&~wx.TOP, 12) - panel.SetSizerAndFit(sizer) - return panel - def create_controls(self, parent): - sizer = wx.GridBagSizer(8, 8) - label = wx.StaticText(parent, -1, 'Username') - username = wx.TextCtrl(parent, -1, '', size=(180, -1)) - username.Bind(wx.EVT_TEXT, self.on_text) - sizer.Add(label, (0, 0), flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT) - sizer.Add(username, (0, 1)) - self.username = username - label = wx.StaticText(parent, -1, 'Password') - password = wx.TextCtrl(parent, -1, '', size=(180, -1), style=wx.TE_PASSWORD) - password.Bind(wx.EVT_TEXT, self.on_text) - sizer.Add(label, (1, 0), flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT) - sizer.Add(password, (1, 1)) - self.password = password - return sizer - def create_buttons(self, parent): - ok = wx.Button(parent, wx.ID_OK, 'OK') - cancel = wx.Button(parent, wx.ID_CANCEL, 'Cancel') - ok.SetDefault() - ok.Disable() - self.ok = ok - sizer = wx.BoxSizer(wx.HORIZONTAL) - sizer.AddStretchSpacer(1) - sizer.Add(ok) - sizer.AddSpacer(8) - sizer.Add(cancel) - return sizer - def validate(self): - if self.username.GetValue() and self.password.GetValue(): - self.ok.Enable() - else: - self.ok.Disable() - def on_text(self, event): - self.validate() - -class EditFeedDialog(wx.Dialog): - def __init__(self, parent, feed, add=False): - title = 'Add RSS/Atom Feed' if add else 'Edit RSS/Atom Feed' - super(EditFeedDialog, self).__init__(parent, -1, title) - util.set_icon(self) - #self.SetIcon(wx.IconFromBitmap(wx.Bitmap('icons/feed.png'))) - self.feed = feed - self.add = add - panel = self.create_panel(self) - self.Fit() - self.validate() - def create_panel(self, parent): - panel = wx.Panel(parent, -1) - sizer = wx.BoxSizer(wx.VERTICAL) - controls = self.create_controls(panel) - if self.add: - buttons = self.create_add_buttons(panel) - else: - buttons = self.create_edit_buttons(panel) - line = wx.StaticLine(panel, -1) - sizer.AddStretchSpacer(1) - sizer.Add(controls, 0, wx.ALIGN_CENTER_HORIZONTAL|wx.ALL, 25) - sizer.AddStretchSpacer(1) - sizer.Add(line, 0, wx.EXPAND) - sizer.Add(buttons, 0, wx.EXPAND|wx.ALL, 8) - panel.SetSizerAndFit(sizer) - return panel - def create_controls(self, parent): - sizer = wx.GridBagSizer(8, 8) - indexes = [0, 1, 3, 5, 7] - labels = ['Feed URL', 'Feed Title', 'Feed Link', 'Polling Interval', 'Border Color'] - for index, text in zip(indexes, labels): - label = wx.StaticText(parent, -1, text) - font = label.GetFont() - font.SetWeight(wx.FONTWEIGHT_BOLD) - label.SetFont(font) - sizer.Add(label, (index, 0), flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT) - controls = [] - for index in indexes[:-2]: - style = wx.TE_READONLY if index == 0 else 0 - control = wx.TextCtrl(parent, -1, '', size=(300, -1), style=style) - control.Bind(wx.EVT_TEXT, self.on_text) - sizer.Add(control, (index, 1), (1, 2)) - controls.append(control) - url, title, link = controls - self.url, self.title, self.link = controls - url.ChangeValue(self.feed.url) - title.ChangeValue(self.feed.title) - link.ChangeValue(self.feed.link) - url.SetBackgroundColour(parent.GetBackgroundColour()) - _interval, _units = util.split_time(self.feed.interval) - interval = wx.SpinCtrl(parent, -1, str(_interval), min=1, max=60, size=(64, -1)) - units = wx.Choice(parent, -1) - units.Append('second(s)', 1) - units.Append('minute(s)', 60) - units.Append('hour(s)', 60*60) - units.Append('day(s)', 60*60*24) - units.Select(_units) - self.interval, self.units = interval, units - sizer.Add(interval, (5, 1)) - sizer.Add(units, (5, 2)) - self.color = color = wx.Button(parent, -1) - color.Bind(wx.EVT_BUTTON, self.on_color) - color._color = self.feed.color - _color = self.feed.color or settings.POPUP_BORDER_COLOR - color.SetBackgroundColour(wx.Colour(*_color)) - sizer.Add(color, (7, 1)) - self.default = default = wx.Button(parent, -1, 'Use Default') - default.Bind(wx.EVT_BUTTON, self.on_default) - sizer.Add(default, (7, 2)) - label = wx.StaticText(parent, -1, 'The feed title will be shown in the pop-up window for items from this feed.') - label.Wrap(300) - sizer.Add(label, (2, 1), (1, 2), flag=wx.ALIGN_CENTER_VERTICAL) - label = wx.StaticText(parent, -1, 'The feed link will launch in your browser if you click on the feed title in a pop-up window.') - label.Wrap(300) - sizer.Add(label, (4, 1), (1, 2), flag=wx.ALIGN_CENTER_VERTICAL) - label = wx.StaticText(parent, -1, 'The polling interval specifies how often the application will check the feed for new items. When adding a new feed, the application automatically fills this in by examining the items in the feed.') - label.Wrap(300) - sizer.Add(label, (6, 1), (1, 2), flag=wx.ALIGN_CENTER_VERTICAL) - label = wx.StaticText(parent, -1, 'The color specifies the border color of pop-up windows for this feed, if you want to override the default.') - label.Wrap(300) - sizer.Add(label, (8, 1), (1, 2), flag=wx.ALIGN_CENTER_VERTICAL) - return sizer - def create_add_buttons(self, parent): - sizer = wx.BoxSizer(wx.HORIZONTAL) - back = wx.Button(parent, wx.ID_BACKWARD, '< Back') - next = wx.Button(parent, wx.ID_FORWARD, 'Finish') - cancel = wx.Button(parent, wx.ID_CANCEL, 'Cancel') - next.SetDefault() - next.Bind(wx.EVT_BUTTON, self.on_next) - back.Bind(wx.EVT_BUTTON, self.on_back) - self.next = next - sizer.AddStretchSpacer(1) - sizer.Add(back) - sizer.AddSpacer(4) - sizer.Add(next) - sizer.AddSpacer(16) - sizer.Add(cancel) - return sizer - def create_edit_buttons(self, parent): - sizer = wx.BoxSizer(wx.HORIZONTAL) - next = wx.Button(parent, wx.ID_FORWARD, 'OK') - cancel = wx.Button(parent, wx.ID_CANCEL, 'Cancel') - next.SetDefault() - next.Bind(wx.EVT_BUTTON, self.on_next) - self.next = next - sizer.AddStretchSpacer(1) - sizer.Add(next) - sizer.AddSpacer(8) - sizer.Add(cancel) - return sizer - def validate(self): - controls = [self.url, self.title, self.link] - if all(control.GetValue() for control in controls): - self.next.Enable() - else: - self.next.Disable() - def on_color(self, event): - data = wx.ColourData() - data.SetColour(self.color.GetBackgroundColour()) - dialog = wx.ColourDialog(self, data) - if dialog.ShowModal() == wx.ID_OK: - color = dialog.GetColourData().GetColour() - self.color.SetBackgroundColour(color) - self.color._color = (color.Red(), color.Green(), color.Blue()) - def on_default(self, event): - self.color.SetBackgroundColour(wx.Colour(*settings.POPUP_BORDER_COLOR)) - self.color._color = None - def on_text(self, event): - self.validate() - def on_back(self, event): - self.EndModal(wx.ID_BACKWARD) - def on_next(self, event): - url = self.url.GetValue() - title = self.title.GetValue() - link = self.link.GetValue() - interval = int(self.interval.GetValue()) - multiplier = self.units.GetClientData(self.units.GetSelection()) - interval = interval * multiplier - if interval < 60: - dialog = wx.MessageDialog(self, 'Are you sure you want to check this feed every %d second(s)?\n\nYou might make the website administrator unhappy!' % interval, 'Confirm Polling Interval', wx.YES_NO|wx.NO_DEFAULT|wx.ICON_QUESTION) - result = dialog.ShowModal() - dialog.Destroy() - if result == wx.ID_NO: - return - self.feed.title = title - self.feed.link = link - self.feed.interval = interval - self.feed.color = self.color._color - self.EndModal(wx.ID_OK) - -class EditFilterDialog(wx.Dialog): - def __init__(self, parent, model, filter=None): - title = 'Edit Filter' if filter else 'Add Filter' - style = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER - super(EditFilterDialog, self).__init__(parent, -1, title, style=style) - util.set_icon(self) - self.model = model - self.filter = filter or feeds.Filter('') - panel = self.create_panel(self) - buttons = self.create_buttons(self) - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(panel, 1, wx.EXPAND|wx.ALL, 8) - sizer.Add(buttons, 0, wx.EXPAND|wx.ALL&~wx.TOP, 8) - self.SetSizerAndFit(sizer) - self.validate() - def create_panel(self, parent): - panel = wx.Panel(parent, -1) - rules = self.create_rules(panel) - options = self.create_options(panel) - sizer = wx.BoxSizer(wx.HORIZONTAL) - sizer.Add(rules, 1, wx.EXPAND) - sizer.AddSpacer(8) - sizer.Add(options, 0, wx.EXPAND) - panel.SetSizer(sizer) - return panel - def create_buttons(self, parent): - ok = wx.Button(parent, wx.ID_OK, 'OK') - cancel = wx.Button(parent, wx.ID_CANCEL, 'Cancel') - sizer = wx.BoxSizer(wx.HORIZONTAL) - sizer.AddStretchSpacer(1) - sizer.Add(ok) - sizer.AddSpacer(8) - sizer.Add(cancel) - ok.SetDefault() - ok.Bind(wx.EVT_BUTTON, self.on_ok) - self.ok = ok - return sizer - def create_rules(self, parent): - box = wx.StaticBox(parent, -1, 'Filter Rules') - box = wx.StaticBoxSizer(box, wx.VERTICAL) - code = wx.TextCtrl(parent, -1, self.filter.code, style=wx.TE_MULTILINE, size=(250, -1)) - text = ''' - Examples: - -microsoft and -apple (exclude microsoft and apple) - google or yahoo (require google or yahoo) - -author:BoringGuy (search author field only) - ''' - text = '\n'.join(line.strip() for line in text.strip().split('\n')) - help = wx.StaticText(parent, -1, text) - box.Add(code, 1, wx.EXPAND|wx.ALL, 8) - box.Add(help, 0, wx.EXPAND|wx.ALL&~wx.TOP, 8) - code.Bind(wx.EVT_TEXT, self.on_event) - self.code = code - return box - def create_options(self, parent): - sizer = wx.BoxSizer(wx.VERTICAL) - box = wx.StaticBox(parent, -1, 'Options') - box = wx.StaticBoxSizer(box, wx.VERTICAL) - match_case = wx.CheckBox(parent, -1, 'Match Case') - match_whole_words = wx.CheckBox(parent, -1, 'Match Whole Words') - match_case.SetValue(not self.filter.ignore_case) - match_whole_words.SetValue(self.filter.whole_word) - box.Add(match_case, 0, wx.ALL, 8) - box.Add(match_whole_words, 0, wx.ALL&~wx.TOP, 8) - sizer.Add(box, 0, wx.EXPAND) - sizer.AddSpacer(8) - box = wx.StaticBox(parent, -1, 'Apply Filter To') - box = wx.StaticBoxSizer(box, wx.VERTICAL) - all_feeds = wx.RadioButton(parent, -1, 'All Feeds', style=wx.RB_GROUP) - selected_feeds = wx.RadioButton(parent, -1, 'Selected Feeds') - if self.filter.feeds: - selected_feeds.SetValue(True) - feeds = wx.CheckListBox(parent, -1, size=(150, 150), style=wx.LB_HSCROLL|wx.LB_EXTENDED) - def cmp_title(a, b): - return cmp(a.title.lower(), b.title.lower()) - self.lookup = {} - items = self.model.controller.manager.feeds - for index, feed in enumerate(sorted(items, cmp=cmp_title)): - feeds.Append(feed.title) - self.lookup[index] = feed - feeds.Check(index, feed in self.filter.feeds) - box.Add(all_feeds, 0, wx.ALL, 8) - box.Add(selected_feeds, 0, wx.ALL&~wx.TOP, 8) - box.Add(feeds, 1, wx.ALL&~wx.TOP, 8) - sizer.Add(box, 1, wx.EXPAND) - match_case.Bind(wx.EVT_CHECKBOX, self.on_event) - match_whole_words.Bind(wx.EVT_CHECKBOX, self.on_event) - all_feeds.Bind(wx.EVT_RADIOBUTTON, self.on_event) - selected_feeds.Bind(wx.EVT_RADIOBUTTON, self.on_event) - feeds.Bind(wx.EVT_CHECKLISTBOX, self.on_event) - self.match_case = match_case - self.match_whole_words = match_whole_words - self.all_feeds = all_feeds - self.selected_feeds = selected_feeds - self.feeds = feeds - return sizer - def get_selected_feeds(self): - result = set() - if self.selected_feeds.GetValue(): - for index in range(self.feeds.GetCount()): - if self.feeds.IsChecked(index): - result.add(self.lookup[index]) - return result - def validate(self): - feeds = self.get_selected_feeds() - valid = True - valid = valid and self.code.GetValue() - valid = valid and (self.all_feeds.GetValue() or feeds) - try: - filters.parse(self.code.GetValue()) - except Exception: - valid = False - self.ok.Enable(bool(valid)) - self.feeds.Enable(self.selected_feeds.GetValue()) - def on_event(self, event): - self.validate() - def on_ok(self, event): - filter = self.filter - filter.code = self.code.GetValue() - filter.ignore_case = not self.match_case.GetValue() - filter.whole_word = self.match_whole_words.GetValue() - filter.feeds = self.get_selected_feeds() - event.Skip() - -class Model(object): - def __init__(self, controller): - self.controller = controller - self.reset() - def reset(self): - self._feed_sort = -1 - self._filter_sort = -1 - feeds = self.controller.manager.feeds - feeds = [feed.make_copy() for feed in feeds] - self.feeds = feeds - filters = self.controller.manager.filters - filters = [filter.make_copy() for filter in filters] - self.filters = filters - self.settings = {} - def __getattr__(self, key): - if key != key.upper(): - return super(Model, self).__getattr__(key) - if key in self.settings: - return self.settings[key] - return getattr(settings, key) - def __setattr__(self, key, value): - if key != key.upper(): - return super(Model, self).__setattr__(key, value) - self.settings[key] = value - def apply(self): - self.apply_filters() - self.apply_feeds() - self.apply_settings() - self.controller.save() - def apply_settings(self): - for key, value in self.settings.items(): - setattr(settings, key, value) - def apply_feeds(self): - before = {} - after = {} - controller = self.controller - for feed in controller.manager.feeds: - before[feed.uuid] = feed - for feed in self.feeds: - after[feed.uuid] = feed - before_set = set(before.keys()) - after_set = set(after.keys()) - added = after_set - before_set - deleted = before_set - after_set - same = after_set & before_set - for uuid in added: - feed = after[uuid] - controller.manager.add_feed(feed) - for uuid in deleted: - feed = before[uuid] - controller.manager.remove_feed(feed) - for uuid in same: - a = before[uuid] - b = after[uuid] - a.copy_from(b) - def apply_filters(self): - before = {} - after = {} - controller = self.controller - for filter in controller.manager.filters: - before[filter.uuid] = filter - for filter in self.filters: - after[filter.uuid] = filter - before_set = set(before.keys()) - after_set = set(after.keys()) - added = after_set - before_set - deleted = before_set - after_set - same = after_set & before_set - for uuid in added: - filter = after[uuid] - controller.manager.add_filter(filter) - for uuid in deleted: - filter = before[uuid] - controller.manager.remove_filter(filter) - for uuid in same: - a = before[uuid] - b = after[uuid] - a.copy_from(b) - def sort_feeds(self, column): - def cmp_enabled(a, b): - return cmp(a.enabled, b.enabled) - def cmp_clicks(a, b): - return cmp(b.clicks, a.clicks) - def cmp_item_count(a, b): - return cmp(b.item_count, a.item_count) - def cmp_interval(a, b): - return cmp(a.interval, b.interval) - def cmp_title(a, b): - return cmp(a.title.lower(), b.title.lower()) - def cmp_url(a, b): - return cmp(a.url.lower(), b.url.lower()) - funcs = { - INDEX_ENABLED: cmp_enabled, - INDEX_URL: cmp_url, - INDEX_TITLE: cmp_title, - INDEX_INTERVAL: cmp_interval, - INDEX_CLICKS: cmp_clicks, - INDEX_ITEM_COUNT: cmp_item_count, - } - self.feeds.sort(cmp=funcs[column]) - if column == self._feed_sort: - self.feeds.reverse() - self._feed_sort = -1 - else: - self._feed_sort = column - def sort_filters(self, column): - def cmp_enabled(a, b): - return cmp(a.enabled, b.enabled) - def cmp_rules(a, b): - return cmp(a.code, b.code) - def cmp_feeds(a, b): - return cmp(len(a.feeds), len(b.feeds)) - def cmp_in(a, b): - return cmp(b.inputs, a.inputs) - def cmp_out(a, b): - return cmp(b.outputs, a.outputs) - funcs = { - INDEX_ENABLED: cmp_enabled, - INDEX_RULES: cmp_rules, - INDEX_FEEDS: cmp_feeds, - INDEX_IN: cmp_in, - INDEX_OUT: cmp_out, - } - self.filters.sort(cmp=funcs[column]) - if column == self._filter_sort: - self.filters.reverse() - self._filter_sort = -1 - else: - self._filter_sort = column - -class SettingsDialog(wx.Dialog): - def __init__(self, parent, controller): - title = '%s Preferences' % settings.APP_NAME - style = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER - super(SettingsDialog, self).__init__(parent, -1, title, style=style) - util.set_icon(self) - #self.SetIcon(wx.IconFromBitmap(wx.Bitmap('icons/feed.png'))) - self.model = Model(controller) - panel = self.create_panel(self) - self.Fit() - self.SetMinSize(self.GetSize()) - def create_panel(self, parent): - panel = wx.Panel(parent, -1) - notebook = self.create_notebook(panel) - line = wx.StaticLine(panel, -1) - buttons = self.create_buttons(panel) - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(notebook, 1, wx.EXPAND|wx.ALL, 0) - sizer.Add(line, 0, wx.EXPAND) - sizer.Add(buttons, 0, wx.EXPAND|wx.ALL, 8) - panel.SetSizerAndFit(sizer) - return panel - def create_notebook(self, parent): - images = wx.ImageList(48, 32) - images.Add(util.scale_bitmap(wx.Bitmap('icons/feed32.png'), -1, -1, self.GetBackgroundColour())) - images.Add(util.scale_bitmap(wx.Bitmap('icons/comment32.png'), -1, -1, self.GetBackgroundColour())) - images.Add(util.scale_bitmap(wx.Bitmap('icons/cog32.png'), -1, -1, self.GetBackgroundColour())) - images.Add(util.scale_bitmap(wx.Bitmap('icons/filter32.png'), -1, -1, self.GetBackgroundColour())) - images.Add(util.scale_bitmap(wx.Bitmap('icons/info32.png'), -1, -1, self.GetBackgroundColour())) - notebook = wx.Toolbook(parent, -1) - notebook.SetInternalBorder(0) - notebook.AssignImageList(images) - feeds = FeedsPanel(notebook, self) - popups = PopupsPanel(notebook, self) - options = OptionsPanel(notebook, self) - filters = FiltersPanel(notebook, self) - about = AboutPanel(notebook) - notebook.AddPage(feeds, 'Feeds', imageId=0) - notebook.AddPage(popups, 'Pop-ups', imageId=1) - notebook.AddPage(options, 'Options', imageId=2) - notebook.AddPage(filters, 'Filters', imageId=3) - notebook.AddPage(about, 'About', imageId=4) - self.popups = popups - self.options = options - notebook.Fit() - return notebook - def create_buttons(self, parent): - sizer = wx.BoxSizer(wx.HORIZONTAL) - ok = wx.Button(parent, wx.ID_OK, 'OK') - cancel = wx.Button(parent, wx.ID_CANCEL, 'Cancel') - apply = wx.Button(parent, wx.ID_APPLY, 'Apply') - ok.Bind(wx.EVT_BUTTON, self.on_ok) - apply.Bind(wx.EVT_BUTTON, self.on_apply) - ok.SetDefault() - apply.Disable() - self.apply_button = apply - sizer.AddStretchSpacer(1) - sizer.Add(ok) - sizer.AddSpacer(8) - sizer.Add(cancel) - sizer.AddSpacer(8) - sizer.Add(apply) - return sizer - def apply(self): - self.popups.update_model() - self.options.update_model() - self.model.apply() - self.model.controller.poll() - def on_change(self): - self.apply_button.Enable() - def on_ok(self, event): - self.apply() - event.Skip() - def on_apply(self, event): - self.apply() - self.apply_button.Disable() - -class FeedsList(wx.ListCtrl): - def __init__(self, parent, dialog): - style = wx.LC_REPORT|wx.LC_VIRTUAL#|wx.LC_HRULES|wx.LC_VRULES - super(FeedsList, self).__init__(parent, -1, style=style) - self.dialog = dialog - self.model = dialog.model - images = wx.ImageList(16, 16, True) - images.AddWithColourMask(wx.Bitmap('icons/unchecked.png'), wx.WHITE) - images.AddWithColourMask(wx.Bitmap('icons/checked.png'), wx.WHITE) - self.AssignImageList(images, wx.IMAGE_LIST_SMALL) - self.InsertColumn(INDEX_ENABLED, 'On') - self.InsertColumn(INDEX_URL, 'Feed URL') - self.InsertColumn(INDEX_TITLE, 'Feed Title') - self.InsertColumn(INDEX_INTERVAL, 'Interval') - self.InsertColumn(INDEX_ITEM_COUNT, 'Items') - self.InsertColumn(INDEX_CLICKS, 'Clicks') - self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down) - self.Bind(wx.EVT_LIST_COL_CLICK, self.on_col_click) - self.update() - self.SetColumnWidth(INDEX_ENABLED, 32) - self.SetColumnWidth(INDEX_URL, 165) - self.SetColumnWidth(INDEX_TITLE, 165) - self.SetColumnWidth(INDEX_INTERVAL, 75) - self.SetColumnWidth(INDEX_ITEM_COUNT, -2) - self.SetColumnWidth(INDEX_CLICKS, -2) - def update(self): - self.SetItemCount(len(self.model.feeds)) - self.Refresh() - def on_col_click(self, event): - column = event.GetColumn() - self.model.sort_feeds(column) - self.update() - def on_left_down(self, event): - index, flags = self.HitTest(event.GetPosition()) - if index >= 0 and (flags & wx.LIST_HITTEST_ONITEMICON): - self.toggle(index) - event.Skip() - def toggle(self, index): - feed = self.model.feeds[index] - feed.enabled = not feed.enabled - self.RefreshItem(index) - self.dialog.on_change() - def OnGetItemImage(self, index): - feed = self.model.feeds[index] - return 1 if feed.enabled else 0 - def OnGetItemText(self, index, column): - feed = self.model.feeds[index] - if column == INDEX_URL: - return feed.url - if column == INDEX_TITLE: - return feed.title - if column == INDEX_INTERVAL: - return util.split_time_str(feed.interval) - if column == INDEX_CLICKS: - return str(feed.clicks) if feed.clicks else '' - if column == INDEX_ITEM_COUNT: - return str(feed.item_count) if feed.item_count else '' - return '' - -class FiltersList(wx.ListCtrl): - def __init__(self, parent, dialog): - style = wx.LC_REPORT|wx.LC_VIRTUAL#|wx.LC_HRULES|wx.LC_VRULES - super(FiltersList, self).__init__(parent, -1, style=style) - self.dialog = dialog - self.model = dialog.model - images = wx.ImageList(16, 16, True) - images.AddWithColourMask(wx.Bitmap('icons/unchecked.png'), wx.WHITE) - images.AddWithColourMask(wx.Bitmap('icons/checked.png'), wx.WHITE) - self.AssignImageList(images, wx.IMAGE_LIST_SMALL) - self.InsertColumn(INDEX_ENABLED, 'On') - self.InsertColumn(INDEX_RULES, 'Filter Rules') - self.InsertColumn(INDEX_FEEDS, 'Feeds') - self.InsertColumn(INDEX_IN, 'In') - self.InsertColumn(INDEX_OUT, 'Out') - self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down) - self.Bind(wx.EVT_LIST_COL_CLICK, self.on_col_click) - self.update() - self.SetColumnWidth(INDEX_ENABLED, 32) - self.SetColumnWidth(INDEX_RULES, 200) - self.SetColumnWidth(INDEX_FEEDS, 64) - self.SetColumnWidth(INDEX_IN, 64) - self.SetColumnWidth(INDEX_OUT, 64) - def update(self): - self.SetItemCount(len(self.model.filters)) - self.Refresh() - def on_col_click(self, event): - column = event.GetColumn() - self.model.sort_filters(column) - self.update() - def on_left_down(self, event): - index, flags = self.HitTest(event.GetPosition()) - if index >= 0 and (flags & wx.LIST_HITTEST_ONITEMICON): - self.toggle(index) - event.Skip() - def toggle(self, index): - filter = self.model.filters[index] - filter.enabled = not filter.enabled - self.RefreshItem(index) - self.dialog.on_change() - def OnGetItemImage(self, index): - filter = self.model.filters[index] - return 1 if filter.enabled else 0 - def OnGetItemText(self, index, column): - filter = self.model.filters[index] - if column == INDEX_RULES: - return filter.code.replace('\n', ' ') - if column == INDEX_FEEDS: - return str(len(filter.feeds)) if filter.feeds else 'All' - if column == INDEX_IN: - return str(filter.inputs) - if column == INDEX_OUT: - return str(filter.outputs) - return '' - -class FeedsPanel(wx.Panel): - def __init__(self, parent, dialog): - super(FeedsPanel, self).__init__(parent, -1) - self.dialog = dialog - self.model = dialog.model - panel = self.create_panel(self) - sizer = wx.BoxSizer(wx.VERTICAL) - line = wx.StaticLine(self, -1) - sizer.Add(line, 0, wx.EXPAND) - sizer.Add(panel, 1, wx.EXPAND|wx.ALL, 8) - self.SetSizerAndFit(sizer) - def create_panel(self, parent): - panel = wx.Panel(parent, -1) - list = FeedsList(panel, self.dialog) - list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_selection) - list.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.on_selection) - list.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.on_edit) - list.Bind(wx.EVT_LEFT_DOWN, self.on_left_down) - self.list = list - buttons = self.create_buttons(panel) - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(list, 1, wx.EXPAND) - sizer.AddSpacer(8) - sizer.Add(buttons, 0, wx.EXPAND) - panel.SetSizerAndFit(sizer) - return panel - def create_buttons(self, parent): - new = wx.Button(parent, -1, 'Add...') - #import_feeds = wx.Button(parent, -1, 'Import...') - edit = wx.Button(parent, -1, 'Edit...') - delete = wx.Button(parent, -1, 'Delete') - new.Bind(wx.EVT_BUTTON, self.on_new) - edit.Bind(wx.EVT_BUTTON, self.on_edit) - delete.Bind(wx.EVT_BUTTON, self.on_delete) - edit.Disable() - delete.Disable() - self.edit = edit - self.delete = delete - sizer = wx.BoxSizer(wx.HORIZONTAL) - sizer.Add(new) - sizer.AddSpacer(8) - #sizer.Add(import_feeds) - #sizer.AddSpacer(8) - sizer.Add(edit) - sizer.AddSpacer(8) - sizer.Add(delete) - sizer.AddStretchSpacer(1) - return sizer - def update(self): - self.list.update() - self.update_buttons() - self.dialog.on_change() - def on_selection(self, event): - event.Skip() - self.update_buttons() - def update_buttons(self): - count = self.list.GetSelectedItemCount() - self.edit.Enable(count == 1) - self.delete.Enable(count > 0) - def on_left_down(self, event): - index, flags = self.list.HitTest(event.GetPosition()) - if flags & wx.LIST_HITTEST_NOWHERE: - self.edit.Disable() - self.delete.Disable() - event.Skip() - def on_edit(self, event): - count = self.list.GetSelectedItemCount() - if count != 1: - return - index = self.list.GetNextItem(-1, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED) - feed = self.model.feeds[index] - window = EditFeedDialog(self, feed) - window.CenterOnScreen() - result = window.ShowModal() - window.Destroy() - if result == wx.ID_OK: - self.update() - def on_new(self, event): - feed = AddFeedDialog.show_wizard(self) - if feed: - self.model.feeds.append(feed) - self.update() - def on_delete(self, event): - dialog = wx.MessageDialog(self.dialog, 'Are you sure you want to delete the selected feed(s)?', 'Confirm Delete', wx.YES_NO|wx.NO_DEFAULT|wx.ICON_QUESTION) - result = dialog.ShowModal() - dialog.Destroy() - if result != wx.ID_YES: - return - feeds = [] - index = -1 - while True: - index = self.list.GetNextItem(index, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED) - if index < 0: - break - feed = self.model.feeds[index] - feeds.append(feed) - if feeds: - for feed in feeds: - self.model.feeds.remove(feed) - self.update() - -class FiltersPanel(wx.Panel): - def __init__(self, parent, dialog): - super(FiltersPanel, self).__init__(parent, -1) - self.dialog = dialog - self.model = dialog.model - panel = self.create_panel(self) - sizer = wx.BoxSizer(wx.VERTICAL) - line = wx.StaticLine(self, -1) - sizer.Add(line, 0, wx.EXPAND) - sizer.Add(panel, 1, wx.EXPAND|wx.ALL, 8) - self.SetSizerAndFit(sizer) - def create_panel(self, parent): - panel = wx.Panel(parent, -1) - list = FiltersList(panel, self.dialog) - list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_selection) - list.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.on_selection) - list.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.on_edit) - list.Bind(wx.EVT_LEFT_DOWN, self.on_left_down) - self.list = list - buttons = self.create_buttons(panel) - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(list, 1, wx.EXPAND) - sizer.AddSpacer(8) - sizer.Add(buttons, 0, wx.EXPAND) - panel.SetSizerAndFit(sizer) - return panel - def create_buttons(self, parent): - new = wx.Button(parent, -1, 'Add...') - edit = wx.Button(parent, -1, 'Edit...') - delete = wx.Button(parent, -1, 'Delete') - new.Bind(wx.EVT_BUTTON, self.on_new) - edit.Bind(wx.EVT_BUTTON, self.on_edit) - delete.Bind(wx.EVT_BUTTON, self.on_delete) - edit.Disable() - delete.Disable() - self.edit = edit - self.delete = delete - sizer = wx.BoxSizer(wx.HORIZONTAL) - sizer.Add(new) - sizer.AddSpacer(8) - sizer.Add(edit) - sizer.AddSpacer(8) - sizer.Add(delete) - sizer.AddStretchSpacer(1) - return sizer - def update(self): - self.list.update() - self.update_buttons() - self.dialog.on_change() - def on_selection(self, event): - event.Skip() - self.update_buttons() - def update_buttons(self): - count = self.list.GetSelectedItemCount() - self.edit.Enable(count == 1) - self.delete.Enable(count > 0) - def on_left_down(self, event): - index, flags = self.list.HitTest(event.GetPosition()) - if flags & wx.LIST_HITTEST_NOWHERE: - self.edit.Disable() - self.delete.Disable() - event.Skip() - def on_edit(self, event): - count = self.list.GetSelectedItemCount() - if count != 1: - return - index = self.list.GetNextItem(-1, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED) - filter = self.model.filters[index] - window = EditFilterDialog(self, self.model, filter) - window.Center() - result = window.ShowModal() - window.Destroy() - if result == wx.ID_OK: - self.update() - def on_new(self, event): - window = EditFilterDialog(self, self.model) - window.Center() - result = window.ShowModal() - filter = window.filter - window.Destroy() - if result == wx.ID_OK: - self.model.filters.append(filter) - self.update() - def on_delete(self, event): - dialog = wx.MessageDialog(self.dialog, 'Are you sure you want to delete the selected filter(s)?', 'Confirm Delete', wx.YES_NO|wx.NO_DEFAULT|wx.ICON_QUESTION) - result = dialog.ShowModal() - dialog.Destroy() - if result != wx.ID_YES: - return - filters = [] - index = -1 - while True: - index = self.list.GetNextItem(index, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED) - if index < 0: - break - filter = self.model.filters[index] - filters.append(filter) - if filters: - for filter in filters: - self.model.filters.remove(filter) - self.update() - -class PopupsPanel(wx.Panel): - def __init__(self, parent, dialog): - super(PopupsPanel, self).__init__(parent, -1) - self.dialog = dialog - self.model = dialog.model - panel = self.create_panel(self) - sizer = wx.BoxSizer(wx.VERTICAL) - line = wx.StaticLine(self, -1) - sizer.Add(line, 0, wx.EXPAND) - sizer.Add(panel, 1, wx.EXPAND|wx.ALL, 8) - self.update_controls() - self.SetSizerAndFit(sizer) - def create_panel(self, parent): - panel = wx.Panel(parent, -1) - sizer = wx.BoxSizer(wx.VERTICAL) - behavior = self.create_behavior(panel) - appearance = self.create_appearance(panel) - content = self.create_content(panel) - sizer.Add(behavior, 0, wx.EXPAND) - sizer.AddSpacer(8) - sizer.Add(appearance, 0, wx.EXPAND) - sizer.AddSpacer(8) - sizer.Add(content, 0, wx.EXPAND) - panel.SetSizerAndFit(sizer) - return panel - def create_appearance(self, parent): - box = wx.StaticBox(parent, -1, 'Appearance') - sizer = wx.StaticBoxSizer(box, wx.VERTICAL) - grid = wx.GridBagSizer(8, 8) - labels = ['Position', 'Width', 'Monitor', 'Transparency', 'Border', 'Border Size'] - positions = [(0, 0), (0, 3), (1, 0), (1, 3), (2, 0), (2, 3)] - for label, position in zip(labels, positions): - text = wx.StaticText(parent, -1, label) - grid.Add(text, position, flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT) - position = wx.Choice(parent, -1) - position.Append('Upper Left', (-1, -1)) - position.Append('Upper Right', (1, -1)) - position.Append('Lower Left', (-1, 1)) - position.Append('Lower Right', (1, 1)) - position.Append('Center', (0, 0)) - width = wx.SpinCtrl(parent, -1, '1', min=1, max=9999, size=(64, -1)) - transparency = wx.SpinCtrl(parent, -1, '0', min=0, max=255, size=(64, -1)) - display = wx.Choice(parent, -1) - for index in range(wx.Display_GetCount()): - display.Append('Monitor #%d' % (index + 1), index) - border_color = wx.Button(parent, -1) - border_size = wx.SpinCtrl(parent, -1, '1', min=0, max=9, size=(64, -1)) - - grid.Add(position, (0, 1), flag=wx.EXPAND) - grid.Add(display, (1, 1), flag=wx.EXPAND) - grid.Add(width, (0, 4)) - grid.Add(transparency, (1, 4)) - grid.Add(border_color, (2, 1), flag=wx.EXPAND) - grid.Add(border_size, (2, 4)) - text = wx.StaticText(parent, -1, 'pixels') - grid.Add(text, (0, 5), flag=wx.ALIGN_CENTER_VERTICAL) - text = wx.StaticText(parent, -1, '[0-255], 255=opaque') - grid.Add(text, (1, 5), flag=wx.ALIGN_CENTER_VERTICAL) - text = wx.StaticText(parent, -1, 'pixels') - grid.Add(text, (2, 5), flag=wx.ALIGN_CENTER_VERTICAL) - sizer.Add(grid, 1, wx.EXPAND|wx.ALL, 8) - - position.Bind(wx.EVT_CHOICE, self.on_change) - display.Bind(wx.EVT_CHOICE, self.on_change) - width.Bind(wx.EVT_SPINCTRL, self.on_change) - transparency.Bind(wx.EVT_SPINCTRL, self.on_change) - border_size.Bind(wx.EVT_SPINCTRL, self.on_change) - border_color.Bind(wx.EVT_BUTTON, self.on_border_color) - - self.position = position - self.display = display - self.width = width - self.transparency = transparency - self.border_color = border_color - self.border_size = border_size - return sizer - def create_behavior(self, parent): - box = wx.StaticBox(parent, -1, 'Behavior') - sizer = wx.StaticBoxSizer(box, wx.VERTICAL) - grid = wx.GridBagSizer(8, 8) - - text = wx.StaticText(parent, -1, 'Duration') - grid.Add(text, (0, 0), flag=wx.ALIGN_CENTER_VERTICAL) - text = wx.StaticText(parent, -1, 'seconds') - grid.Add(text, (0, 2), flag=wx.ALIGN_CENTER_VERTICAL) - - duration = wx.SpinCtrl(parent, -1, '1', min=1, max=60, size=(64, -1)) - auto = wx.CheckBox(parent, -1, 'Infinite duration') - sound = wx.CheckBox(parent, -1, 'Sound notification') - hover = wx.CheckBox(parent, -1, 'Wait if hovering') - top = wx.CheckBox(parent, -1, 'Stay on top') - - grid.Add(duration, (0, 1)) - grid.Add(auto, (0, 4), flag=wx.ALIGN_CENTER_VERTICAL) - grid.Add(sound, (1, 4), flag=wx.ALIGN_CENTER_VERTICAL) - grid.Add(hover, (0, 6), flag=wx.ALIGN_CENTER_VERTICAL) - grid.Add(top, (1, 6), flag=wx.ALIGN_CENTER_VERTICAL) - - sizer.Add(grid, 1, wx.EXPAND|wx.ALL, 8) - - duration.Bind(wx.EVT_SPINCTRL, self.on_change) - auto.Bind(wx.EVT_CHECKBOX, self.on_change) - sound.Bind(wx.EVT_CHECKBOX, self.on_change) - hover.Bind(wx.EVT_CHECKBOX, self.on_change) - top.Bind(wx.EVT_CHECKBOX, self.on_change) - - self.duration = duration - self.auto = auto - self.sound = sound - self.hover = hover - self.top = top - return sizer - def create_content(self, parent): - box = wx.StaticBox(parent, -1, 'Content') - sizer = wx.StaticBoxSizer(box, wx.VERTICAL) - grid = wx.GridBagSizer(8, 8) - - text = wx.StaticText(parent, -1, 'Max. Title Length') - grid.Add(text, (0, 0), flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT) - text = wx.StaticText(parent, -1, 'Max. Body Length') - grid.Add(text, (1, 0), flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT) - text = wx.StaticText(parent, -1, 'characters') - grid.Add(text, (0, 2), flag=wx.ALIGN_CENTER_VERTICAL) - text = wx.StaticText(parent, -1, 'characters') - grid.Add(text, (1, 2), flag=wx.ALIGN_CENTER_VERTICAL) - - title = wx.SpinCtrl(parent, -1, '1', min=1, max=9999, size=(64, -1)) - body = wx.SpinCtrl(parent, -1, '1', min=1, max=9999, size=(64, -1)) - grid.Add(title, (0, 1)) - grid.Add(body, (1, 1)) - - sizer.Add(grid, 1, wx.EXPAND|wx.ALL, 8) - - title.Bind(wx.EVT_SPINCTRL, self.on_change) - body.Bind(wx.EVT_SPINCTRL, self.on_change) - - self.title = title - self.body = body - return sizer - def update_controls(self): - model = self.model - self.width.SetValue(model.POPUP_WIDTH) - self.transparency.SetValue(model.POPUP_TRANSPARENCY) - self.duration.SetValue(model.POPUP_DURATION) - self.auto.SetValue(not model.POPUP_AUTO_PLAY) - self.sound.SetValue(model.PLAY_SOUND) - self.hover.SetValue(model.POPUP_WAIT_ON_HOVER) - self.top.SetValue(model.POPUP_STAY_ON_TOP) - self.title.SetValue(model.POPUP_TITLE_LENGTH) - self.body.SetValue(model.POPUP_BODY_LENGTH) - util.select_choice(self.position, model.POPUP_POSITION) - util.select_choice(self.display, model.POPUP_DISPLAY) - self.border_color.SetBackgroundColour(wx.Colour(*settings.POPUP_BORDER_COLOR)) - self.border_size.SetValue(model.POPUP_BORDER_SIZE) - def update_model(self): - model = self.model - model.POPUP_WIDTH = self.width.GetValue() - model.POPUP_TRANSPARENCY = self.transparency.GetValue() - model.POPUP_DURATION = self.duration.GetValue() - model.POPUP_TITLE_LENGTH = self.title.GetValue() - model.POPUP_BODY_LENGTH = self.body.GetValue() - model.POPUP_AUTO_PLAY = not self.auto.GetValue() - model.POPUP_WAIT_ON_HOVER = self.hover.GetValue() - model.POPUP_STAY_ON_TOP = self.top.GetValue() - model.PLAY_SOUND = self.sound.GetValue() - model.POPUP_POSITION = self.position.GetClientData(self.position.GetSelection()) - model.POPUP_DISPLAY = self.display.GetClientData(self.display.GetSelection()) - model.POPUP_BORDER_SIZE = self.border_size.GetValue() - color = self.border_color.GetBackgroundColour() - model.POPUP_BORDER_COLOR = (color.Red(), color.Green(), color.Blue()) - def on_border_color(self, event): - data = wx.ColourData() - data.SetColour(self.border_color.GetBackgroundColour()) - dialog = wx.ColourDialog(self, data) - if dialog.ShowModal() == wx.ID_OK: - self.border_color.SetBackgroundColour(dialog.GetColourData().GetColour()) - self.on_change(event) - def on_change(self, event): - self.dialog.on_change() - event.Skip() - -class OptionsPanel(wx.Panel): - def __init__(self, parent, dialog): - super(OptionsPanel, self).__init__(parent, -1) - self.dialog = dialog - self.model = dialog.model - panel = self.create_panel(self) - sizer = wx.BoxSizer(wx.VERTICAL) - line = wx.StaticLine(self, -1) - sizer.Add(line, 0, wx.EXPAND) - sizer.Add(panel, 1, wx.EXPAND|wx.ALL, 8) - self.update_controls() - self.SetSizerAndFit(sizer) - def create_panel(self, parent): - panel = wx.Panel(parent, -1) - sizer = wx.BoxSizer(wx.VERTICAL) - general = self.create_general(panel) - caching = self.create_caching(panel) - proxy = self.create_proxy(panel) - sizer.Add(general, 0, wx.EXPAND) - sizer.AddSpacer(8) - sizer.Add(caching, 0, wx.EXPAND) - sizer.AddSpacer(8) - sizer.Add(proxy, 0, wx.EXPAND) - panel.SetSizerAndFit(sizer) - return panel - def create_general(self, parent): - box = wx.StaticBox(parent, -1, 'General') - sizer = wx.StaticBoxSizer(box, wx.VERTICAL) - grid = wx.GridBagSizer(8, 8) - - idle = wx.CheckBox(parent, -1, "Don't check feeds if I've been idle for") - grid.Add(idle, (0, 0), flag=wx.ALIGN_CENTER_VERTICAL) - text = wx.StaticText(parent, -1, 'seconds') - grid.Add(text, (0, 2), flag=wx.ALIGN_CENTER_VERTICAL) - - timeout = wx.SpinCtrl(parent, -1, '1', min=1, max=9999, size=(64, -1)) - grid.Add(timeout, (0, 1)) - - auto_update = wx.CheckBox(parent, -1, 'Check for software updates automatically') - grid.Add(auto_update, (1, 0), flag=wx.ALIGN_CENTER_VERTICAL) - check_now = wx.Button(parent, -1, 'Check Now') - grid.Add(check_now, (1, 1), flag=wx.ALIGN_CENTER_VERTICAL) - - sizer.Add(grid, 1, wx.EXPAND|wx.ALL, 8) - - timeout.Bind(wx.EVT_SPINCTRL, self.on_change) - idle.Bind(wx.EVT_CHECKBOX, self.on_change) - auto_update.Bind(wx.EVT_CHECKBOX, self.on_change) - check_now.Bind(wx.EVT_BUTTON, self.on_check_now) - - self.idle = idle - self.timeout = timeout - self.auto_update = auto_update - self.check_now = check_now - return sizer - def create_caching(self, parent): - box = wx.StaticBox(parent, -1, 'Caching') - sizer = wx.StaticBoxSizer(box, wx.VERTICAL) - grid = wx.GridBagSizer(8, 8) - - text = wx.StaticText(parent, -1, 'Pop-up History') - grid.Add(text, (0, 0), flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT) - #text = wx.StaticText(parent, -1, 'Item Cache') - #grid.Add(text, (1, 0), flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT) - text = wx.StaticText(parent, -1, 'days') - grid.Add(text, (0, 2), flag=wx.ALIGN_CENTER_VERTICAL) - #text = wx.StaticText(parent, -1, 'items per feed') - #grid.Add(text, (1, 2), flag=wx.ALIGN_CENTER_VERTICAL) - - item = wx.SpinCtrl(parent, -1, '1', min=1, max=365, size=(64, -1)) - grid.Add(item, (0, 1)) - #feed = wx.SpinCtrl(parent, -1, '1', min=1, max=9999, size=(64, -1)) - #grid.Add(feed, (1, 1)) - - clear_item = wx.Button(parent, -1, 'Clear') - grid.Add(clear_item, (0, 3)) - #clear_feed = wx.Button(parent, -1, 'Clear') - #grid.Add(clear_feed, (1, 3)) - - sizer.Add(grid, 1, wx.EXPAND|wx.ALL, 8) - - item.Bind(wx.EVT_SPINCTRL, self.on_change) - #feed.Bind(wx.EVT_SPINCTRL, self.on_change) - clear_item.Bind(wx.EVT_BUTTON, self.on_clear_item) - #clear_feed.Bind(wx.EVT_BUTTON, self.on_clear_feed) - - self.item = item - #self.feed = feed - self.clear_item = clear_item - #self.clear_feed = clear_feed - return sizer - def create_proxy(self, parent): - box = wx.StaticBox(parent, -1, 'Proxy') - sizer = wx.StaticBoxSizer(box, wx.VERTICAL) - grid = wx.GridBagSizer(8, 8) - - use_proxy = wx.CheckBox(parent, -1, 'Use a proxy server') - grid.Add(use_proxy, (0, 0), flag=wx.ALIGN_CENTER_VERTICAL) - proxy_url = wx.TextCtrl(parent, -1, style=wx.TE_PASSWORD) - grid.Add(proxy_url, (1, 0), flag=wx.EXPAND) - text = wx.StaticText(parent, -1, 'Format: http://:@:\nLeave blank to use Windows proxy settings.') - grid.Add(text, (2, 0), flag=wx.ALIGN_CENTER_VERTICAL) - - sizer.Add(grid, 1, wx.EXPAND|wx.ALL, 8) - - use_proxy.Bind(wx.EVT_CHECKBOX, self.on_change) - proxy_url.Bind(wx.EVT_TEXT, self.on_change) - - self.use_proxy = use_proxy - self.proxy_url = proxy_url - return sizer - def update_controls(self): - model = self.model - self.idle.SetValue(model.DISABLE_WHEN_IDLE) - self.timeout.SetValue(model.USER_IDLE_TIMEOUT) - self.auto_update.SetValue(model.CHECK_FOR_UPDATES) - one_day = 60 * 60 * 24 - self.item.SetValue(model.ITEM_CACHE_AGE / one_day) - self.use_proxy.SetValue(model.USE_PROXY) - self.proxy_url.ChangeValue(util.decode_password(model.PROXY_URL) or '') - self.enable_controls() - def update_model(self): - model = self.model - model.DISABLE_WHEN_IDLE = self.idle.GetValue() - model.USER_IDLE_TIMEOUT = self.timeout.GetValue() - model.CHECK_FOR_UPDATES = self.auto_update.GetValue() - one_day = 60 * 60 * 24 - model.ITEM_CACHE_AGE = self.item.GetValue() * one_day - model.USE_PROXY = self.use_proxy.GetValue() - model.PROXY_URL = util.encode_password(self.proxy_url.GetValue()) - def enable_controls(self): - self.timeout.Enable(self.idle.GetValue()) - self.proxy_url.Enable(self.use_proxy.GetValue()) - def on_change(self, event): - self.enable_controls() - self.dialog.on_change() - event.Skip() - def on_clear_item(self, event): - self.model.controller.manager.clear_item_history() - self.clear_item.Disable() - def on_clear_feed(self, event): - self.model.controller.manager.clear_feed_cache() - self.clear_feed.Disable() - def on_check_now(self, event): - self.check_now.Disable() - self.model.controller.check_for_updates() - -class AboutPanel(wx.Panel): - def __init__(self, parent): - super(AboutPanel, self).__init__(parent, -1) - panel = self.create_panel(self) - sizer = wx.BoxSizer(wx.VERTICAL) - line = wx.StaticLine(self, -1) - sizer.Add(line, 0, wx.EXPAND) - sizer.Add(panel, 1, wx.EXPAND|wx.ALL, 8) - credits = ''' - %s %s :: Copyright (c) 2009-2013, Michael Fogleman - - 16x16px icons in this application are from the Silk Icon set provided by Mark James under a Creative Commons Attribution 2.5 License. http://www.famfamfam.com/lab/icons/silk/ - - Third-party components of this software include the following: - - * Python 2.6 - http://www.python.org/ - * wxPython 2.8.10 - http://www.wxpython.org/ - * Universal Feed Parser - http://www.feedparser.org/ - * PLY 3.3 - http://www.dabeaz.com/ply/ - * py2exe 0.6.9 - http://www.py2exe.org/ - * Inno Setup - http://www.jrsoftware.org/isinfo.php - - - Universal Feed Parser, a component of this software, requires that the following text be included in the distribution of this application: - - Copyright (c) 2002-2005, Mark Pilgrim - All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - - * 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. - - 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. - - - PLY 3.3 (Python Lex-Yacc), a component of this software, requires that the following text be included in the distribution of this application: - - Copyright (C) 2001-2009, - David M. Beazley (Dabeaz LLC) - All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - - * 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. - - * Neither the name of the David Beazley or Dabeaz LLC 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. - ''' % (settings.APP_NAME, settings.APP_VERSION) - credits = '\n'.join(line.strip() for line in credits.strip().split('\n')) - text = wx.TextCtrl(self, -1, credits, style=wx.TE_MULTILINE|wx.TE_READONLY) - text.SetBackgroundColour(self.GetBackgroundColour()) - sizer.Add(text, 0, wx.EXPAND|wx.ALL&~wx.TOP, 8) - self.SetSizerAndFit(sizer) - def create_panel(self, parent): - panel = wx.Panel(parent, -1, style=wx.BORDER_SUNKEN) - panel.SetBackgroundColour(wx.WHITE) - sizer = wx.BoxSizer(wx.VERTICAL) - bitmap = wx.StaticBitmap(panel, -1, wx.Bitmap('icons/about.png')) - sizer.AddStretchSpacer(1) - sizer.Add(bitmap, 0, wx.ALIGN_CENTER_HORIZONTAL) - sizer.AddStretchSpacer(1) - panel.SetSizerAndFit(sizer) - return panel - \ No newline at end of file +# -*- coding: utf-8 -*- + +"""[summary] + +Returns: + [type] -- [description] +""" + +import wx +import wx.adv + +import feeds +import filters +import util +from settings import settings + +INDEX_ENABLED = 0 +INDEX_URL = 1 +INDEX_TITLE = 2 +INDEX_INTERVAL = 3 +INDEX_ITEM_COUNT = 4 +INDEX_CLICKS = 5 + +INDEX_RULES = 1 +INDEX_FEEDS = 2 +INDEX_IN = 3 +INDEX_OUT = 4 + + +class TaskBarIcon(wx.adv.TaskBarIcon): + """[summary] + + Arguments: + wx {[type]} -- [description] + """ + + def __init__(self, controller): + """[summary] + + Arguments: + controller {[type]} -- [description] + """ + + super(TaskBarIcon, self).__init__() + + self.controller = controller + self.set_icon('icons/feed.png') + self.Bind(wx.adv.EVT_TASKBAR_LEFT_DOWN, self.on_left_down) + + def CreatePopupMenu(self): + """[summary] + + Returns: + [type] -- [description] + """ + + menu = wx.Menu() + util.menu_item(menu, 'Add Feed...', self.on_add_feed, 'icons/add.png') + util.menu_item(menu, 'Preferences...', + self.on_settings, 'icons/cog.png') + menu.AppendSeparator() + if self.controller.enabled: + util.menu_item(menu, 'Disable Updates', + self.on_disable, 'icons/delete.png') + util.menu_item(menu, 'Update Now', + self.on_force_update, 'icons/transmit.png') + else: + util.menu_item(menu, 'Enable Updates', + self.on_enable, 'icons/accept.png') + item = util.menu_item(menu, 'Update Now', + self.on_force_update, 'icons/transmit.png') + item.Enable(False) + menu.AppendSeparator() + util.menu_item(menu, 'Exit', self.on_exit, 'icons/door_out.png') + return menu + + def set_icon(self, path): + """[summary] + + Arguments: + path {[type]} -- [description] + """ + + # icon = wx.IconFromBitmap(wx.Bitmap(path)) # imcompatible? + icon = wx.Icon(path) + self.SetIcon(icon, settings.APP_NAME) + + def on_exit(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.controller.close() + + def on_left_down(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.controller.show_popup() + + def on_force_update(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.controller.force_poll() + + def on_disable(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.controller.disable() + + def on_enable(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.controller.enable() + + def on_add_feed(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.controller.add_feed() + + def on_settings(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.controller.edit_settings() + + +class AddFeedDialog(wx.Dialog): + """[summary] + + Arguments: + wx {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + @staticmethod + def show_wizard(parent, url=''): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Keyword Arguments: + url {str} -- [description] (default: {''}) + + Returns: + [type] -- [description] + """ + + while True: + window = AddFeedDialog(parent, url) + window.Center() + result = window.ShowModal() + data = window.result + window.Destroy() + if result != wx.ID_OK: + return None + url = data.original_url + entries = util.get(data, 'entries', []) + feed = feeds.Feed(url) + feed.title = util.get(data.feed, 'title', '') + feed.link = util.get(data.feed, 'link', '') + feed.username = util.encode_password(data.username) + feed.password = util.encode_password(data.password) + feed.interval = util.guess_polling_interval(entries) + window = EditFeedDialog(parent, feed, True) + window.Center() + result = window.ShowModal() + window.Destroy() + if result == wx.ID_BACKWARD: + continue + if result == wx.ID_OK: + return feed + return None + + def __init__(self, parent, initial_url=''): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Keyword Arguments: + initial_url {str} -- [description] (default: {''}) + """ + + super(AddFeedDialog, self).__init__(parent, -1, 'Add RSS/Atom Feed') + util.set_icon(self) + # self.SetIcon(wx.IconFromBitmap(wx.Bitmap('icons/feed.png'))) + self.initial_url = initial_url + self.result = None + panel = self.create_panel(self) + self.Fit() + self.validate() + + def get_initial_url(self): + """[summary] + + Returns: + [type] -- [description] + """ + + if self.initial_url: + return self.initial_url + + if wx.TheClipboard.Open(): + object = wx.TextDataObject() + success = wx.TheClipboard.GetData(object) + wx.TheClipboard.Close() + + if success: + url = object.GetText() + + if url.startswith('http'): + return url + + return '' + + def create_panel(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + panel = wx.Panel(parent, -1) + sizer = wx.BoxSizer(wx.VERTICAL) + controls = self.create_controls(panel) + buttons = self.create_buttons(panel) + line = wx.StaticLine(panel, -1) + sizer.AddStretchSpacer(1) + sizer.Add(controls, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 25) + sizer.AddStretchSpacer(1) + sizer.Add(line, 0, wx.EXPAND) + sizer.Add(buttons, 0, wx.EXPAND | wx.ALL, 8) + panel.SetSizerAndFit(sizer) + return panel + + def create_controls(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + sizer = wx.GridBagSizer(8, 8) + label = wx.StaticText(parent, -1, 'Feed URL') + font = label.GetFont() + font.SetWeight(wx.FONTWEIGHT_BOLD) + label.SetFont(font) + value = self.get_initial_url() + value = value.replace('feed:https://', 'https://') + value = value.replace('feed://', 'http://') + url = wx.TextCtrl(parent, -1, value, size=(300, -1)) + url.Bind(wx.EVT_TEXT, self.on_text) + status = wx.StaticText(parent, -1, '') + sizer.Add(label, (0, 0), flag=wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + sizer.Add(url, (0, 1)) + sizer.Add(status, (1, 1)) + self.url = url + self.status = status + return sizer + + def create_buttons(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + sizer = wx.BoxSizer(wx.HORIZONTAL) + back = wx.Button(parent, wx.ID_BACKWARD, '< Back') + next = wx.Button(parent, wx.ID_FORWARD, 'Next >') + cancel = wx.Button(parent, wx.ID_CANCEL, 'Cancel') + back.Disable() + next.SetDefault() + next.Bind(wx.EVT_BUTTON, self.on_next) + self.next = next + sizer.AddStretchSpacer(1) + sizer.Add(back) + sizer.AddSpacer(4) + sizer.Add(next) + sizer.AddSpacer(16) + sizer.Add(cancel) + return sizer + + def validate(self): + """[summary] + """ + + if self.url.GetValue(): + self.next.Enable() + else: + self.next.Disable() + + def on_text(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.validate() + + def on_next(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + url = self.url.GetValue() + self.lock() + util.start_thread(self.check_feed, url) + + def on_valid(self, result): + """[summary] + + Arguments: + result {[type]} -- [description] + """ + + self.result = result + self.EndModal(wx.ID_OK) + + def on_invalid(self): + """[summary] + """ + + dialog = wx.MessageDialog( + self, 'The URL entered does not appear to be a valid RSS/Atom feed.', 'Invalid Feed', wx.OK | wx.ICON_ERROR) + dialog.Center() + dialog.ShowModal() + dialog.Destroy() + self.unlock() + + def on_password(self, url, username, password): + """[summary] + + Arguments: + url {[type]} -- [description] + username {[type]} -- [description] + password {[type]} -- [description] + """ + + dialog = PasswordDialog(self, username, password) + dialog.Center() + result = dialog.ShowModal() + username = dialog.username.GetValue() + password = dialog.password.GetValue() + dialog.Destroy() + + if result == wx.ID_OK: + util.start_thread(self.check_feed, url, username, password) + else: + self.unlock() + + def lock(self): + """[summary] + """ + + self.url.Disable() + self.next.Disable() + self.status.SetLabel('Checking feed, please wait...') + + def unlock(self): + """[summary] + """ + + self.url.Enable() + self.next.Enable() + self.status.SetLabel('') + self.url.SelectAll() + self.url.SetFocus() + + def check_feed(self, url, username=None, password=None): + """[summary] + + Arguments: + url {[type]} -- [description] + + Keyword Arguments: + username {[type]} -- [description] (default: {None}) + password {[type]} -- [description] (default: {None}) + """ + + d = util.parse(url, username, password) + if not self: # cancelled + return + status = util.get(d, 'status', 0) + if status == 401: # auth required + wx.CallAfter(self.on_password, url, username, password) + elif util.is_valid_feed(d): + d['original_url'] = url + d['username'] = username + d['password'] = password + wx.CallAfter(self.on_valid, d) + else: + wx.CallAfter(self.on_invalid) + + +class PasswordDialog(wx.Dialog): + """[summary] + + Arguments: + wx {[type]} -- [description] + """ + + def __init__(self, parent, username=None, password=None): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Keyword Arguments: + username {[type]} -- [description] (default: {None}) + password {[type]} -- [description] (default: {None}) + """ + + super(PasswordDialog, self).__init__(parent, -1, 'Password Required') + util.set_icon(self) + panel = self.create_panel(self) + + if username: + self.username.SetValue(username) + if password: + self.password.SetValue(password) + + self.Fit() + self.validate() + + def create_panel(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + panel = wx.Panel(parent, -1) + sizer = wx.BoxSizer(wx.VERTICAL) + controls = self.create_controls(panel) + buttons = self.create_buttons(panel) + sizer.AddStretchSpacer(1) + sizer.Add(controls, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 12) + sizer.AddStretchSpacer(1) + sizer.Add(buttons, 0, wx.EXPAND | wx.ALL & ~wx.TOP, 12) + panel.SetSizerAndFit(sizer) + return panel + + def create_controls(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + sizer = wx.GridBagSizer(8, 8) + label = wx.StaticText(parent, -1, 'Username') + username = wx.TextCtrl(parent, -1, '', size=(180, -1)) + username.Bind(wx.EVT_TEXT, self.on_text) + sizer.Add(label, (0, 0), flag=wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + sizer.Add(username, (0, 1)) + self.username = username + label = wx.StaticText(parent, -1, 'Password') + password = wx.TextCtrl( + parent, -1, '', size=(180, -1), style=wx.TE_PASSWORD) + password.Bind(wx.EVT_TEXT, self.on_text) + sizer.Add(label, (1, 0), flag=wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + sizer.Add(password, (1, 1)) + self.password = password + return sizer + + def create_buttons(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + ok = wx.Button(parent, wx.ID_OK, 'OK') + cancel = wx.Button(parent, wx.ID_CANCEL, 'Cancel') + ok.SetDefault() + ok.Disable() + self.ok = ok + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.AddStretchSpacer(1) + sizer.Add(ok) + sizer.AddSpacer(8) + sizer.Add(cancel) + return sizer + + def validate(self): + """[summary] + """ + + if self.username.GetValue() and self.password.GetValue(): + self.ok.Enable() + else: + self.ok.Disable() + + def on_text(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.validate() + + +class EditFeedDialog(wx.Dialog): + """[summary] + + Arguments: + wx {[type]} -- [description] + """ + + def __init__(self, parent, feed, add=False): + """[summary] + + Arguments: + parent {[type]} -- [description] + feed {[type]} -- [description] + + Keyword Arguments: + add {bool} -- [description] (default: {False}) + """ + + title = 'Add RSS/Atom Feed' if add else 'Edit RSS/Atom Feed' + super(EditFeedDialog, self).__init__(parent, -1, title) + util.set_icon(self) + # self.SetIcon(wx.IconFromBitmap(wx.Bitmap('icons/feed.png'))) + self.feed = feed + self.add = add + panel = self.create_panel(self) + self.Fit() + self.validate() + + def create_panel(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + panel = wx.Panel(parent, -1) + sizer = wx.BoxSizer(wx.VERTICAL) + controls = self.create_controls(panel) + if self.add: + buttons = self.create_add_buttons(panel) + else: + buttons = self.create_edit_buttons(panel) + line = wx.StaticLine(panel, -1) + sizer.AddStretchSpacer(1) + sizer.Add(controls, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 25) + sizer.AddStretchSpacer(1) + sizer.Add(line, 0, wx.EXPAND) + sizer.Add(buttons, 0, wx.EXPAND | wx.ALL, 8) + panel.SetSizerAndFit(sizer) + return panel + + def create_controls(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + sizer = wx.GridBagSizer(8, 8) + indexes = [0, 1, 3, 5, 7] + labels = ['Feed URL', 'Feed Title', 'Feed Link', + 'Polling Interval', 'Border Color'] + for index, text in zip(indexes, labels): + label = wx.StaticText(parent, -1, text) + font = label.GetFont() + font.SetWeight(wx.FONTWEIGHT_BOLD) + label.SetFont(font) + sizer.Add(label, (index, 0), + flag=wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + controls = [] + for index in indexes[:-2]: + style = wx.TE_READONLY if index == 0 else 0 + control = wx.TextCtrl(parent, -1, '', size=(300, -1), style=style) + control.Bind(wx.EVT_TEXT, self.on_text) + sizer.Add(control, (index, 1), (1, 2)) + controls.append(control) + url, title, link = controls + self.url, self.title, self.link = controls + url.ChangeValue(self.feed.url) + title.ChangeValue(self.feed.title) + link.ChangeValue(self.feed.link) + url.SetBackgroundColour(parent.GetBackgroundColour()) + _interval, _units = util.split_time(self.feed.interval) + interval = wx.SpinCtrl(parent, -1, str(_interval), + min=1, max=60, size=(64, -1)) + units = wx.Choice(parent, -1) + units.Append('second(s)', 1) + units.Append('minute(s)', 60) + units.Append('hour(s)', 60*60) + units.Append('day(s)', 60*60*24) + units.Select(_units) + self.interval, self.units = interval, units + sizer.Add(interval, (5, 1)) + sizer.Add(units, (5, 2)) + self.color = color = wx.Button(parent, -1) + color.Bind(wx.EVT_BUTTON, self.on_color) + color._color = self.feed.color + _color = self.feed.color or settings.POPUP_BORDER_COLOR + color.SetBackgroundColour(wx.Colour(*_color)) + sizer.Add(color, (7, 1)) + self.default = default = wx.Button(parent, -1, 'Use Default') + default.Bind(wx.EVT_BUTTON, self.on_default) + sizer.Add(default, (7, 2)) + label = wx.StaticText( + parent, -1, 'The feed title will be shown in the pop-up window for items from this feed.') + label.Wrap(300) + sizer.Add(label, (2, 1), (1, 2), flag=wx.ALIGN_CENTER_VERTICAL) + label = wx.StaticText( + parent, -1, 'The feed link will launch in your browser if you click on the feed title in a pop-up window.') + label.Wrap(300) + sizer.Add(label, (4, 1), (1, 2), flag=wx.ALIGN_CENTER_VERTICAL) + label = wx.StaticText( + parent, -1, 'The polling interval specifies how often the application will check the feed for new items. When adding a new feed, the application automatically fills this in by examining the items in the feed.') + label.Wrap(300) + sizer.Add(label, (6, 1), (1, 2), flag=wx.ALIGN_CENTER_VERTICAL) + label = wx.StaticText( + parent, -1, 'The color specifies the border color of pop-up windows for this feed, if you want to override the default.') + label.Wrap(300) + sizer.Add(label, (8, 1), (1, 2), flag=wx.ALIGN_CENTER_VERTICAL) + return sizer + + def create_add_buttons(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + sizer = wx.BoxSizer(wx.HORIZONTAL) + back = wx.Button(parent, wx.ID_BACKWARD, '< Back') + next = wx.Button(parent, wx.ID_FORWARD, 'Finish') + cancel = wx.Button(parent, wx.ID_CANCEL, 'Cancel') + next.SetDefault() + next.Bind(wx.EVT_BUTTON, self.on_next) + back.Bind(wx.EVT_BUTTON, self.on_back) + self.next = next + sizer.AddStretchSpacer(1) + sizer.Add(back) + sizer.AddSpacer(4) + sizer.Add(next) + sizer.AddSpacer(16) + sizer.Add(cancel) + return sizer + + def create_edit_buttons(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + sizer = wx.BoxSizer(wx.HORIZONTAL) + next = wx.Button(parent, wx.ID_FORWARD, 'OK') + cancel = wx.Button(parent, wx.ID_CANCEL, 'Cancel') + next.SetDefault() + next.Bind(wx.EVT_BUTTON, self.on_next) + self.next = next + sizer.AddStretchSpacer(1) + sizer.Add(next) + sizer.AddSpacer(8) + sizer.Add(cancel) + return sizer + + def validate(self): + """[summary] + """ + + controls = [self.url, self.title, self.link] + if all(control.GetValue() for control in controls): + self.next.Enable() + else: + self.next.Disable() + + def on_color(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + data = wx.ColourData() + data.SetColour(self.color.GetBackgroundColour()) + dialog = wx.ColourDialog(self, data) + if dialog.ShowModal() == wx.ID_OK: + color = dialog.GetColourData().GetColour() + self.color.SetBackgroundColour(color) + self.color._color = (color.Red(), color.Green(), color.Blue()) + + def on_default(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.color.SetBackgroundColour(wx.Colour(*settings.POPUP_BORDER_COLOR)) + self.color._color = None + + def on_text(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.validate() + + def on_back(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.EndModal(wx.ID_BACKWARD) + + def on_next(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + url = self.url.GetValue() + title = self.title.GetValue() + link = self.link.GetValue() + interval = int(self.interval.GetValue()) + multiplier = self.units.GetClientData(self.units.GetSelection()) + interval = interval * multiplier + if interval < 60: + dialog = wx.MessageDialog(self, 'Are you sure you want to check this feed every %d second(s)?\n\nYou might make the website administrator unhappy!' % + interval, 'Confirm Polling Interval', wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION) + result = dialog.ShowModal() + dialog.Destroy() + if result == wx.ID_NO: + return + self.feed.title = title + self.feed.link = link + self.feed.interval = interval + self.feed.color = self.color._color + self.EndModal(wx.ID_OK) + + +class EditFilterDialog(wx.Dialog): + """[summary] + + Arguments: + wx {[type]} -- [description] + """ + + def __init__(self, parent, model, filter=None): + """[summary] + + Arguments: + parent {[type]} -- [description] + model {[type]} -- [description] + + Keyword Arguments: + filter {[type]} -- [description] (default: {None}) + """ + + title = 'Edit Filter' if filter else 'Add Filter' + style = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER + super(EditFilterDialog, self).__init__(parent, -1, title, style=style) + util.set_icon(self) + self.model = model + self.filter = filter or feeds.Filter('') + panel = self.create_panel(self) + buttons = self.create_buttons(self) + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(panel, 1, wx.EXPAND | wx.ALL, 8) + sizer.Add(buttons, 0, wx.EXPAND | wx.ALL & ~wx.TOP, 8) + self.SetSizerAndFit(sizer) + self.validate() + + def create_panel(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + panel = wx.Panel(parent, -1) + rules = self.create_rules(panel) + options = self.create_options(panel) + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(rules, 1, wx.EXPAND) + sizer.AddSpacer(8) + sizer.Add(options, 0, wx.EXPAND) + panel.SetSizer(sizer) + return panel + + def create_buttons(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + ok = wx.Button(parent, wx.ID_OK, 'OK') + cancel = wx.Button(parent, wx.ID_CANCEL, 'Cancel') + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.AddStretchSpacer(1) + sizer.Add(ok) + sizer.AddSpacer(8) + sizer.Add(cancel) + ok.SetDefault() + ok.Bind(wx.EVT_BUTTON, self.on_ok) + self.ok = ok + return sizer + + def create_rules(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + box = wx.StaticBox(parent, -1, 'Filter Rules') + box = wx.StaticBoxSizer(box, wx.VERTICAL) + code = wx.TextCtrl(parent, -1, self.filter.code, + style=wx.TE_MULTILINE, size=(250, -1)) + text = ''' + Examples: + -microsoft and -apple (exclude microsoft and apple) + google or yahoo (require google or yahoo) + -author:BoringGuy (search author field only) + ''' + text = '\n'.join(line.strip() for line in text.strip().split('\n')) + help = wx.StaticText(parent, -1, text) + box.Add(code, 1, wx.EXPAND | wx.ALL, 8) + box.Add(help, 0, wx.EXPAND | wx.ALL & ~wx.TOP, 8) + code.Bind(wx.EVT_TEXT, self.on_event) + self.code = code + return box + + def create_options(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + sizer = wx.BoxSizer(wx.VERTICAL) + box = wx.StaticBox(parent, -1, 'Options') + box = wx.StaticBoxSizer(box, wx.VERTICAL) + match_case = wx.CheckBox(parent, -1, 'Match Case') + match_whole_words = wx.CheckBox(parent, -1, 'Match Whole Words') + match_case.SetValue(not self.filter.ignore_case) + match_whole_words.SetValue(self.filter.whole_word) + box.Add(match_case, 0, wx.ALL, 8) + box.Add(match_whole_words, 0, wx.ALL & ~wx.TOP, 8) + sizer.Add(box, 0, wx.EXPAND) + sizer.AddSpacer(8) + box = wx.StaticBox(parent, -1, 'Apply Filter To') + box = wx.StaticBoxSizer(box, wx.VERTICAL) + all_feeds = wx.RadioButton(parent, -1, 'All Feeds', style=wx.RB_GROUP) + selected_feeds = wx.RadioButton(parent, -1, 'Selected Feeds') + if self.filter.feeds: + selected_feeds.SetValue(True) + feeds = wx.CheckListBox( + parent, -1, size=(150, 150), style=wx.LB_HSCROLL | wx.LB_EXTENDED) + + def cmp_title(a, b): + """[summary] + + Arguments: + a {[type]} -- [description] + b {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + return cmp(a.title.lower(), b.title.lower()) + + self.lookup = {} + items = self.model.controller.manager.feeds + for index, feed in enumerate(sorted(items, cmp=cmp_title)): + feeds.Append(feed.title) + self.lookup[index] = feed + feeds.Check(index, feed in self.filter.feeds) + box.Add(all_feeds, 0, wx.ALL, 8) + box.Add(selected_feeds, 0, wx.ALL & ~wx.TOP, 8) + box.Add(feeds, 1, wx.ALL & ~wx.TOP, 8) + sizer.Add(box, 1, wx.EXPAND) + match_case.Bind(wx.EVT_CHECKBOX, self.on_event) + match_whole_words.Bind(wx.EVT_CHECKBOX, self.on_event) + all_feeds.Bind(wx.EVT_RADIOBUTTON, self.on_event) + selected_feeds.Bind(wx.EVT_RADIOBUTTON, self.on_event) + feeds.Bind(wx.EVT_CHECKLISTBOX, self.on_event) + self.match_case = match_case + self.match_whole_words = match_whole_words + self.all_feeds = all_feeds + self.selected_feeds = selected_feeds + self.feeds = feeds + return sizer + + def get_selected_feeds(self): + """[summary] + + Returns: + [type] -- [description] + """ + + result = set() + + if self.selected_feeds.GetValue(): + for index in range(self.feeds.GetCount()): + if self.feeds.IsChecked(index): + result.add(self.lookup[index]) + return result + + def validate(self): + """[summary] + """ + + feeds = self.get_selected_feeds() + valid = True + valid = valid and self.code.GetValue() + valid = valid and (self.all_feeds.GetValue() or feeds) + try: + filters.parse(self.code.GetValue()) + except Exception: + valid = False + self.ok.Enable(bool(valid)) + self.feeds.Enable(self.selected_feeds.GetValue()) + + def on_event(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.validate() + + def on_ok(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + filter = self.filter + filter.code = self.code.GetValue() + filter.ignore_case = not self.match_case.GetValue() + filter.whole_word = self.match_whole_words.GetValue() + filter.feeds = self.get_selected_feeds() + event.Skip() + + +class Model(object): + """[summary] + + Arguments: + object {[type]} -- [description] + """ + + def __init__(self, controller): + """[summary] + + Arguments: + controller {[type]} -- [description] + """ + + self.controller = controller + self.reset() + + def reset(self): + """[summary] + """ + + self._feed_sort = -1 + self._filter_sort = -1 + feeds = self.controller.manager.feeds + feeds = [feed.make_copy() for feed in feeds] + self.feeds = feeds + filters = self.controller.manager.filters + filters = [filter.make_copy() for filter in filters] + self.filters = filters + self.settings = {} + + def __getattr__(self, key): + """[summary] + + Arguments: + key {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + if key != key.upper(): + return super(Model, self).__getattr__(key) + if key in self.settings: + return self.settings[key] + return getattr(settings, key) + + def __setattr__(self, key, value): + """[summary] + + Arguments: + key {[type]} -- [description] + value {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + if key != key.upper(): + return super(Model, self).__setattr__(key, value) + self.settings[key] = value + + def apply(self): + """[summary] + """ + + self.apply_filters() + self.apply_feeds() + self.apply_settings() + self.controller.save() + + def apply_settings(self): + """[summary] + """ + + for key, value in list(self.settings.items()): + setattr(settings, key, value) + + def apply_feeds(self): + """[summary] + """ + + before = {} + after = {} + controller = self.controller + for feed in controller.manager.feeds: + before[feed.uuid] = feed + for feed in self.feeds: + after[feed.uuid] = feed + before_set = set(before.keys()) + after_set = set(after.keys()) + added = after_set - before_set + deleted = before_set - after_set + same = after_set & before_set + for uuid in added: + feed = after[uuid] + controller.manager.add_feed(feed) + for uuid in deleted: + feed = before[uuid] + controller.manager.remove_feed(feed) + for uuid in same: + a = before[uuid] + b = after[uuid] + a.copy_from(b) + + def apply_filters(self): + """[summary] + """ + + before = {} + after = {} + controller = self.controller + for filter in controller.manager.filters: + before[filter.uuid] = filter + for filter in self.filters: + after[filter.uuid] = filter + before_set = set(before.keys()) + after_set = set(after.keys()) + added = after_set - before_set + deleted = before_set - after_set + same = after_set & before_set + for uuid in added: + filter = after[uuid] + controller.manager.add_filter(filter) + for uuid in deleted: + filter = before[uuid] + controller.manager.remove_filter(filter) + for uuid in same: + a = before[uuid] + b = after[uuid] + a.copy_from(b) + + def sort_feeds(self, column): + """[summary] + + Arguments: + column {[type]} -- [description] + """ + + def cmp_enabled(a, b): + """[summary] + + Arguments: + a {[type]} -- [description] + b {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + return cmp(a.enabled, b.enabled) + + def cmp_clicks(a, b): + """[summary] + + Arguments: + a {[type]} -- [description] + b {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + return cmp(b.clicks, a.clicks) + + def cmp_item_count(a, b): + """[summary] + + Arguments: + a {[type]} -- [description] + b {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + return cmp(b.item_count, a.item_count) + + def cmp_interval(a, b): + """[summary] + + Arguments: + a {[type]} -- [description] + b {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + return cmp(a.interval, b.interval) + + def cmp_title(a, b): + """[summary] + + Arguments: + a {[type]} -- [description] + b {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + return cmp(a.title.lower(), b.title.lower()) + + def cmp_url(a, b): + """[summary] + + Arguments: + a {[type]} -- [description] + b {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + return cmp(a.url.lower(), b.url.lower()) + funcs = { + INDEX_ENABLED: cmp_enabled, + INDEX_URL: cmp_url, + INDEX_TITLE: cmp_title, + INDEX_INTERVAL: cmp_interval, + INDEX_CLICKS: cmp_clicks, + INDEX_ITEM_COUNT: cmp_item_count, + } + self.feeds.sort(cmp=funcs[column]) + if column == self._feed_sort: + self.feeds.reverse() + self._feed_sort = -1 + else: + self._feed_sort = column + + def sort_filters(self, column): + """[summary] + + Arguments: + column {[type]} -- [description] + """ + + def cmp_enabled(a, b): + """[summary] + + Arguments: + a {[type]} -- [description] + b {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + return cmp(a.enabled, b.enabled) + + def cmp_rules(a, b): + """[summary] + + Arguments: + a {[type]} -- [description] + b {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + return cmp(a.code, b.code) + + def cmp_feeds(a, b): + """[summary] + + Arguments: + a {[type]} -- [description] + b {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + return cmp(len(a.feeds), len(b.feeds)) + + def cmp_in(a, b): + """[summary] + + Arguments: + a {[type]} -- [description] + b {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + return cmp(b.inputs, a.inputs) + + def cmp_out(a, b): + """[summary] + + Arguments: + a {[type]} -- [description] + b {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + return cmp(b.outputs, a.outputs) + funcs = { + INDEX_ENABLED: cmp_enabled, + INDEX_RULES: cmp_rules, + INDEX_FEEDS: cmp_feeds, + INDEX_IN: cmp_in, + INDEX_OUT: cmp_out, + } + self.filters.sort(cmp=funcs[column]) + if column == self._filter_sort: + self.filters.reverse() + self._filter_sort = -1 + else: + self._filter_sort = column + + +class SettingsDialog(wx.Dialog): + """[summary] + + Arguments: + wx {[type]} -- [description] + """ + + def __init__(self, parent, controller): + """[summary] + + Arguments: + parent {[type]} -- [description] + controller {[type]} -- [description] + """ + + title = '%s Preferences' % settings.APP_NAME + style = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER + super(SettingsDialog, self).__init__(parent, -1, title, style=style) + util.set_icon(self) + # self.SetIcon(wx.IconFromBitmap(wx.Bitmap('icons/feed.png'))) + self.model = Model(controller) + panel = self.create_panel(self) + self.Fit() + self.SetMinSize(self.GetSize()) + + def create_panel(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + panel = wx.Panel(parent, -1) + notebook = self.create_notebook(panel) + line = wx.StaticLine(panel, -1) + buttons = self.create_buttons(panel) + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(notebook, 1, wx.EXPAND | wx.ALL, 0) + sizer.Add(line, 0, wx.EXPAND) + sizer.Add(buttons, 0, wx.EXPAND | wx.ALL, 8) + panel.SetSizerAndFit(sizer) + return panel + + def create_notebook(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + images = wx.ImageList(48, 32) + images.Add(util.scale_bitmap( + wx.Bitmap('icons/feed32.png'), -1, -1, self.GetBackgroundColour())) + images.Add(util.scale_bitmap( + wx.Bitmap('icons/comment32.png'), -1, -1, self.GetBackgroundColour())) + images.Add(util.scale_bitmap( + wx.Bitmap('icons/cog32.png'), -1, -1, self.GetBackgroundColour())) + images.Add(util.scale_bitmap( + wx.Bitmap('icons/filter32.png'), -1, -1, self.GetBackgroundColour())) + images.Add(util.scale_bitmap( + wx.Bitmap('icons/info32.png'), -1, -1, self.GetBackgroundColour())) + + notebook = wx.Toolbook(parent, -1) + # notebook.SetInternalBorder(0) # FIXME: ¿? + notebook.AssignImageList(images) + feeds = FeedsPanel(notebook, self) + popups = PopupsPanel(notebook, self) + options = OptionsPanel(notebook, self) + filters = FiltersPanel(notebook, self) + about = AboutPanel(notebook) + notebook.AddPage(feeds, 'Feeds', imageId=0) + notebook.AddPage(popups, 'Pop-ups', imageId=1) + notebook.AddPage(options, 'Options', imageId=2) + notebook.AddPage(filters, 'Filters', imageId=3) + notebook.AddPage(about, 'About', imageId=4) + self.popups = popups + self.options = options + notebook.Fit() + return notebook + + def create_buttons(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + sizer = wx.BoxSizer(wx.HORIZONTAL) + ok = wx.Button(parent, wx.ID_OK, 'OK') + cancel = wx.Button(parent, wx.ID_CANCEL, 'Cancel') + apply = wx.Button(parent, wx.ID_APPLY, 'Apply') + ok.Bind(wx.EVT_BUTTON, self.on_ok) + apply.Bind(wx.EVT_BUTTON, self.on_apply) + ok.SetDefault() + apply.Disable() + self.apply_button = apply + sizer.AddStretchSpacer(1) + sizer.Add(ok) + sizer.AddSpacer(8) + sizer.Add(cancel) + sizer.AddSpacer(8) + sizer.Add(apply) + return sizer + + def apply(self): + """[summary] + """ + + self.popups.update_model() + self.options.update_model() + self.model.apply() + self.model.controller.poll() + + def on_change(self): + """[summary] + """ + + self.apply_button.Enable() + + def on_ok(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.apply() + event.Skip() + + def on_apply(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.apply() + self.apply_button.Disable() + + +class FeedsList(wx.ListCtrl): + """[summary] + + Arguments: + wx {[type]} -- [description] + """ + + def __init__(self, parent, dialog): + """[summary] + + Arguments: + parent {[type]} -- [description] + dialog {[type]} -- [description] + """ + + style = wx.LC_REPORT | wx.LC_VIRTUAL # |wx.LC_HRULES|wx.LC_VRULES + super(FeedsList, self).__init__(parent, -1, style=style) + self.dialog = dialog + self.model = dialog.model + images = wx.ImageList(16, 16, True) + # images.AddWithColourMask(wx.Bitmap('icons/unchecked.png'), wx.WHITE) # FIXME: delete + images.Add(wx.Bitmap('icons/unchecked.png'), wx.WHITE) + # images.AddWithColourMask(wx.Bitmap('icons/checked.png'), wx.WHITE) # FIXME: delete + images.Add(wx.Bitmap('icons/checked.png'), wx.WHITE) + self.AssignImageList(images, wx.IMAGE_LIST_SMALL) + self.InsertColumn(INDEX_ENABLED, 'On') + self.InsertColumn(INDEX_URL, 'Feed URL') + self.InsertColumn(INDEX_TITLE, 'Feed Title') + self.InsertColumn(INDEX_INTERVAL, 'Interval') + self.InsertColumn(INDEX_ITEM_COUNT, 'Items') + self.InsertColumn(INDEX_CLICKS, 'Clicks') + self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down) + self.Bind(wx.EVT_LIST_COL_CLICK, self.on_col_click) + self.update() + self.SetColumnWidth(INDEX_ENABLED, 32) + self.SetColumnWidth(INDEX_URL, 165) + self.SetColumnWidth(INDEX_TITLE, 165) + self.SetColumnWidth(INDEX_INTERVAL, 75) + self.SetColumnWidth(INDEX_ITEM_COUNT, -2) + self.SetColumnWidth(INDEX_CLICKS, -2) + + def update(self): + """[summary] + """ + + self.SetItemCount(len(self.model.feeds)) + self.Refresh() + + def on_col_click(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + column = event.GetColumn() + self.model.sort_feeds(column) + self.update() + + def on_left_down(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + index, flags = self.HitTest(event.GetPosition()) + if index >= 0 and (flags & wx.LIST_HITTEST_ONITEMICON): + self.toggle(index) + event.Skip() + + def toggle(self, index): + """[summary] + + Arguments: + index {[type]} -- [description] + """ + + feed = self.model.feeds[index] + feed.enabled = not feed.enabled + self.RefreshItem(index) + self.dialog.on_change() + + def OnGetItemImage(self, index): + """[summary] + + Arguments: + index {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + feed = self.model.feeds[index] + return 1 if feed.enabled else 0 + + def OnGetItemText(self, index, column): + """[summary] + + Arguments: + index {[type]} -- [description] + column {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + feed = self.model.feeds[index] + if column == INDEX_URL: + return feed.url + if column == INDEX_TITLE: + return feed.title + if column == INDEX_INTERVAL: + return util.split_time_str(feed.interval) + if column == INDEX_CLICKS: + return str(feed.clicks) if feed.clicks else '' + if column == INDEX_ITEM_COUNT: + return str(feed.item_count) if feed.item_count else '' + return '' + + +class FiltersList(wx.ListCtrl): + """[summary] + + Arguments: + wx {[type]} -- [description] + """ + + def __init__(self, parent, dialog): + """[summary] + + Arguments: + parent {[type]} -- [description] + dialog {[type]} -- [description] + """ + + style = wx.LC_REPORT | wx.LC_VIRTUAL # |wx.LC_HRULES|wx.LC_VRULES + super(FiltersList, self).__init__(parent, -1, style=style) + self.dialog = dialog + self.model = dialog.model + images = wx.ImageList(16, 16, True) + # images.AddWithColourMask(wx.Bitmap('icons/unchecked.png'), wx.WHITE) # FIXME: deprecated + images.Add(wx.Bitmap('icons/unchecked.png'), wx.WHITE) + # images.AddWithColourMask(wx.Bitmap('icons/checked.png'), wx.WHITE) # FIXME: deprecated + images.Add(wx.Bitmap('icons/checked.png'), wx.WHITE) + self.AssignImageList(images, wx.IMAGE_LIST_SMALL) + self.InsertColumn(INDEX_ENABLED, 'On') + self.InsertColumn(INDEX_RULES, 'Filter Rules') + self.InsertColumn(INDEX_FEEDS, 'Feeds') + self.InsertColumn(INDEX_IN, 'In') + self.InsertColumn(INDEX_OUT, 'Out') + self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down) + self.Bind(wx.EVT_LIST_COL_CLICK, self.on_col_click) + self.update() + self.SetColumnWidth(INDEX_ENABLED, 32) + self.SetColumnWidth(INDEX_RULES, 200) + self.SetColumnWidth(INDEX_FEEDS, 64) + self.SetColumnWidth(INDEX_IN, 64) + self.SetColumnWidth(INDEX_OUT, 64) + + def update(self): + """[summary] + """ + + self.SetItemCount(len(self.model.filters)) + self.Refresh() + + def on_col_click(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + column = event.GetColumn() + self.model.sort_filters(column) + self.update() + + def on_left_down(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + index, flags = self.HitTest(event.GetPosition()) + if index >= 0 and (flags & wx.LIST_HITTEST_ONITEMICON): + self.toggle(index) + event.Skip() + + def toggle(self, index): + """[summary] + + Arguments: + index {[type]} -- [description] + """ + + filter = self.model.filters[index] + filter.enabled = not filter.enabled + self.RefreshItem(index) + self.dialog.on_change() + + def OnGetItemImage(self, index): + """[summary] + + Arguments: + index {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + filter = self.model.filters[index] + return 1 if filter.enabled else 0 + + def OnGetItemText(self, index, column): + """[summary] + + Arguments: + index {[type]} -- [description] + column {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + filter = self.model.filters[index] + if column == INDEX_RULES: + return filter.code.replace('\n', ' ') + if column == INDEX_FEEDS: + return str(len(filter.feeds)) if filter.feeds else 'All' + if column == INDEX_IN: + return str(filter.inputs) + if column == INDEX_OUT: + return str(filter.outputs) + return '' + + +class FeedsPanel(wx.Panel): + """[summary] + + Arguments: + wx {[type]} -- [description] + """ + + def __init__(self, parent, dialog): + """[summary] + + Arguments: + parent {[type]} -- [description] + dialog {[type]} -- [description] + """ + + super(FeedsPanel, self).__init__(parent, -1) + self.dialog = dialog + self.model = dialog.model + panel = self.create_panel(self) + sizer = wx.BoxSizer(wx.VERTICAL) + line = wx.StaticLine(self, -1) + sizer.Add(line, 0, wx.EXPAND) + sizer.Add(panel, 1, wx.EXPAND | wx.ALL, 8) + self.SetSizerAndFit(sizer) + + def create_panel(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + panel = wx.Panel(parent, -1) + list = FeedsList(panel, self.dialog) + list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_selection) + list.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.on_selection) + list.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.on_edit) + list.Bind(wx.EVT_LEFT_DOWN, self.on_left_down) + self.list = list + buttons = self.create_buttons(panel) + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(list, 1, wx.EXPAND) + sizer.AddSpacer(8) + sizer.Add(buttons, 0, wx.EXPAND) + panel.SetSizerAndFit(sizer) + return panel + + def create_buttons(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + new = wx.Button(parent, -1, 'Add...') + #import_feeds = wx.Button(parent, -1, 'Import...') + edit = wx.Button(parent, -1, 'Edit...') + delete = wx.Button(parent, -1, 'Delete') + new.Bind(wx.EVT_BUTTON, self.on_new) + edit.Bind(wx.EVT_BUTTON, self.on_edit) + delete.Bind(wx.EVT_BUTTON, self.on_delete) + edit.Disable() + delete.Disable() + self.edit = edit + self.delete = delete + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(new) + sizer.AddSpacer(8) + # sizer.Add(import_feeds) + # sizer.AddSpacer(8) + sizer.Add(edit) + sizer.AddSpacer(8) + sizer.Add(delete) + sizer.AddStretchSpacer(1) + return sizer + + def update(self): + """[summary] + """ + + self.list.update() + self.update_buttons() + self.dialog.on_change() + + def on_selection(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + event.Skip() + self.update_buttons() + + def update_buttons(self): + """[summary] + """ + + count = self.list.GetSelectedItemCount() + self.edit.Enable(count == 1) + self.delete.Enable(count > 0) + + def on_left_down(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + index, flags = self.list.HitTest(event.GetPosition()) + if flags & wx.LIST_HITTEST_NOWHERE: + self.edit.Disable() + self.delete.Disable() + event.Skip() + + def on_edit(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + count = self.list.GetSelectedItemCount() + if count != 1: + return + index = self.list.GetNextItem(-1, + wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED) + feed = self.model.feeds[index] + window = EditFeedDialog(self, feed) + window.CenterOnScreen() + result = window.ShowModal() + window.Destroy() + if result == wx.ID_OK: + self.update() + + def on_new(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + feed = AddFeedDialog.show_wizard(self) + if feed: + self.model.feeds.append(feed) + self.update() + + def on_delete(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + dialog = wx.MessageDialog(self.dialog, + 'Are you sure you want to delete the selected feed(s)?', + 'Confirm Delete', + wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION) + result = dialog.ShowModal() + dialog.Destroy() + if result != wx.ID_YES: + return + feeds = [] + index = -1 + while True: + index = self.list.GetNextItem( + index, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED) + if index < 0: + break + feed = self.model.feeds[index] + feeds.append(feed) + if feeds: + for feed in feeds: + self.model.feeds.remove(feed) + self.update() + + +class FiltersPanel(wx.Panel): + """[summary] + + Arguments: + wx {[type]} -- [description] + """ + + def __init__(self, parent, dialog): + """[summary] + + Arguments: + parent {[type]} -- [description] + dialog {[type]} -- [description] + """ + + super(FiltersPanel, self).__init__(parent, -1) + self.dialog = dialog + self.model = dialog.model + panel = self.create_panel(self) + sizer = wx.BoxSizer(wx.VERTICAL) + line = wx.StaticLine(self, -1) + sizer.Add(line, 0, wx.EXPAND) + sizer.Add(panel, 1, wx.EXPAND | wx.ALL, 8) + self.SetSizerAndFit(sizer) + + def create_panel(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + panel = wx.Panel(parent, -1) + list = FiltersList(panel, self.dialog) + list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_selection) + list.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.on_selection) + list.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.on_edit) + list.Bind(wx.EVT_LEFT_DOWN, self.on_left_down) + self.list = list + buttons = self.create_buttons(panel) + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(list, 1, wx.EXPAND) + sizer.AddSpacer(8) + sizer.Add(buttons, 0, wx.EXPAND) + panel.SetSizerAndFit(sizer) + return panel + + def create_buttons(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + new = wx.Button(parent, -1, 'Add...') + edit = wx.Button(parent, -1, 'Edit...') + delete = wx.Button(parent, -1, 'Delete') + new.Bind(wx.EVT_BUTTON, self.on_new) + edit.Bind(wx.EVT_BUTTON, self.on_edit) + delete.Bind(wx.EVT_BUTTON, self.on_delete) + edit.Disable() + delete.Disable() + self.edit = edit + self.delete = delete + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(new) + sizer.AddSpacer(8) + sizer.Add(edit) + sizer.AddSpacer(8) + sizer.Add(delete) + sizer.AddStretchSpacer(1) + return sizer + + def update(self): + """[summary] + """ + + self.list.update() + self.update_buttons() + self.dialog.on_change() + + def on_selection(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + event.Skip() + self.update_buttons() + + def update_buttons(self): + """[summary] + """ + + count = self.list.GetSelectedItemCount() + self.edit.Enable(count == 1) + self.delete.Enable(count > 0) + + def on_left_down(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + index, flags = self.list.HitTest(event.GetPosition()) + if flags & wx.LIST_HITTEST_NOWHERE: + self.edit.Disable() + self.delete.Disable() + event.Skip() + + def on_edit(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + count = self.list.GetSelectedItemCount() + if count != 1: + return + index = self.list.GetNextItem(-1, + wx.LIST_NEXT_ALL, + wx.LIST_STATE_SELECTED) + filter = self.model.filters[index] + window = EditFilterDialog(self, self.model, filter) + window.Center() + result = window.ShowModal() + window.Destroy() + if result == wx.ID_OK: + self.update() + + def on_new(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + window = EditFilterDialog(self, self.model) + window.Center() + result = window.ShowModal() + filter = window.filter + window.Destroy() + if result == wx.ID_OK: + self.model.filters.append(filter) + self.update() + + def on_delete(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + dialog = wx.MessageDialog(self.dialog, + 'Are you sure you want to delete the selected filter(s)?', + 'Confirm Delete', + wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION) + result = dialog.ShowModal() + dialog.Destroy() + if result != wx.ID_YES: + return + filters = [] + index = -1 + while True: + index = self.list.GetNextItem( + index, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED) + if index < 0: + break + filter = self.model.filters[index] + filters.append(filter) + if filters: + for filter in filters: + self.model.filters.remove(filter) + self.update() + + +class PopupsPanel(wx.Panel): + """[summary] + + Arguments: + wx {[type]} -- [description] + """ + + def __init__(self, parent, dialog): + """[summary] + + Arguments: + parent {[type]} -- [description] + dialog {[type]} -- [description] + """ + + super(PopupsPanel, self).__init__(parent, -1) + self.dialog = dialog + self.model = dialog.model + panel = self.create_panel(self) + sizer = wx.BoxSizer(wx.VERTICAL) + line = wx.StaticLine(self, -1) + sizer.Add(line, 0, wx.EXPAND) + sizer.Add(panel, 1, wx.EXPAND | wx.ALL, 8) + self.update_controls() + self.SetSizerAndFit(sizer) + + def create_panel(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + panel = wx.Panel(parent, -1) + sizer = wx.BoxSizer(wx.VERTICAL) + behavior = self.create_behavior(panel) + appearance = self.create_appearance(panel) + content = self.create_content(panel) + sizer.Add(behavior, 0, wx.EXPAND) + sizer.AddSpacer(8) + sizer.Add(appearance, 0, wx.EXPAND) + sizer.AddSpacer(8) + sizer.Add(content, 0, wx.EXPAND) + panel.SetSizerAndFit(sizer) + return panel + + def create_appearance(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + box = wx.StaticBox(parent, -1, 'Appearance') + sizer = wx.StaticBoxSizer(box, wx.VERTICAL) + grid = wx.GridBagSizer(8, 8) + labels = ['Position', 'Width', 'Monitor', + 'Transparency', 'Border', 'Border Size'] + positions = [(0, 0), (0, 3), (1, 0), (1, 3), (2, 0), (2, 3)] + for label, position in zip(labels, positions): + text = wx.StaticText(parent, -1, label) + grid.Add(text, position, + flag=wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + position = wx.Choice(parent, -1) + position.Append('Upper Left', (-1, -1)) + position.Append('Upper Right', (1, -1)) + position.Append('Lower Left', (-1, 1)) + position.Append('Lower Right', (1, 1)) + position.Append('Center', (0, 0)) + width = wx.SpinCtrl(parent, -1, '1', min=1, max=9999, size=(64, -1)) + transparency = wx.SpinCtrl( + parent, -1, '0', min=0, max=255, size=(64, -1)) + display = wx.Choice(parent, -1) + + for index in range(wx.Display.GetCount()): + display.Append('Monitor #%d' % (index + 1), index) + + border_color = wx.Button(parent, -1) + border_size = wx.SpinCtrl(parent, -1, '1', min=0, max=9, size=(64, -1)) + + grid.Add(position, (0, 1), flag=wx.EXPAND) + grid.Add(display, (1, 1), flag=wx.EXPAND) + grid.Add(width, (0, 4)) + grid.Add(transparency, (1, 4)) + grid.Add(border_color, (2, 1), flag=wx.EXPAND) + grid.Add(border_size, (2, 4)) + text = wx.StaticText(parent, -1, 'pixels') + grid.Add(text, (0, 5), flag=wx.ALIGN_CENTER_VERTICAL) + text = wx.StaticText(parent, -1, '[0-255], 255=opaque') + grid.Add(text, (1, 5), flag=wx.ALIGN_CENTER_VERTICAL) + text = wx.StaticText(parent, -1, 'pixels') + grid.Add(text, (2, 5), flag=wx.ALIGN_CENTER_VERTICAL) + sizer.Add(grid, 1, wx.EXPAND | wx.ALL, 8) + + position.Bind(wx.EVT_CHOICE, self.on_change) + display.Bind(wx.EVT_CHOICE, self.on_change) + width.Bind(wx.EVT_SPINCTRL, self.on_change) + transparency.Bind(wx.EVT_SPINCTRL, self.on_change) + border_size.Bind(wx.EVT_SPINCTRL, self.on_change) + border_color.Bind(wx.EVT_BUTTON, self.on_border_color) + + self.position = position + self.display = display + self.width = width + self.transparency = transparency + self.border_color = border_color + self.border_size = border_size + return sizer + + def create_behavior(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + box = wx.StaticBox(parent, -1, 'Behavior') + sizer = wx.StaticBoxSizer(box, wx.VERTICAL) + grid = wx.GridBagSizer(8, 8) + + text = wx.StaticText(parent, -1, 'Duration') + grid.Add(text, (0, 0), flag=wx.ALIGN_CENTER_VERTICAL) + text = wx.StaticText(parent, -1, 'seconds') + grid.Add(text, (0, 2), flag=wx.ALIGN_CENTER_VERTICAL) + + duration = wx.SpinCtrl(parent, -1, '1', min=1, max=60, size=(64, -1)) + auto = wx.CheckBox(parent, -1, 'Infinite duration') + sound = wx.CheckBox(parent, -1, 'Sound notification') + hover = wx.CheckBox(parent, -1, 'Wait if hovering') + top = wx.CheckBox(parent, -1, 'Stay on top') + + grid.Add(duration, (0, 1)) + grid.Add(auto, (0, 4), flag=wx.ALIGN_CENTER_VERTICAL) + grid.Add(sound, (1, 4), flag=wx.ALIGN_CENTER_VERTICAL) + grid.Add(hover, (0, 6), flag=wx.ALIGN_CENTER_VERTICAL) + grid.Add(top, (1, 6), flag=wx.ALIGN_CENTER_VERTICAL) + + sizer.Add(grid, 1, wx.EXPAND | wx.ALL, 8) + + duration.Bind(wx.EVT_SPINCTRL, self.on_change) + auto.Bind(wx.EVT_CHECKBOX, self.on_change) + sound.Bind(wx.EVT_CHECKBOX, self.on_change) + hover.Bind(wx.EVT_CHECKBOX, self.on_change) + top.Bind(wx.EVT_CHECKBOX, self.on_change) + + self.duration = duration + self.auto = auto + self.sound = sound + self.hover = hover + self.top = top + return sizer + + def create_content(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + box = wx.StaticBox(parent, -1, 'Content') + sizer = wx.StaticBoxSizer(box, wx.VERTICAL) + grid = wx.GridBagSizer(8, 8) + + text = wx.StaticText(parent, -1, 'Max. Title Length') + grid.Add(text, (0, 0), flag=wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + text = wx.StaticText(parent, -1, 'Max. Body Length') + grid.Add(text, (1, 0), flag=wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + text = wx.StaticText(parent, -1, 'characters') + grid.Add(text, (0, 2), flag=wx.ALIGN_CENTER_VERTICAL) + text = wx.StaticText(parent, -1, 'characters') + grid.Add(text, (1, 2), flag=wx.ALIGN_CENTER_VERTICAL) + + title = wx.SpinCtrl(parent, -1, '1', min=1, max=9999, size=(64, -1)) + body = wx.SpinCtrl(parent, -1, '1', min=1, max=9999, size=(64, -1)) + grid.Add(title, (0, 1)) + grid.Add(body, (1, 1)) + + sizer.Add(grid, 1, wx.EXPAND | wx.ALL, 8) + + title.Bind(wx.EVT_SPINCTRL, self.on_change) + body.Bind(wx.EVT_SPINCTRL, self.on_change) + + self.title = title + self.body = body + return sizer + + def update_controls(self): + """[summary] + """ + + model = self.model + self.width.SetValue(model.POPUP_WIDTH) + self.transparency.SetValue(model.POPUP_TRANSPARENCY) + self.duration.SetValue(model.POPUP_DURATION) + self.auto.SetValue(not model.POPUP_AUTO_PLAY) + self.sound.SetValue(model.PLAY_SOUND) + self.hover.SetValue(model.POPUP_WAIT_ON_HOVER) + self.top.SetValue(model.POPUP_STAY_ON_TOP) + self.title.SetValue(model.POPUP_TITLE_LENGTH) + self.body.SetValue(model.POPUP_BODY_LENGTH) + util.select_choice(self.position, model.POPUP_POSITION) + util.select_choice(self.display, model.POPUP_DISPLAY) + self.border_color.SetBackgroundColour( + wx.Colour(*settings.POPUP_BORDER_COLOR)) + self.border_size.SetValue(model.POPUP_BORDER_SIZE) + + def update_model(self): + """[summary] + """ + + model = self.model + model.POPUP_WIDTH = self.width.GetValue() + model.POPUP_TRANSPARENCY = self.transparency.GetValue() + model.POPUP_DURATION = self.duration.GetValue() + model.POPUP_TITLE_LENGTH = self.title.GetValue() + model.POPUP_BODY_LENGTH = self.body.GetValue() + model.POPUP_AUTO_PLAY = not self.auto.GetValue() + model.POPUP_WAIT_ON_HOVER = self.hover.GetValue() + model.POPUP_STAY_ON_TOP = self.top.GetValue() + model.PLAY_SOUND = self.sound.GetValue() + model.POPUP_POSITION = self.position.GetClientData( + self.position.GetSelection()) + model.POPUP_DISPLAY = self.display.GetClientData( + self.display.GetSelection()) + model.POPUP_BORDER_SIZE = self.border_size.GetValue() + color = self.border_color.GetBackgroundColour() + model.POPUP_BORDER_COLOR = (color.Red(), color.Green(), color.Blue()) + + def on_border_color(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + data = wx.ColourData() + data.SetColour(self.border_color.GetBackgroundColour()) + dialog = wx.ColourDialog(self, data) + if dialog.ShowModal() == wx.ID_OK: + self.border_color.SetBackgroundColour( + dialog.GetColourData().GetColour()) + self.on_change(event) + + def on_change(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.dialog.on_change() + event.Skip() + + +class OptionsPanel(wx.Panel): + """[summary] + + Arguments: + wx {[type]} -- [description] + """ + + def __init__(self, parent, dialog): + """[summary] + + Arguments: + parent {[type]} -- [description] + dialog {[type]} -- [description] + """ + + super(OptionsPanel, self).__init__(parent, -1) + self.dialog = dialog + self.model = dialog.model + panel = self.create_panel(self) + sizer = wx.BoxSizer(wx.VERTICAL) + line = wx.StaticLine(self, -1) + sizer.Add(line, 0, wx.EXPAND) + sizer.Add(panel, 1, wx.EXPAND | wx.ALL, 8) + self.update_controls() + self.SetSizerAndFit(sizer) + + def create_panel(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + panel = wx.Panel(parent, -1) + sizer = wx.BoxSizer(wx.VERTICAL) + general = self.create_general(panel) + caching = self.create_caching(panel) + proxy = self.create_proxy(panel) + sizer.Add(general, 0, wx.EXPAND) + sizer.AddSpacer(8) + sizer.Add(caching, 0, wx.EXPAND) + sizer.AddSpacer(8) + sizer.Add(proxy, 0, wx.EXPAND) + panel.SetSizerAndFit(sizer) + return panel + + def create_general(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + box = wx.StaticBox(parent, -1, 'General') + sizer = wx.StaticBoxSizer(box, wx.VERTICAL) + grid = wx.GridBagSizer(8, 8) + + idle = wx.CheckBox( + parent, -1, "Don't check feeds if I've been idle for") + grid.Add(idle, (0, 0), flag=wx.ALIGN_CENTER_VERTICAL) + text = wx.StaticText(parent, -1, 'seconds') + grid.Add(text, (0, 2), flag=wx.ALIGN_CENTER_VERTICAL) + + timeout = wx.SpinCtrl(parent, -1, '1', min=1, max=9999, size=(64, -1)) + grid.Add(timeout, (0, 1)) + + auto_update = wx.CheckBox( + parent, -1, 'Check for software updates automatically') + grid.Add(auto_update, (1, 0), flag=wx.ALIGN_CENTER_VERTICAL) + check_now = wx.Button(parent, -1, 'Check Now') + grid.Add(check_now, (1, 1), flag=wx.ALIGN_CENTER_VERTICAL) + + sizer.Add(grid, 1, wx.EXPAND | wx.ALL, 8) + + timeout.Bind(wx.EVT_SPINCTRL, self.on_change) + idle.Bind(wx.EVT_CHECKBOX, self.on_change) + auto_update.Bind(wx.EVT_CHECKBOX, self.on_change) + check_now.Bind(wx.EVT_BUTTON, self.on_check_now) + + self.idle = idle + self.timeout = timeout + self.auto_update = auto_update + self.check_now = check_now + return sizer + + def create_caching(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + box = wx.StaticBox(parent, -1, 'Caching') + sizer = wx.StaticBoxSizer(box, wx.VERTICAL) + grid = wx.GridBagSizer(8, 8) + + text = wx.StaticText(parent, -1, 'Pop-up History') + grid.Add(text, (0, 0), flag=wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + #text = wx.StaticText(parent, -1, 'Item Cache') + #grid.Add(text, (1, 0), flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT) + text = wx.StaticText(parent, -1, 'days') + grid.Add(text, (0, 2), flag=wx.ALIGN_CENTER_VERTICAL) + #text = wx.StaticText(parent, -1, 'items per feed') + #grid.Add(text, (1, 2), flag=wx.ALIGN_CENTER_VERTICAL) + + item = wx.SpinCtrl(parent, -1, '1', min=1, max=365, size=(64, -1)) + grid.Add(item, (0, 1)) + #feed = wx.SpinCtrl(parent, -1, '1', min=1, max=9999, size=(64, -1)) + #grid.Add(feed, (1, 1)) + + clear_item = wx.Button(parent, -1, 'Clear') + grid.Add(clear_item, (0, 3)) + #clear_feed = wx.Button(parent, -1, 'Clear') + #grid.Add(clear_feed, (1, 3)) + + sizer.Add(grid, 1, wx.EXPAND | wx.ALL, 8) + + item.Bind(wx.EVT_SPINCTRL, self.on_change) + #feed.Bind(wx.EVT_SPINCTRL, self.on_change) + clear_item.Bind(wx.EVT_BUTTON, self.on_clear_item) + #clear_feed.Bind(wx.EVT_BUTTON, self.on_clear_feed) + + self.item = item + #self.feed = feed + self.clear_item = clear_item + #self.clear_feed = clear_feed + return sizer + + def create_proxy(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + box = wx.StaticBox(parent, -1, 'Proxy') + sizer = wx.StaticBoxSizer(box, wx.VERTICAL) + grid = wx.GridBagSizer(8, 8) + + use_proxy = wx.CheckBox(parent, -1, 'Use a proxy server') + grid.Add(use_proxy, (0, 0), flag=wx.ALIGN_CENTER_VERTICAL) + proxy_url = wx.TextCtrl(parent, -1, style=wx.TE_PASSWORD) + grid.Add(proxy_url, (1, 0), flag=wx.EXPAND) + text = wx.StaticText( + parent, -1, 'Format: http://:@:\nLeave blank to use Windows proxy settings.') + grid.Add(text, (2, 0), flag=wx.ALIGN_CENTER_VERTICAL) + + sizer.Add(grid, 1, wx.EXPAND | wx.ALL, 8) + + use_proxy.Bind(wx.EVT_CHECKBOX, self.on_change) + proxy_url.Bind(wx.EVT_TEXT, self.on_change) + + self.use_proxy = use_proxy + self.proxy_url = proxy_url + return sizer + + def update_controls(self): + """[summary] + """ + + model = self.model + self.idle.SetValue(model.DISABLE_WHEN_IDLE) + self.timeout.SetValue(model.USER_IDLE_TIMEOUT) + self.auto_update.SetValue(model.CHECK_FOR_UPDATES) + one_day = 60 * 60 * 24 + self.item.SetValue(model.ITEM_CACHE_AGE / one_day) + self.use_proxy.SetValue(model.USE_PROXY) + self.proxy_url.ChangeValue(util.decode_password(model.PROXY_URL) or '') + self.enable_controls() + + def update_model(self): + """[summary] + """ + + model = self.model + model.DISABLE_WHEN_IDLE = self.idle.GetValue() + model.USER_IDLE_TIMEOUT = self.timeout.GetValue() + model.CHECK_FOR_UPDATES = self.auto_update.GetValue() + one_day = 60 * 60 * 24 + model.ITEM_CACHE_AGE = self.item.GetValue() * one_day + model.USE_PROXY = self.use_proxy.GetValue() + model.PROXY_URL = util.encode_password(self.proxy_url.GetValue()) + + def enable_controls(self): + """[summary] + """ + + self.timeout.Enable(self.idle.GetValue()) + self.proxy_url.Enable(self.use_proxy.GetValue()) + + def on_change(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.enable_controls() + self.dialog.on_change() + event.Skip() + + def on_clear_item(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.model.controller.manager.clear_item_history() + self.clear_item.Disable() + + def on_clear_feed(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.model.controller.manager.clear_feed_cache() + self.clear_feed.Disable() + + def on_check_now(self, event): + """[summary] + + Arguments: + event {[type]} -- [description] + """ + + self.check_now.Disable() + self.model.controller.check_for_updates() + + +class AboutPanel(wx.Panel): + """[summary] + + Arguments: + wx {[type]} -- [description] + """ + + def __init__(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + """ + + super(AboutPanel, self).__init__(parent, -1) + panel = self.create_panel(self) + sizer = wx.BoxSizer(wx.VERTICAL) + line = wx.StaticLine(self, -1) + sizer.Add(line, 0, wx.EXPAND) + sizer.Add(panel, 1, wx.EXPAND | wx.ALL, 8) + credits = ''' + %s %s :: Copyright (c) 2009-2013, Michael Fogleman + + 16x16px icons in this application are from the Silk Icon set provided by Mark James under a Creative Commons Attribution 2.5 License. http://www.famfamfam.com/lab/icons/silk/ + + Third-party components of this software include the following: + + * Python 2.6 - http://www.python.org/ + * wxPython 2.8.10 - http://www.wxpython.org/ + * Universal Feed Parser - http://www.feedparser.org/ + * PLY 3.3 - http://www.dabeaz.com/ply/ + * py2exe 0.6.9 - http://www.py2exe.org/ + * Inno Setup - http://www.jrsoftware.org/isinfo.php + + Universal Feed Parser, a component of this software, requires that the following text be included in the distribution of this application: + + Copyright (c) 2002-2005, Mark Pilgrim + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + * 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. + + 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. + + PLY 3.3 (Python Lex-Yacc), a component of this software, requires that the following text be included in the distribution of this application: + + Copyright (C) 2001-2009, + David M. Beazley (Dabeaz LLC) + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + * 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. + + * Neither the name of the David Beazley or Dabeaz LLC 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. + ''' % (settings.APP_NAME, settings.APP_VERSION) + credits = '\n'.join(line.strip() + for line in credits.strip().split('\n')) + text = wx.TextCtrl(self, -1, credits, + style=wx.TE_MULTILINE | wx.TE_READONLY) + text.SetBackgroundColour(self.GetBackgroundColour()) + sizer.Add(text, 0, wx.EXPAND | wx.ALL & ~wx.TOP, 8) + self.SetSizerAndFit(sizer) + + def create_panel(self, parent): + """[summary] + + Arguments: + parent {[type]} -- [description] + + Returns: + [type] -- [description] + """ + + panel = wx.Panel(parent, -1, style=wx.BORDER_SUNKEN) + panel.SetBackgroundColour(wx.WHITE) + sizer = wx.BoxSizer(wx.VERTICAL) + bitmap = wx.StaticBitmap(panel, -1, wx.Bitmap('icons/about.png')) + sizer.AddStretchSpacer(1) + sizer.Add(bitmap, 0, wx.ALIGN_CENTER_HORIZONTAL) + sizer.AddStretchSpacer(1) + panel.SetSizerAndFit(sizer) + + return panel + +# EOF \ No newline at end of file