async def shell(asyncState): while True: if not asyncState.loop_running and not asyncState.restarting: systray = SysTrayIcon("grey_icon.ico", "null_internet", asyncState.menu_options, on_quit=quitApplication) systray.start() asyncState.loop_running = True if asyncState.loop_running and asyncState.restarting: test_input = ('test_item', None, newMenu) await addMenuOption(asyncState, test_input) systray.shutdown() systray = SysTrayIcon("grey_icon.ico", "null_internet", asyncState.menu_options, on_quit=quitApplication) systray.start() asyncState.restarting = False if systray: await ping(systray, asyncState) await asyncio.sleep(1)
class TrayIcon(object): icon_fn = "icon.png" def __init__(self, title, menu=None, on_quit=None, icon=icon_fn): """ Param *menu: Recebe tupla contendo tuplas com ("texto do botão","ícone do menu",função). Param icon: Recebe o nome do ícone a ser carregado. """ self.__sysTrayIcon = SysTrayIcon(icon, title, menu, on_quit=on_quit) def run(self): self.__sysTrayIcon.start() def stop(self): self.__sysTrayIcon.shutdown()
class Tray: def __init__(self, MODE, VALUE, MODE_EXERCISES, EXERCISES): self.MODE = MODE if self.MODE == 0: self.menu_options = ( ("Mode : Repeat", None, self._openConfig), ("Every : " + str(VALUE) + "s", None, self._openConfig), ("Sequence's type : " + ((MODE_EXERCISES == 1) and "Randomized" or "Ordered"), None, self._openConfig), ("Exercises loaded : " + str(EXERCISES), None, self._openConfig), ("Reload", None, self._reload)) elif self.MODE == 1: self.menu_options = ( ("Mode : Intervals", None, self._openConfig), ("Hours : " + str(VALUE[0]), None, self._openConfig), ("Number of sessions per interval: " + str(VALUE[1]), None, self._openConfig), ("Time between two sessions : " + str(VALUE[2]) + "s", None, self._openConfig), ("Sequence's type : " + ((MODE_EXERCISES == 1) and "Randomized" or "Ordered"), None, self._openConfig), ("Exercises loaded : " + str(EXERCISES), None, self._openConfig), ("Reload", None, self._reload)) self.icon = getAbsPath("./config/icon.ico") self.name = "Workout Starter Pack" self.tray = SysTrayIcon(self.icon, self.name, self.menu_options , on_quit=self.quitTray) def init_icon_tray(self): self.tray.start() #Open the config file def _openConfig(self): config = getAbsPath("./config/config.ini") os.startfile(config) #Reload the whole program def _reload(self): self.tray.shutdown() restartProgram() #Quit the program and shutdown the tray def quitTray(self, t=0): notify("Workout Starter Pack", "Quitting.") os.kill(os.getpid(), signal.SIGINT)
print("[%.2d] %s" % (i, mat[2])) i += 1 n = int(input("Inserisci il numero di materie da scaricare: ")) stringa = "MaterialeSelezionato," for j in range(n): x = -1 while x not in range(1, i): x = input("Materia: ") if x.isnumeric(): x = int(x) sess.materieDaScaricare.append(x) stringa+=str(x) + " " else: continue dizionarioMateriale = {"MaterialeSelezionato": stringa} writeVariables(dizionarioMateriale) else: lista = [] for element in var.split(): if element != "": lista.append(int(element)) sess.materieDaScaricare = lista # Mostro il menù. sess.menu() systray.shutdown()
class SnuClipboardManager(NormalApp): icon = 'data/icon_small.png' tray = None tray_thread = None window_minimized = BooleanProperty(False) shift_pressed = BooleanProperty(False) ctrl_pressed = BooleanProperty(False) alt_pressed = BooleanProperty(False) window_leave_timeout = ObjectProperty(allownone=True) on_top = BooleanProperty(False) window_min_width = NumericProperty(130) window_title = 'Snu Clipboard Manager' window_width_target = NumericProperty(200) modify_mode = BooleanProperty(False) clipboard_updater = ObjectProperty(allownone=True) current_clipboard = StringProperty('') clipboard_folder = StringProperty('') clipboard_folders = ListProperty() clipboard_data = ListProperty() clipboard_data_display = ListProperty() clipboard_history = ListProperty() hiddens = ListProperty() main_history_length = NumericProperty(1) data_directory = StringProperty('') show_undo = BooleanProperty(True) show_strip = BooleanProperty(False) max_history = NumericProperty(20) preset_type = StringProperty() preset_element = StringProperty() preset_second_element = StringProperty() edit_type = StringProperty() edit_path = StringProperty() edit_name = StringProperty() edit_name_original = StringProperty() edit_section = StringProperty() edit_content = StringProperty() search_text = StringProperty() search_results = ListProperty() search_content = BooleanProperty(True) def search(self, *_): data = [] if self.search_text: search_text = self.search_text.lower() for clip in self.clipboard_data: if clip['viewtype'] == 'item': if (search_text in clip['text'].lower()) or ( self.search_content and search_text in clip['clipboard'].lower()): new_clip = clip.copy() new_clip['hidden'] = False new_clip['show_folder'] = True data.append(new_clip) self.search_results = data def dismiss_popup(self, *_): if self.popup is not None: try: self.popup.dismiss() except: pass self.popup = None def rescale_interface(self, *_, force=False): """Called when the window changes resolution, calculates variables dependent on screen size""" self.store_window_size() if Window.width != self.last_width: self.last_width = Window.width self.popup_x = min(Window.width, 640) if (Window.height != self.last_height) or force: self.last_height = Window.height self.button_scale = int( float(cm(0.85)) * (int(self.config.get("Settings", "buttonsize")) / 100)) self.text_scale = int( (self.button_scale / 3) * int(self.config.get("Settings", "textsize")) / 100) self.display_border = self.button_scale / 3 self.display_padding = self.button_scale / 4 def get_application_config(self, **kwargs): if platform == 'win': self.data_directory = os.getenv( 'APPDATA') + os.path.sep + "Snu Clipboard Manager" if not os.path.isdir(self.data_directory): os.makedirs(self.data_directory) elif platform == 'linux': self.data_directory = os.path.expanduser( '~') + os.path.sep + ".snuclipboardmanager" if not os.path.isdir(self.data_directory): os.makedirs(self.data_directory) elif platform == 'macosx': self.data_directory = os.path.expanduser( '~') + os.path.sep + ".snuclipboardmanager" if not os.path.isdir(self.data_directory): os.makedirs(self.data_directory) elif platform == 'android': self.data_directory = self.user_data_dir else: self.data_directory = os.path.sep config_file = os.path.realpath( os.path.join(self.data_directory, "snuclipboardmanager.ini")) return config_file def on_config_change(self, config, section, key, value): self.load_config_values() self.rescale_interface(force=True) def load_config_values(self): self.show_undo = self.config.getboolean("Settings", 'showundo') self.show_strip = self.config.getboolean("Settings", 'showstrip') max_history = int(self.config.get("Settings", 'max_history')) if max_history < 1: self.max_history = 1 else: self.max_history = max_history main_history_length = int( self.config.get("Settings", "main_history_length")) if main_history_length > self.max_history: self.main_history_length = self.max_history elif main_history_length < 1: self.main_history_length = 1 else: self.main_history_length = main_history_length def load_clipboards(self, edit=False): self.load_hiddens() self.clipboard_data = [] if edit: self.clipboard_data.append({ 'text': 'Add Section', 'viewtype': 'adder-heading', 'hidden': False, 'section': self.clipboard_folder, 'path': self.clipboard_folder, 'show_folder': False }) if os.path.isdir(self.clipboard_folder): sections = os.listdir(self.clipboard_folder) for section in sections: if section in self.hiddens and not edit: hidden = True else: hidden = False section_folder = os.path.join(self.clipboard_folder, section) if os.path.isdir(section_folder): self.clipboard_data.append({ 'text': section, 'viewtype': 'heading', 'hidden': hidden, 'section': section, 'path': section_folder, 'section_folder': self.clipboard_folder, 'show_folder': False }) if edit: self.clipboard_data.append({ 'text': 'Add Item', 'viewtype': 'adder-item', 'hidden': hidden, 'section': section, 'path': section_folder, 'section_folder': self.clipboard_folder, 'show_folder': False }) for file in os.listdir(section_folder): fullpath = os.path.join(section_folder, file) if os.path.isfile(fullpath): with open(fullpath, 'r') as datafile: try: data = datafile.read() except: continue self.clipboard_data.append({ 'text': os.path.splitext(file)[0], 'viewtype': 'item', 'clipboard': data, 'hidden': hidden, 'section': section, 'path': fullpath, 'section_folder': section_folder, 'show_folder': False }) self.update_display_clipboards() def toggle_hide_section(self, section, hide_others=False): for data in self.clipboard_data: if data['section'] == section: if hide_others: data['hidden'] = False else: data['hidden'] = not data['hidden'] elif hide_others: data['hidden'] = True self.save_hiddens() self.root.ids.presets.refresh_from_data() self.update_display_clipboards() def update_display_clipboards(self, *_): data = [] for clip in self.clipboard_data: if not clip['hidden'] or clip['viewtype'] == 'heading': data.append(clip) self.clipboard_data_display = data def clear_history(self): self.clipboard_history = self.clipboard_history[:1] def remove_history_item(self, index): if index > 0: self.clipboard_history.pop(index) def remove_history_matches(self, clipboard): for clip in reversed(self.clipboard_history): if clip['text'] == clipboard: self.clipboard_history.remove(clip) def set_history_item(self, index): try: history_item = self.clipboard_history.pop(index) except: return self.clipboard_history.insert(0, history_item) self.set_clipboard(history_item['text'], skip_history=True) def set_clipboard_from_widget(self, widget): if widget.focus: self.set_clipboard(widget.text, overwrite_history=True) def set_clipboard(self, text, skip_history=False, overwrite_history=False): #Sets a preset to the clipboard if text and text != self.current_clipboard: Clipboard.copy(text) if overwrite_history: if len(self.clipboard_history) == 0: self.clipboard_history.append({'text': text}) else: self.clipboard_history[0]['text'] = text self.current_clipboard = text self.refresh_history_areas() if skip_history: self.current_clipboard = text def refresh_history_areas(self): self.root.ids.historyFull.refresh_from_data() self.root.ids.historyShort.refresh_from_data() def update_current_clipboard(self, *_): #Sets the current clipboard to a local variable clipboard = Clipboard.paste() if clipboard and self.current_clipboard != clipboard: self.current_clipboard = clipboard if self.config.getboolean('Settings', 'no_duplicates'): self.remove_history_matches(clipboard) self.clipboard_history.insert(0, {'text': clipboard}) self.clipboard_history = self.clipboard_history[:self.max_history] def undo_clipboard(self, *_): self.set_history_item(1) def strip_clipboard(self, *_): Clipboard.copy(self.current_clipboard.strip()) def settings_mode(self, heading=''): self.close_settings() if self.modify_mode: self.modify_mode = False if self.root.ids.editArea.current == 'edit': self.load_clipboards() else: self.modify_mode = True if self.root.ids.editArea.current == 'edit': self.load_clipboards(edit=True) self.size_window() def delete_preset(self, preset): self.preset_type = preset.viewtype self.preset_element = preset.path if preset.viewtype == 'heading': confirm_text = "This entire section of presets will be deleted permanently" else: confirm_text = "This single preset will be deleted permanently" self.dismiss_popup() content = ConfirmPopupContent(text=confirm_text, yes_text='Delete', no_text="Don't Delete", warn_yes=True) content.bind(on_answer=self.delete_preset_answer) self.popup = NormalPopup(title="Confirm Delete ", content=content, size_hint=(1, None), size=(1000, self.button_scale * 4)) self.popup.bind(on_dismiss=self.dismiss_popup) self.popup.open() def delete_preset_answer(self, instance, answer): self.dismiss_popup() if answer == 'yes': if self.preset_type == 'item': if os.path.isfile(self.preset_element): os.remove(self.preset_element) self.load_clipboards(edit=True) elif self.preset_type == 'heading': if os.path.isdir(self.preset_element): shutil.rmtree(self.preset_element) self.load_clipboards(edit=True) def edit_preset(self, preset): if preset.viewtype == 'item': self.edit_type = preset.viewtype self.edit_path = preset.path self.edit_name = preset.text self.edit_name_original = preset.text self.edit_section = preset.section_folder self.edit_content = preset.clipboard self.root.ids.editContentArea.focus = True elif preset.viewtype == 'heading': self.preset_type = 'heading' self.preset_element = preset.section_folder self.preset_second_element = preset.text content = InputPopupContent(text="Rename folder to:", input_text=preset.text, hint='Folder Name') content.bind(on_answer=self.rename_folder_answer) self.popup = NormalPopup(title="Rename Folder", content=content, size_hint=(1, None), size=(1000, self.button_scale * 5)) self.popup.bind(on_dismiss=self.dismiss_popup) self.popup.open() def rename_folder_answer(self, instance, answer): new_name = instance.ids["input"].text.strip(' ') self.dismiss_popup() if not new_name: return if answer == 'yes': old_path = os.path.join(self.preset_element, self.preset_second_element) new_path = os.path.join(self.preset_element, new_name) if old_path != new_path: try: os.rename(old_path, new_path) if self.edit_section == old_path: self.edit_path = os.path.join( new_path, self.edit_name_original) + '.txt' self.edit_section = new_path self.load_clipboards(edit=True) except Exception as e: app.message_popup(text="Unable to rename folder: " + str(e), title="Warning") def save_edit(self): if not self.edit_path or not self.edit_name or not self.edit_section: return new_file = os.path.join(self.edit_section, self.edit_name) + '.txt' if new_file != self.edit_path: #rename file try: os.rename(self.edit_path, new_file) self.edit_path = new_file except Exception as e: app.message_popup(text="Unable to save edit: " + str(e), title="Warning") return try: file = open(self.edit_path, "w") file.write(self.edit_content) file.close() self.load_clipboards(edit=True) except Exception as e: app.message_popup(text="Unable to write to file: " + str(e), title="Warning") def clear_edit(self, *_): self.edit_content = '' self.edit_name = '' self.edit_name_original = '' self.edit_path = '' self.edit_section = '' def scroll_presets_to(self, section, item): scroll_to_index = None preset_area = self.root.ids['presets'] for index, data in enumerate(preset_area.data): if not item: if data['viewtype'] == 'heading' and data['section'] == section: scroll_to_index = index + 2 break else: if data['section'] == section and data[ 'viewtype'] == 'item' and data['path'] == item: scroll_to_index = index break if scroll_to_index is not None: preset_area.scroll_to_index(scroll_to_index) def instant_add(self, path, section): self.dismiss_popup() if not self.modify_mode: self.settings_mode() self.root.ids.editArea.current = 'edit' self.load_clipboards(edit=True) self.edit_type = 'item' self.edit_path = path self.edit_name = '' self.edit_name_original = '' self.edit_section = section self.edit_content = self.current_clipboard self.scroll_presets_to(section, '') content = InstantAddPresetContent() content.bind(on_answer=self.instant_add_preset_answer) self.popup = NormalPopup(title='Create File', content=content, size_hint=(1, 1), size=(1000, 2000)) self.popup.bind(on_dismiss=self.dismiss_popup) self.popup.bind(on_dismiss=self.clear_edit) self.popup.open() def instant_add_preset_answer(self, instance, answer): self.dismiss_popup() if answer == 'yes': filename = os.path.join(self.edit_path, self.edit_name) + '.txt' if os.path.exists(filename): self.message_popup(text="File already exists!", title="Warning") self.clear_edit() return file = open(filename, 'w') file.write(self.edit_content) file.close() self.load_clipboards() self.clear_edit() def add_preset(self, preset_type, preset_location): self.preset_type = preset_type self.preset_element = preset_location if preset_type == 'heading': input_text = "Add a folder with the name:" hint_text = "Folder Name" title_text = "Create Folder" else: input_text = "Add a preset with the name:" hint_text = "Preset Name" title_text = "Create File" self.dismiss_popup() content = InputPopupContent(text=input_text, hint=hint_text) content.bind(on_answer=self.add_preset_answer) self.popup = NormalPopup(title=title_text, content=content, size_hint=(1, None), size=(1000, self.button_scale * 5)) self.popup.bind(on_dismiss=self.dismiss_popup) self.popup.open() def add_preset_answer(self, instance, answer): preset_name = instance.ids['input'].text.strip(' ') self.dismiss_popup() if not preset_name: return if answer == 'yes': preset_type = self.preset_type preset_location = self.preset_element if preset_type == 'heading': path = os.path.join(preset_location, preset_name) try: os.makedirs(path) except Exception as e: app.message_popup(text="Unable to create folder: " + str(e), title="Warning") self.load_clipboards(edit=True) else: filename = os.path.join(preset_location, preset_name) + '.txt' if not os.path.isfile(filename): try: file = open(filename, 'w+') file.close() self.edit_type = preset_type self.edit_path = filename self.edit_name = preset_name self.edit_name_original = preset_name self.edit_section = preset_location self.edit_content = '' self.root.ids.editContentArea.focus = True except Exception as e: app.message_popup(text="Unable to create file: " + str(e), title="Warning") self.load_clipboards(edit=True) else: app.message_popup(text="File already exists!", title="Warning") def load_clipboard_folders(self): folders = [self.clipboard_folder] folder_items = self.config.items("PresetsPaths") for key, folder in folder_items: if folder not in folders: folders.append(folder) folders_data = [] for folder in folders: if folder: folders_data.append({'path': folder}) self.clipboard_folders = folders_data def save_clipboard_folders(self): self.config.remove_section("PresetsPaths") self.config.add_section("PresetsPaths") for index, folder_item in enumerate(self.clipboard_folders): folder = folder_item['path'] self.config.set("PresetsPaths", str(index), folder) def remove_presets_folder(self, path): for index, preset in enumerate(self.clipboard_folders): if preset['path'] == path: self.clipboard_folders.pop(index) self.save_clipboard_folders() def set_presets_folder(self, path): self.save_hiddens() self.clipboard_folder = path self.config.set("Settings", "presetsfolder", path) self.load_clipboards() def add_presets_folder(self, *_): self.not_on_top() from plyer import filechooser try: filechooser.choose_dir(on_selection=self.add_presets_folder_finish, title='Select A Presets Directory') except: #Filechooser can fail if user selects root if self.config.getboolean("Settings", "alwaysontop"): self.always_on_top() pass def add_presets_folder_finish(self, path): if self.config.getboolean("Settings", "alwaysontop"): self.always_on_top() path = path[0] if path: if path not in self.clipboard_folders: self.clipboard_folders.append({'path': path}) self.save_clipboard_folders() self.set_presets_folder(path) else: self.message("Path Already Added") else: self.message("No Path Found") def browse_presets(self): try: import webbrowser webbrowser.open(self.clipboard_folder) except: pass def size_window(self, *_): Window.left = self.window_left Window.top = self.window_top height = self.window_height width = int(self.button_scale * 4 * (float(self.config.get("Settings", "normalwidth")) / 100)) if width < self.window_min_width: width = self.window_min_width if self.modify_mode: self.window_width_target = width width = int( self.button_scale * 17 * (float(self.config.get("Settings", "expandedwidth")) / 100)) Window.size = width, height else: Window.size = width, height self.window_width_target = Window.width def on_button_scale(self, *_): self.size_window() def save_hiddens(self): if not self.clipboard_folder: return hiddens = [] for data in self.clipboard_data: if data['hidden'] and data['viewtype'] == 'heading': section = data['section'] if section not in hiddens: hiddens.append(section) hiddens_filename = os.path.join(self.clipboard_folder, 'hiddens.txt') try: hiddens_file = open(hiddens_filename, 'w') except: return for hidden in hiddens: hiddens_file.write(hidden + '\n') hiddens_file.close() def load_hiddens(self): hiddens = [] hiddens_filename = os.path.join(self.clipboard_folder, 'hiddens.txt') try: hiddens_file = open(hiddens_filename, 'r') except: self.hiddens = [] return lines = hiddens_file.readlines() hiddens_file.close() for line in lines: line = line.strip('\n') if line: hiddens.append(line) self.hiddens = hiddens def build_config(self, config): """Setup config file if it is not found""" window_height, window_width, window_top, window_left = self.get_window_size( ) config.setdefaults( 'Settings', { 'buttonsize': 100, 'textsize': 100, 'expandedwidth': 100, 'normalwidth': 100, 'alwaysontop': 1, 'minimizestart': 0, 'minimizetray': 0, 'showtray': 1, 'auto_shrink': 0, 'auto_expand': 0, 'no_duplicates': 0, 'showundo': 1, 'showstrip': 0, 'presetsfolder': '', 'max_history': 20, 'main_history_length': 1, 'window_height': window_height, 'window_top': window_top, 'window_left': window_left, 'window_width': window_width }) config.setdefaults('PresetsPaths', {}) def build_settings(self, settings): """Kivy settings dialog panel settings types: title, bool, numeric, options, string, path""" settingspanel = [] settingspanel.append({ "type": "label", "title": " Basic Behavior" }) settingspanel.append({ "type": "numeric", "title": "Max Clipboard History", "desc": "Number of entries to keep in clipboard history. Defaluts to 20.", "section": "Settings", "key": "max_history" }) settingspanel.append({ "type": "bool", "title": "Auto-Shrink Window", "desc": "Automatically shrink the window when the mouse leaves.", "section": "Settings", "key": "auto_shrink" }) settingspanel.append({ "type": "bool", "title": "Auto-Expand Window", "desc": "Automatically expand the window when the mouse enters.", "section": "Settings", "key": "auto_expand" }) settingspanel.append({ "type": "bool", "title": "Minimize On Startup", "desc": "Automatically minimize the program as soon as it starts.", "section": "Settings", "key": "minimizestart" }) if platform == 'win': settingspanel.append({ "type": "bool", "title": "Always-On-Top", "desc": "Sets the window to always-on-top mode on startup. Changing requires a program restart.", "section": "Settings", "key": "alwaysontop" }) settingspanel.append({ "type": "bool", "title": "Show Tray Icon", "desc": "A tray icon will be shown with options. Changing requires program restart.", "section": "Settings", "key": "showtray" }) settingspanel.append({ "type": "bool", "title": "Minimize To Tray", "desc": "Minimize to tray when minimizing the window. Only available when tray icon is enabled.", "section": "Settings", "key": "minimizetray" }) settingspanel.append({ "type": "bool", "title": "Disallow Duplicates", "desc": "Prevent duplicates from being added to clipboard history, removes the previous item when a new duplicate is added.", "section": "Settings", "key": "no_duplicates" }) settingspanel.append({ "type": "label", "title": " Main Window Appearance" }) settingspanel.append({ "type": "bool", "title": "Show Undo Clipboard Button", "desc": "Shows a button to undo the clipboard to the previous state.", "section": "Settings", "key": "showundo" }) settingspanel.append({ "type": "bool", "title": "Show Strip Clipboard Button", "desc": "Shows a button to remove all formatting and extra spaces from clipboard text.", "section": "Settings", "key": "showstrip" }) settingspanel.append({ "type": "numeric", "title": "History In Main Window", "desc": "Number of history elements to show in main window. Defaults to 1.", "section": "Settings", "key": "main_history_length" }) settingspanel.append({ "type": "label", "title": " Interface Scaling" }) settingspanel.append({ "type": "numeric", "title": "Shrunk Width Scale", "desc": "Scale of the window when it is in normal mode. Expects a percentage, defaults to 100", "section": "Settings", "key": "normalwidth" }) settingspanel.append({ "type": "numeric", "title": "Expanded Width Scale", "desc": "Scale of the window when it is in expanded mode. Expects a percentage, defaults to 100", "section": "Settings", "key": "expandedwidth" }) settingspanel.append({ "type": "numeric", "title": "Button Scale", "desc": "Button scale, expects a percentage, defaults to 100.", "section": "Settings", "key": "buttonsize" }) settingspanel.append({ "type": "numeric", "title": "Text Scale", "desc": "Font scale, expects a percentage, defaults to 100.", "section": "Settings", "key": "textsize" }) settings.add_json_panel('Settings', self.config, data=json.dumps(settingspanel)) def build(self): global app app = self if platform == 'win' and self.config.getboolean( "Settings", "showtray"): #setup system tray from infi.systray import SysTrayIcon tray_menu = ( ("Toggle Minimize", None, self.tray_click), ("Undo Clipboard", None, self.undo_clipboard), ("Strip Clipboard", None, self.strip_clipboard), ) icon_file = kivy.resources.resource_find('icon.ico') self.tray = SysTrayIcon(icon_file, 'Snu Clipboard Manager', tray_menu, on_quit=self.tray_quit) self.tray.start() else: self.tray = None self.clipboard_folder = self.config.get("Settings", "presetsfolder") self.load_clipboard_folders() self.load_clipboards() Window.bind(on_key_down=self.key_down) Window.bind(on_key_up=self.key_up) Window.bind(on_cursor_leave=self.window_exited) Window.bind(on_cursor_enter=self.window_entered) Window.bind(on_minimize=self.on_minimize) Window.bind(on_restore=self.on_restore) return MainScreen() def tray_quit(self, tray): Clock.schedule_once(self.stop) def tray_click(self, tray): if self.window_minimized: Clock.schedule_once(self.restore) else: Clock.schedule_once(self.minimize) def restore(self, *_): Window.restore() def minimize(self, *_): Window.minimize() def on_restore(self, window): self.window_minimized = False Window.show() def on_minimize(self, window): self.window_minimized = True if self.config.getboolean("Settings", "minimizetray") and self.tray is not None: Window.hide() def window_exited(self, window): self.window_leave_timeout = Clock.schedule_once( self.window_exited_finish, 1) def window_entered(self, window): self.window_entered_finish() if self.window_leave_timeout is not None: self.window_leave_timeout.cancel() self.window_leave_timeout = None def window_exited_finish(self, *_): if self.config.getboolean("Settings", 'auto_shrink'): if self.modify_mode and self.popup is None and self.root.ids.editArea.current != 'edit': self.settings_mode() def window_entered_finish(self, *_): if self.config.getboolean("Settings", 'auto_expand'): if not self.modify_mode: self.settings_mode() def on_start(self): EventLoop.window.bind(on_keyboard=self.hook_keyboard) self.load_config_values() Window.set_title(self.window_title) if self.config.getboolean("Settings", "alwaysontop"): self.always_on_top() self.load_window_size() self.clipboard_updater = Clock.schedule_interval( self.update_current_clipboard, 0.1) if not self.clipboard_folder: self.root.ids.editArea.current = 'select' self.modify_mode = True Clock.schedule_once(self.add_presets_folder) else: self.modify_mode = False self.size_window() if self.config.getboolean("Settings", "minimizestart"): self.minimize() def always_on_top(self, *_): #Set on top mode self.on_top = True from KivyOnTop import register_topmost register_topmost(Window, self.window_title) def not_on_top(self, *_): self.on_top = False from KivyOnTop import unregister_topmost unregister_topmost(Window, self.window_title) def toggle_on_top(self): if self.on_top: self.not_on_top() else: self.always_on_top() def get_window_size(self): #Get window size if platform == 'win': SPI_GETWORKAREA = 48 SM_CXSCREEN = 0 SM_CYSCREEN = 1 #The height of the screen of the primary display monitor, in pixels. SM_CYCAPTION = 4 #The height of a caption area, in pixels. SM_CYBORDER = 6 #The height of a window border, in pixels. SM_CYFIXEDFRAME = 8 #The thickness of the frame around the perimeter of a window that has a caption but is not sizable, in pixels. from win32api import GetSystemMetrics taskbar_height = GetSystemMetrics(SPI_GETWORKAREA) screen_height = GetSystemMetrics(SM_CYSCREEN) screen_width = GetSystemMetrics(SM_CXSCREEN) window_frame_size = GetSystemMetrics( SM_CYFIXEDFRAME) + GetSystemMetrics(SM_CYBORDER) window_left_offset = window_frame_size window_top_offset = GetSystemMetrics( SM_CYCAPTION) + window_frame_size window_height = screen_height - window_top_offset - window_left_offset - taskbar_height window_width = screen_width - window_left_offset - window_left_offset return window_height, window_width, window_top_offset, window_left_offset else: return 680, self.window_min_width, 30, 5 def on_pause(self): self.save_hiddens() self.config.write() return True def on_stop(self): if not (self.modify_mode and self.root.ids.editArea.current == 'edit'): self.save_hiddens() self.config.write() if self.tray is not None: self.tray.shutdown() def toggle_area(self, area): self.close_settings() if not self.modify_mode: self.root.ids.editArea.current = area self.modify_mode = True if area == 'edit': self.load_clipboards(edit=True) self.size_window() else: if self.root.ids.editArea.current == area: self.modify_mode = False self.load_clipboards() self.size_window() else: self.root.ids.editArea.current = area if area == 'edit': self.load_clipboards(edit=True) def text_input_active(self): """Checks if any 'NormalInput' or 'FloatInput' widgets are currently active (being typed in). Returns: True or False """ input_active = False for widget in self.root.walk(restrict=True): if widget.__class__.__name__ == 'NormalInput' or widget.__class__.__name__ == 'FloatInput' or widget.__class__.__name__ == 'IntegerInput': if widget.focus: input_active = True break return input_active def check_for_active_input(self, root): #Walks widget tree and checks for an active multiline text input, returns that input if found, otherwise returns None has_input = None for widget in root.walk(restrict=True): if isinstance(widget, TextInput): if widget.focus: return widget return has_input def hook_keyboard(self, window, scancode, *_): """This function receives keyboard events""" #print(scancode) if scancode == 282: #f1 key if not self.modify_mode: self.settings_mode() if scancode == 27: #escape key if self.popup: self.popup.content.dispatch('on_answer', 'no') return True if scancode == 13: #enter key if self.popup: has_input = self.check_for_active_input(self.popup) if has_input is not None: self.popup.content.dispatch('on_answer', 'yes') if scancode == 96: #tilde key if self.alt_pressed or self.ctrl_pressed or not self.text_input_active( ): self.settings_mode() if scancode == 283: #f2 key self.toggle_area('history') if scancode == 284: #f3 key self.toggle_area('edit') if scancode == 285: #f4 key self.toggle_area('select') if scancode == 97: #a key if self.alt_pressed: self.toggle_on_top() if scancode == 109: #m key if self.alt_pressed: self.minimize() def key_down(self, key, scancode=None, *_): """Intercepts various key presses and sends commands to the current screen.""" del key if scancode == 307 or scancode == 308: #alt keys self.alt_pressed = True if scancode == 305 or scancode == 306: #ctrl keys self.ctrl_pressed = True if scancode == 303 or scancode == 304: #shift keys self.shift_pressed = True def key_up(self, key, scancode=None, *_): """Checks for the shift key released.""" del key if scancode == 307 or scancode == 308: self.alt_pressed = False if scancode == 305 or scancode == 306: self.ctrl_pressed = False if scancode == 303 or scancode == 304: self.shift_pressed = False
class ProjectApp(App): def build(self): # delete unnecessary default config file to avoid overloading App.get_application_config try: remove('project.ini') except: pass # check if config file in app dir exists self.custom_config_name = 'pppm.ini' self.app_folder = dirname(realpath(__file__)) self.app_folder_config_fn = join(self.app_folder, self.custom_config_name) if isfile(self.app_folder_config_fn): # config is in app dir self.config.filename = self.app_folder_config_fn else: # config is in user dir self.config.filename = join(self.user_data_dir, self.custom_config_name) # title self.title = '3PM' # initialize settings self.use_kivy_settings = False self.settings_cls = SettingsWithTabbedPanel self.icon = join('data', 'icon.ico') # read configuration file self.config.read(self.config.filename) # initialize projects self.projects = Projects(name='projects', config=self.config) self.load_projects() # initialize ebs history self.velocity_history = [] self.load_velocity_history() # initialize timer self.timer = Timer(self.config) self.simulation_string = StringProperty() # screen management and transition self.transition = SlideTransition(duration=.35) root = ScreenManager(transition=self.transition) root.add_widget(self.projects) return root def build_config(self, config): if platform in ['android', 'ios']: # smartphone defaults config.setdefaults( 'timer', { 'start_sound': 1, 'end_sound': 1, 'vibrate': 1, 'notification': 0, 'notification_timeout': 10, 'session_length': 25, 'use_notepad': 0 }) config.setdefaults('ebs', { 'use_ebs': 1, 'number_history': 30, 'log_activity': 0 }) config.setdefaults('system', { 'enable_tray': 0, 'hide_window': 0, 'store_in_app': 0 }) else: # defaults for desktop computers config.setdefaults( 'timer', { 'start_sound': 1, 'end_sound': 1, 'vibrate': 0, 'notification': 1, 'notification_timeout': 10, 'session_length': 25, 'use_notepad': 1 }) config.setdefaults('ebs', { 'use_ebs': 1, 'number_history': 30, 'log_activity': 1 }) config.setdefaults('system', { 'enable_tray': 1, 'hide_window': 0, 'store_in_app': 0 }) def build_settings(self, settings): settings.add_json_panel('Timer', self.config, data=settings_info.timer_settings_json) settings.add_json_panel('Simulation', self.config, data=settings_info.ebs_settings_json) settings.add_json_panel('System', self.config, data=settings_info.system_settings_json) def on_config_change(self, config, section, key, value): # react on store_in_app config change if section == 'system' and key == 'store_in_app': if value == '0': try: # move data to user folder move_if_exists(join(self.app_folder, 'projects.json'), join(self.user_data_dir, 'projects.json')) move_if_exists( join(self.app_folder, 'velocity_history.json'), join(self.user_data_dir, 'velocity_history.json')) move_if_exists( join(self.app_folder, 'daily_activity.txt'), join(self.user_data_dir, 'daily_activity.txt')) # write config to user folder self.config.filename = join(self.user_data_dir, self.custom_config_name) self.config.write() # remove old config file remove(self.app_folder_config_fn) except: # failed to move to new dir, undo config change self.config.set('system', 'store_in_app', '1') else: try: # move data to app folder move_if_exists(join(self.user_data_dir, 'projects.json'), join(self.app_folder, 'projects.json')) move_if_exists( join(self.user_data_dir, 'velocity_history.json'), join(self.app_folder, 'velocity_history.json')) move_if_exists( join(self.user_data_dir, 'daily_activity.txt'), join(self.app_folder, 'daily_activity.txt')) # write config to app folder self.config.filename = join(self.app_folder, self.custom_config_name) self.config.write() # remove old config file remove(join(self.user_data_dir, self.custom_config_name)) except: # failed to move to new dir, undo config change self.config.set('system', 'store_in_app', '0') # react on enable_tray config change if section == 'system' and key == 'enable_tray' and platform == 'win': if value == '0': # shutdown tray icon try: self.systray.shutdown() except: pass else: # initialize system tray - todo: encapsulate menu_options = (("Show Session Info", "Show current task and remaining time.", self.systray_show_info), ) hover_text = "Personal Project Productivity Manager - 3PM" self.systray = SysTrayIcon("data/icon.ico", hover_text, menu_options, default_menu_index=0, on_quit=self.systray_close_window) self.systray.start() # reinitialize timer self.timer.init(config) # reinitialize projects self.projects = Projects(name='projects', config=config) self.load_projects() # update projects view config self.projects.use_ebs = config.get('ebs', 'use_ebs') == '1' self.root.remove_widget(self.root.get_screen('projects')) self.root.add_widget(self.projects) self.root.current = 'projects' def on_start(self): # no current project index self.current_project_index = -1 if platform == 'win' and self.config.get('system', 'enable_tray') == '1': # initialize system tray - todo: encapsulate menu_options = (("Show Session Info", "Show current task and remaining time.", self.systray_show_info), ) hover_text = "Personal Project Productivity Manager - 3PM" self.systray = SysTrayIcon("data/icon.ico", hover_text, menu_options, default_menu_index=0, on_quit=self.systray_close_window) self.systray.start() def on_stop(self): self.config.write() if platform == 'win': # shutdown tray icon try: self.systray.shutdown() except: pass def systray_show_info(self, sysTrayIcon): # remaining time if self.timer.running_down: message_time = '%s remaining.' % self.timer.time_string elif self.timer.running_up: message_time = 'On break since %s.' % self.timer.time_string else: message_time = 'No session running.' # project string if self.current_project_index == -1: project_title = 'Quick Session' else: project_title = self.projects.data[ self.current_project_index]['title'] # show notification self.timer.notification_wrapper.notify( title=project_title, message=message_time, timeout=self.timer.notification_timeout) def systray_close_window(self, sysTrayIcon): # stop app if not called from traybar menu (not settings change) if self.config.get('system', 'enable_tray') == '1': try: App.get_running_app().stop() except: exit(0) def load_velocity_history(self): # load velocity history from file if isfile(self.velocity_history_fn): with open(self.velocity_history_fn) as fd: self.velocity_history = json.load(fd) else: # assume bad estimation self.velocity_history = [ 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9 ] def save_velocity_history(self): # save velocity history to file with open(self.velocity_history_fn, 'w') as fd: json.dump(self.velocity_history, fd) def load_projects(self): # load projects from file if not isfile(self.projects_fn): return with open(self.projects_fn) as fd: data = json.load(fd) self.projects.data = data def save_projects(self): # save projects to file with open(self.projects_fn, 'w') as fd: json.dump(self.projects.data, fd) def delete_project(self, project_index): # go to project list self.go_projects(project_index) # delete project del self.projects.data[project_index] self.save_projects() self.refresh_projects() def finish_project(self, project_index): # go to project list self.go_projects(project_index) if self.config.get('ebs', 'use_ebs') == '1': # get velocity rating vel_rating = self.projects.data[project_index][ 'logged'] / self.projects.data[project_index]['estimated'] if vel_rating > 0: # save velocity rating to history self.velocity_history.append(vel_rating) self.save_velocity_history() # delete project del self.projects.data[project_index] self.save_projects() self.refresh_projects() def edit_project(self, project_index): self.current_project_index = project_index project = self.projects.data[project_index] name = 'project{}'.format(project_index) if self.root.has_screen(name): self.root.remove_widget(self.root.get_screen(name)) if self.config.get('ebs', 'use_ebs') == '1': if self.config.get('timer', 'use_notepad') == '0': view = ProjectViewWithoutNotepad( name=name, project_index=project_index, project_title=project.get('title'), project_estimated=project.get('estimated'), project_logged=project.get('logged')) else: view = ProjectView(name=name, project_index=project_index, project_title=project.get('title'), project_content=project.get('content'), project_estimated=project.get('estimated'), project_logged=project.get('logged')) else: if self.config.get('timer', 'use_notepad') == '0': view = ProjectViewSimpleWithoutNotepad( name=name, project_index=project_index, project_title=project.get('title')) else: view = ProjectViewSimple( name=name, project_index=project_index, project_title=project.get('title'), project_content=project.get('content')) # add widget to root self.root.add_widget(view) self.transition.direction = 'left' self.root.current = view.name # update timer logged view self.timer.update_logged_string(project.get('logged')) # update simulation string self.update_simulation_string(project_index) def quick_session(self): if not self.root.current == '': # remove previous quick view screen if self.root.has_screen(''): self.root.remove_widget(self.root.get_screen('')) view = QuickView(project_content='') self.root.add_widget(view) self.transition.direction = 'left' self.root.current = view.name self.current_project_index = -1 self.start_timer() def add_project(self): self.projects.data.append({ 'title': 'Unnamed Task', 'content': '', 'logged': 0, 'estimated': 1 }) project_index = len(self.projects.data) - 1 self.edit_project(project_index) def set_project_content(self, project_index, project_content): self.projects.data[project_index]['content'] = project_content data = self.projects.data self.projects.data = [] self.projects.data = data self.save_projects() self.refresh_projects() def set_project_title(self, project_index, project_title): self.projects.data[project_index]['title'] = project_title self.save_projects() self.refresh_projects() def set_project_estimated(self, project_index, estimated): new_estimate = float(estimated) # sanity check if not new_estimate > 0: new_estimate = 1.0 # save new estimate to project self.projects.data[project_index]['estimated'] = new_estimate self.save_projects() self.refresh_projects() # update simulation string self.update_simulation_string(project_index) def set_project_logged(self, project_index, logged): self.projects.data[project_index]['logged'] = float(logged) self.save_projects() self.refresh_projects() def refresh_projects(self): data = self.projects.data self.projects.data = [] self.projects.data = data def go_projects(self, project_index): # stop in- or decrementing time Clock.unschedule(self.increment_time) Clock.unschedule(self.decrement_time) if project_index == -1: # quick session view; stop timer self.stop_timer() else: # project view; log work and stop timer if self.timer.running_down: self.stop_work(project_index) # go to project view self.transition.direction = 'right' self.root.current = 'projects' self.current_project_index = -1 def start_work(self, project_index): # start new session if timer completely stopped if not self.timer.running_down and not self.timer.running_up: # start timer self.start_timer() # set current project index self.current_project_index = project_index # or reset and start new session if timer runs up if self.timer.running_up: # stop counting up self.stop_timer() # start timer self.start_timer() def stop_work(self, project_index): # only log work when timer running down if self.timer.running_down: if not project_index == -1: # log work self.log_work(project_index) # save log self.refresh_projects() self.save_projects() # stop timer self.stop_timer() def log_work(self, project_index): # get logged fractional unit: (full session time - remaining time) / full session time full_session_time = float(self.config.get('timer', 'session_length')) logged_new = (full_session_time * 60. - (self.timer.minutes * 60. + self.timer.seconds)) / \ (full_session_time * 60.) logged_total = self.projects.data[project_index]['logged'] + logged_new # update logged self.set_project_logged(project_index, logged_total) # update logged view self.timer.update_logged_string(logged_total) # update simulation view self.update_simulation_string(project_index) def start_timer(self): # stop in- or decrementing time Clock.unschedule(self.increment_time) Clock.unschedule(self.decrement_time) # start decrementing time Clock.schedule_interval(self.decrement_time, 1) # store flags that timer is running down self.timer.running_up = False self.timer.running_down = True # play start sound if file found if self.timer.start_sound_activated and self.timer.start_sound: self.timer.start_sound.play() # hide main window if option activated if platform == 'win' and self.config.get('system', 'hide_window') == '1': self.root_window.hide() def stop_timer(self): # stop in- or decrementing time Clock.unschedule(self.increment_time) Clock.unschedule(self.decrement_time) # store flag that timer is not running down or up self.timer.running_down = False self.timer.running_up = False # reinitialize timer self.timer.minutes = self.timer.session_length self.timer.seconds = 0 self.timer.update_time_string() def decrement_time(self, interval): # decrement time of timer self.timer.seconds -= 1 if self.timer.seconds < 0: self.timer.minutes -= 1 self.timer.seconds = 59 if self.timer.minutes < 0: self.timer_alarm() self.timer.update_time_string() def increment_time(self, interval): # increment time of timer self.timer.seconds += 1 if self.timer.seconds > 60: self.timer.minutes += 1 self.timer.seconds = 0 self.timer.update_time_string() def timer_alarm(self): # stop decrementing time Clock.unschedule(self.decrement_time) # set timer to 0:00 self.timer.minutes = 0 self.timer.seconds = 0 # update time string self.timer.update_time_string() # log work if not self.current_project_index == -1: self.log_work(self.current_project_index) # save log self.refresh_projects() self.save_projects() if platform == 'win' and self.config.get('system', 'hide_window') == '1': # show main window again self.root_window.show() # log date of completed session to file if self.config.get('ebs', 'log_activity') == '1': # date and count for this session date_today = datetime.datetime.today().strftime('%Y-%m-%d') count_today = 1 # read old file if isfile(self.activity_fn): with open(self.activity_fn, 'r') as f: # read lines lines = f.readlines() # get date and count date_file, count_file = lines[-1].split() if date_file == date_today: # add count from earlier sessions today count_today += int(count_file) # remove last line with current log for today lines = lines[:-1] else: lines = [] # append new last line lines.append("%s\t%s\n" % (date_today, str(count_today))) # write new file with open(self.activity_fn, 'w') as f: f.writelines(lines) # store flags that timer is not running down but up self.timer.running_down = False self.timer.running_up = True # start incrementing time Clock.schedule_interval(self.increment_time, 1) # show notification if self.timer.notification_activated: self.timer.notification_wrapper.notify( title="3PM", message="Session finished!", timeout=self.timer.notification_timeout) # play alarm sound if file found if self.timer.start_sound_activated and self.timer.alarm_sound: self.timer.alarm_sound.play() # vibrate on smartphone if self.timer.vibration_activated and platform in ['android', 'ios']: vibrator.vibrate(2) def simulate_completion(self, project_index): # only use most recent histories number_history = int(self.config.get('ebs', 'number_history')) if len(self.velocity_history) > number_history: velocity_history = self.velocity_history[-number_history:] else: velocity_history = self.velocity_history # MC simulate completion of project sessions_needed = [] for i in range(0, 100): # randomly choose a velocity rating vel = random.choice(velocity_history) # simulate necessity sessions sessions_needed.append( vel * self.projects.data[project_index]['estimated']) # sort sessions_needed = sorted(sessions_needed) # pick quartiles quartiles = [sessions_needed[i] for i in [24, 49, 74, 99]] # calc completion logged = float(self.projects.data[project_index]['logged']) completion = [logged * 100 / quartiles[i] for i in [0, 1, 2, 3]] return quartiles, completion def update_simulation_string(self, project_index): # simulate completion of project quartiles, completion = self.simulate_completion(project_index) simulation_string = "%i/%i/%i/%i\n%i%%/%i%%/%i%%/%i%%" % tuple( quartiles + completion) self.timer.update_simulation_string(simulation_string) @property def data_dir(self): # get settings and data dir if self.config.get('system', 'store_in_app') == '1': return dirname(realpath(__file__)) else: return self.user_data_dir @property def projects_fn(self): return join(self.data_dir, 'projects.json') @property def velocity_history_fn(self): return join(self.data_dir, 'velocity_history.json') @property def activity_fn(self): return join(self.data_dir, 'daily_activity.txt')
class WindowsSystemTray(object): def __init__(self): self.image_dir = os.path.join(plexpy.PROG_DIR, 'data/interfaces/', plexpy.CONFIG.INTERFACE, 'images') self.icon = os.path.join(self.image_dir, 'logo-circle.ico') if plexpy.UPDATE_AVAILABLE: self.hover_text = common.PRODUCT + ' - Update Available!' self.update_title = 'Check for Updates - Update Available!' else: self.hover_text = common.PRODUCT self.update_title = 'Check for Updates' if plexpy.CONFIG.LAUNCH_STARTUP: launch_start_icon = os.path.join(self.image_dir, 'check-solid.ico') else: launch_start_icon = None if plexpy.CONFIG.LAUNCH_BROWSER: launch_browser_icon = os.path.join(self.image_dir, 'check-solid.ico') else: launch_browser_icon = None self.menu = [['Open Tautulli', None, self.tray_open, 'default'], ['', None, 'separator', None], [ 'Start Tautulli at Login', launch_start_icon, self.tray_startup, None ], [ 'Open Browser when Tautulli Starts', launch_browser_icon, self.tray_browser, None ], ['', None, 'separator', None], [self.update_title, None, self.tray_check_update, None], ['Restart', None, self.tray_restart, None]] if not plexpy.FROZEN: self.menu.insert(6, ['Update', None, self.tray_update, None]) self.tray_icon = SysTrayIcon(self.icon, self.hover_text, self.menu, on_quit=self.tray_quit) def start(self): logger.info("Launching Windows system tray icon.") try: self.tray_icon.start() except Exception as e: logger.error("Unable to launch system tray icon: %s." % e) def shutdown(self): self.tray_icon.shutdown() def update(self, **kwargs): self.tray_icon.update(**kwargs) def tray_open(self, tray_icon): plexpy.launch_browser(plexpy.CONFIG.HTTP_HOST, plexpy.HTTP_PORT, plexpy.HTTP_ROOT) def tray_startup(self, tray_icon): plexpy.CONFIG.LAUNCH_STARTUP = not plexpy.CONFIG.LAUNCH_STARTUP set_startup() def tray_browser(self, tray_icon): plexpy.CONFIG.LAUNCH_BROWSER = not plexpy.CONFIG.LAUNCH_BROWSER set_startup() def tray_check_update(self, tray_icon): versioncheck.check_update() def tray_update(self, tray_icon): if plexpy.UPDATE_AVAILABLE: plexpy.SIGNAL = 'update' else: self.hover_text = common.PRODUCT + ' - No Update Available' self.update_title = 'Check for Updates - No Update Available' self.menu[5][0] = self.update_title self.update(hover_text=self.hover_text, menu_options=self.menu) def tray_restart(self, tray_icon): plexpy.SIGNAL = 'restart' def tray_quit(self, tray_icon): plexpy.SIGNAL = 'shutdown' def change_tray_update_icon(self): if plexpy.UPDATE_AVAILABLE: self.hover_text = common.PRODUCT + ' - Update Available!' self.update_title = 'Check for Updates - Update Available!' else: self.hover_text = common.PRODUCT + ' - No Update Available' self.update_title = 'Check for Updates' self.menu[5][0] = self.update_title self.update(hover_text=self.hover_text, menu_options=self.menu) def change_tray_icons(self): if plexpy.CONFIG.LAUNCH_STARTUP: launch_start_icon = os.path.join(self.image_dir, 'check-solid.ico') else: launch_start_icon = None if plexpy.CONFIG.LAUNCH_BROWSER: launch_browser_icon = os.path.join(self.image_dir, 'check-solid.ico') else: launch_browser_icon = None self.menu[2][1] = launch_start_icon self.menu[3][1] = launch_browser_icon self.update(menu_options=self.menu)
class TaskTray(): def __init__(self, wallcord): global credit_text self.window = wallcord self.root = self.window.root self.version = self.window.version with open("data.json", "r") as f: self.data = load(f) credit_text = credit_text.replace("!version!", self.version) if self.version in ["1.1.1", "1.1.0", "1.0.0"]: messagebox.showwarning( "FreedomWall", "バージョンとFreedomWallのプログラムがあいませんでした。\n再ダウンロードしてください。") self.window.onoff = False self.window.q.append(self.exit) # メインスレッドじゃないとTkinterメソッドを実行できない。 # だからメインスレッドのFreedomWallクラスのself.qに実行したいのを追加する。 # そしてメインスレッドから実行する。 # だから lambda がある。 self.icon = SysTrayIcon( "icon.ico", "FreedomWall", (("FreedomWall", None, lambda sysTrayIcon: self.window.q.append(self.credit)), ("Set", None, lambda sysTrayIcon: self.window.q.append(self.setting)), ("Del", None, lambda sysTrayIcon: self.window.q.append(self.delete)), ("List", None, lambda sysTrayIcon: self.window.q.append(self.setting_list))), on_quit=lambda sysTrayIcon: self.window.q.append(self.exit)) # 壁紙の設定。 def setting(self): # simpledialog.askstringで入力ボックスを表示する。 # それを使い設定をする。 title = simpledialog.askstring("FreedomWall", "設定したいウィンドウのタイトルにある文字を入力してください。") if not title: return alpha = simpledialog.askstring( "FreedomWall", "壁紙の透明度を入力してください。\nデフォルトは0.2です。\n元の背景が白の場合は0.3あたりの数値が良いです。\n元の背景が黒の場合は0.1あたりの数値が良いです。" ) if not alpha: return try: alpha = float(alpha) except: messagebox.showwarning("FreedomWall", "0.1 ~ 1 の間を設定してください。") return exception = simpledialog.askstring( "FreedomWall", "例外ウィンドウのタイトルにある文字を入力してください。\nコンマ( , )を使うことで複数追加できます。\n\n例えばLINEを登録したあとにLINEについてのウェブサイトを閲覧したとします。\nするとブラウザのタイトルにLINEが入り被りが発生します。\nそれを防ぐためにブラウザのタイトルを入れることをおすすめします。" ) exception = exception.split(",") if exception else [] # 壁紙ファイルを取得する。 file_path = filedialog.askopenfilename( filetypes=[("壁紙ファイル", "*.png;*.jpg;*.mp4")]) if not file_path: return # 存在確認をする。 if not GetWindow(title, "bubun"): messagebox.showwarning("FreedomWall", f"{title}があるタイトルのウィンドウが見つかりませんでした。") return self.data["windows"][title] = { "path": file_path, "alpha": alpha, "exception": exception } with open("data.json", "w") as f: dump(self.data, f, indent=4) # メインスレッドのdataを再読み込みさせる。 self.window.reload() self.window.now = {"path": "", "alpha": 0, "exception": []} messagebox.showinfo("FreedomWall", "設定しました。") # 壁紙の削除。 def delete(self): title = simpledialog.askstring("FreedomWall", "削除したい設定名を入力してください。") if not title: return if not title in self.data["windows"]: messagebox.showwarning("FreedomWall", "その設定が見つかりませんでした。") return del self.data["windows"][title] with open("data.json", "w") as f: dump(self.data, f, indent=4) # メインスレッドのdataを再読み込みさせる。 self.window.reload() self.window.now = {"path": "", "alpha": 0, "exception": []} messagebox.showinfo("FreedomWall", "その設定を削除しました。") # 壁紙一覧。 def setting_list(self): desc = ", ".join(data for data in self.data["windows"].keys()) messagebox.showinfo("FreedomWall", desc) # クレジット。 def credit(self): messagebox.showinfo("FreedomWall", credit_text) # 終了。 def exit(self): self.root.quit() self.window.video = None self.icon.shutdown() # 実行。 def run(self): self.icon.start()
Home[f'E{i+2}'] = f'=DetailledHome!F{i+2}' Home[f'G{i+2}'] = f'=DetailledHome!E{i+2}' Home[f'H{i+2}'] = f'=DetailledHome!I{i+2}' Home['K1'] = '=DetailledHome!V1' #Fill Values sheet for i in range(len(prices)): values[f'A{i+2}'] = prices[i].get('symbol') values[f'B{i+2}'] = prices[i].get('price') values['D1'] = "Last update at " + str( datetime.now().strftime("%H:%M:%S %d-%m-%Y")) #Save Excel try: workbook.save('trading.xlsx') print('Saved !') except: print('Not saved because file is open...') pass while run: modify() for i in range(refresh): if run == True: sleep(1) else: break tray.shutdown()
class TrayIcon(Pipeable): """ Non-blocking tray icon. Tray icons require blocking due to having to wait for button clicks. To prevent blocking on the main thread, a tread is spawned for the tray icon and click events are send through `Pipeable` pipes. """ MenuOptionCallback = Callable[[SysTrayIcon], None] MenuOption = tuple[str, Optional[str], MenuOptionCallback] _thread: Thread _icon: SysTrayIcon _menu_options: list[MenuOption] = [] _stop_event: Event _icon_image: str _hover_text: str def __init__(self, icon_image: str = "icon.ico", hover_text: str = "Hover text", stop_event: Event = Event()): """ Initialize system tray icon. Args: icon_image (str, optional): Name of image to use as tray icon. Defaults to "icon.ico". hover_text (str, optional): Text displayed when the tray icon is hovered. Defaults to "Hover text". stop_event (Event, optional): Flag is set when the tray icon process exits. Defaults to Event(). """ super().__init__() self._stop_event = stop_event self._icon_image = icon_image self._hover_text = hover_text self._menu_options = [ ("Show QR code", None, lambda icon: self._pipe.send("show_qr_code")), ("Show debug logs", None, lambda icon: self._pipe.send("show_debug_logs")) ] def run(self): """ Display the tray icon in the system tray. Tray icon thread is spawned and the tray loop is executed. """ self._icon = SysTrayIcon( self._icon_image, self._hover_text, tuple(self._menu_options), on_quit=self.handle_quit) self._thread = Thread(target=self._loop) self._thread.start() def handle_quit(self, icon: SysTrayIcon): """ Set the stop flag. Called when the quit option is selected from the icons context menu. Args: icon (SysTrayIcon): SysTrayIcon reference """ self._stop_event.set() def join(self): """Join the thread that runs the TrayIcon.""" self._thread.join() def _loop(self): """ Listen to events on the system tray icon. @NOTE This should not be run directly and will block the main thread. It is automatically executed on a spawned worker thread internally. """ self._icon.start() while True: if self._stop_event.is_set(): self._icon.shutdown() return sleep(SLEEP_INTERVAL)
class WinShutdownTimer(GridLayout, ToggleButtonBehavior): # Define default layout attributes font_size = 20 widget_padding = (25, 10, 25, 10) spacer_width = 10 abort_disabled = BooleanProperty(True) abort_background_color = ListProperty([1, 1, 1, 1]) popup_active = BooleanProperty(False) # Define default cmd button attributes shutdown_btn_disabled = BooleanProperty(False) shutdown_btn_state = StringProperty('normal') restart_btn_disabled = BooleanProperty(False) restart_btn_state = StringProperty('normal') hibernate_btn_disabled = BooleanProperty(False) hibernate_btn_state = StringProperty('normal') logoff_btn_disabled = BooleanProperty(False) logoff_btn_state = StringProperty('normal') # Define default time button attributes set20_disabled = BooleanProperty(False) set20_state = StringProperty('normal') set40_disabled = BooleanProperty(False) set40_state = StringProperty('normal') set60_disabled = BooleanProperty(False) set60_state = StringProperty('normal') set90_disabled = BooleanProperty(False) set90_state = StringProperty('normal') set120_disabled = BooleanProperty(False) set120_state = StringProperty('normal') preset_status = BooleanProperty(True) preset_keybinding_enabled = BooleanProperty(True) # Define -15min/+15min buttons' status sub_time_disabled = BooleanProperty(True) add_time_disabled = BooleanProperty(False) # Establish countdown variable countdown = NumericProperty(0) # Define Start/Pause button attributes start_pause = StringProperty('Start') start_pause_disabled = BooleanProperty(True) # Establish final cmd string variable final_cmd = StringProperty('') # Retrieve default settings if the file exists, else create the file and # set defaults try: with open(user_settings_file, 'r') as f: user_settings = ast.literal_eval(f.read()) except FileNotFoundError: user_settings = { 'default_cmd': 'shutdown', 'default_time': 'set20', } with open(user_settings_file, 'w+') as f: json.dump(user_settings, f, indent=4) def __init__(self): super(WinShutdownTimer, self).__init__() self._keyboard = Window.request_keyboard(self._keyboard_closed, self) self._keyboard.bind(on_key_down=self._on_keyboard_down) # Define system tray icon icon_path = os.path.join(os.path.dirname(__file__), '.\Images\powerbutton_UAh_icon.ico') # Context Menu Options -- NOTE: Quit is REQUIRED def on_quit(systray): App.get_running_app().stop() def on_help(systray): ctypes.windll.user32.MessageBoxW( None, u'Help options to be defined in a future release', u'Help', 0) # def on_abort(systray): # self.reset() def on_about(systray): ctypes.windll.user32.MessageBoxW(None, u'WinShutdown\n\nBy: WutDuk?', u'About', 0) menu_options = ( ('Help', None, on_help), # ('Abort', None, on_abort), ('About', None, on_about)) # # Set tray icon options self.systray = SysTrayIcon(icon_path, 'WinShutdown', menu_options, on_quit) # Bind to closing the window Window.bind(on_close=self.close_systray) # Close system tray icon def close_systray(self, *args): self.systray.shutdown() def apply_defaults(self): default_cmd = self.user_settings['default_cmd'] default_time = self.user_settings['default_time'] # cmd_group buttons if default_cmd == 'shutdown': self.shutdown_btn_state = 'down' elif default_cmd == 'restart': self.restart_btn_state = 'down' elif default_cmd == 'hibernate': self.hibernate_btn_state = 'down' elif default_cmd == 'log off': self.logoff_btn_state = 'down' # time buttons if default_time == 'set20': self.set20_state = 'down' self.countdown = 20 * 60 elif default_time == 'set40': self.set40_state = 'down' self.countdown = 40 * 60 elif default_time == 'set60': self.set60_state = 'down' self.countdown = 60 * 60 elif default_time == 'set90': self.set90_state = 'down' self.countdown = 90 * 60 elif default_time == 'set120': self.set120_state = 'down' self.countdown = 120 * 60 # Enable Start/Pause and -15 min buttons self.start_pause_disabled = False self.sub_time_disabled = False # Method to get the current cmd value def get_cmd(self): if self.ids.shutdown.state == 'down': self.cmd = 'Shutdown' elif self.ids.restart.state == 'down': self.cmd = 'Restart' elif self.ids.hibernate.state == 'down': self.cmd = 'Hibernate' elif self.ids.logoff.state == 'down': self.cmd = 'Log Off' return self.cmd # Method to get the current time value def get_time(self): if self.ids.set20.state == 'down': self.time = 'set20' elif self.ids.set40.state == 'down': self.time = 'set40' elif self.ids.set60.state == 'down': self.time = 'set60' elif self.ids.set90.state == 'down': self.time = 'set90' elif self.ids.set120.state == 'down': self.time = 'set120' return self.time def set_app_settings(self): with open(user_settings_file, 'w') as f: json.dump(self.user_settings, f, indent=4) def get_curr_settings(self): self.user_settings['default_cmd'] = self.get_cmd().lower() self.user_settings['default_time'] = self.get_time() def _keyboard_closed(self): self._keyboard.unbind(on_key_down=self._on_keyboard_down) self._keyboard = None # See \kivy\core\window\__init__.py for keycode details def _on_keyboard_down(self, keyboard, keycode, text, modifiers): if self.preset_keybinding_enabled == True: # cmd buttons if keycode[0] == 115: self.ids.shutdown.trigger_action(0) elif keycode[0] == 114: self.ids.restart.trigger_action(0) elif keycode[0] == 104: self.ids.hibernate.trigger_action(0) elif keycode[0] == 108: self.ids.logoff.trigger_action(0) # preset duration buttons elif keycode[0] in (50, 258): self.ids.set20.trigger_action(0) elif keycode[0] in (52, 260): self.ids.set40.trigger_action(0) elif keycode[0] in (54, 262): self.ids.set60.trigger_action(0) elif keycode[0] in (57, 265): self.ids.set90.trigger_action(0) elif keycode[0] in (49, 257): self.ids.set120.trigger_action(0) # subtract time / add time buttons if self.sub_time_disabled == False: if keycode[0] in (45, 269, 274, 276): self.ids.minus15.trigger_action(0) if self.add_time_disabled == False: if keycode[0] in (61, 270, 273, 275): self.ids.plus15.trigger_action(0) # If there's no active popup, then if self.popup_active == False: # Start/Stop buttons if self.countdown > 0 and self.start_pause_disabled == False: if keycode[0] in (13, 16, 32, 271): self.ids.start_pause.trigger_action(0) # Abort button if self.abort_disabled == False: if keycode[0] == 97: self.ids.abort.trigger_action(0) # If there is an active popop, then if self.popup_active == True: # ImminentPopup Yes/No buttons if keycode[0] == 121: self.imminent_popup.ids.yes.trigger_action(0) elif keycode[0] == 110: self.imminent_popup.ids.no.trigger_action(0) def toggle_keybinding_allowed(self): if self.ids.start_pause.state == 'down': self.preset_keybinding_enabled = False else: self.preset_keybinding_enabled = True # Method called when countdown reaches 0 to execute the selected # cmd from the cmd_group togglebuttons def initiate_shutdown(self, *args): cmd = self.cmd # Provide the reason for the restart or shutdown. These events are # documented as "Other Planned" d_cmd = '/d p:0:0' # Comment on the reason for the restart or shutdown. c_cmd = f'/c "Automated User-initiated {cmd} via WinShutdown"' # If the Start/Pause button is down (should say 'Pause') and the # countdown is at 0, then if self.countdown == 0: # If the Shutdown button is down, then if cmd == 'Shutdown': # Compile the cmd string for a shutdown # final_cmd = f'shutdown /s' + ' ' + d_cmd + ' ' + c_cmd # final_cmd = f'shutdown /s /f' + ' ' + d_cmd + ' ' + c_cmd self.final_cmd = f'shutdown /p {d_cmd} {c_cmd}' # Else, if the Restart button is down, then elif cmd == 'Restart': # Compile the cmd string for a restart self.final_cmd = f'shutdown /r {d_cmd} {c_cmd}' # Else, if the Hibernate button is down, then elif cmd == 'Hibernate': # Compile the cmd string for a hibernate self.final_cmd = 'shutdown /h' # Else, if none of the above, compile a cmd string for logoff elif cmd == 'Log Off': self.final_cmd = 'shutdown /l' # Instantiate and open the final popup then start final timer self.final_popup = FinalPopup() self.final_popup.open() self.popup_active = True self.final_popup.start_final_timer() # Method called by FinalPopup to execute self.final_cmd def exec_cmd(self, *args): # send final cmd to cmd shell subprocess.call(final_cmd, shell=True) # Method to set the countdown timer. This doesn't add time. # Instead it replaces time. def set_timer(self, button_time): # If the countdown is at 0, then if self.countdown == 0: # Then set the countdown to the time of the button that # initiated the call self.countdown = button_time # Method to clear the timer to 0 def clear_timer(self): self.countdown = 0 # Method to toggle the '-15 min' button's state def toggle_sub_time_status(self): # If the countdown is less than 15 minutes, then if self.countdown < 15 * 60: # The '-15 min' button is disabled because there's # no time to subtract self.sub_time_disabled = True # Otherwise, the button is active else: self.sub_time_disabled = False # Method to toggle the '+ 15 min' button status def toggle_add_time_status(self): # If '+ 15 min' button is not disabled, set to disabled if self.add_time_disabled == False: self.add_time_disabled = True # Else, if countdown is 0 and '+ 15 min' button is disabled, # make it active elif self.countdown == 0 or self.add_time_disabled == True: self.add_time_disabled = False # Method to toggle the Start/Pause button status def toggle_start_pause_status(self): # If the countdown is greater than 0 if self.countdown > 0: # The Start/Pause button is active and can be clicked self.start_pause_disabled = False else: # Otherwise the timer is at 0 and there's no function for # this button, so it's disabled self.start_pause_disabled = True # Method to toggle the Abort button state def toggle_abort_status(self): # If countdown is greater than 0 and the Start/Pause button # is down, then if self.countdown > 0 and self.ids.start_pause.state == 'down': # The Abort button is active, and colored red self.abort_disabled = False self.abort_background_color = [1, 0, 0, 1] # Else the button is disabled and returns to default gray else: self.abort_disabled = True self.abort_background_color = [1, 1, 1, 1] # Method to toggle the status of preset cmd buttons # (20, 40, 60, 90, 120) def toggle_cmd_status(self): # If the Start/Pause button is down (countdown is active), then if self.ids.start_pause.state == 'down': # Then preset cmd buttons are down. To apply a preset, Pause or # Abort the countdown. self.preset_status = True # Otherwise they are available and can be selected at any time else: self.preset_status = False self.shutdown_btn_disabled = self.preset_status self.restart_btn_disabled = self.preset_status self.hibernate_btn_disabled = self.preset_status self.logoff_btn_disabled = self.preset_status # Method to toggle the status of preset time buttons # (20, 40, 60, 90, 120) def toggle_preset_status(self): # If the Start/Pause button is down (countdown is active), then if self.ids.start_pause.state == 'down': # Then preset time buttons are down. To apply a preset, Pause or # Abort the countdown. self.preset_status = True # Otherwise they are available and can be selected at any time else: self.preset_status = False self.set20_disabled = self.preset_status self.set40_disabled = self.preset_status self.set60_disabled = self.preset_status self.set90_disabled = self.preset_status self.set120_disabled = self.preset_status # Method to toggle the Start/Pause button state (up or down) def toggle_start_pause_state(self): # If Start/Pause says 'Start', then if self.start_pause == 'Start': # Set the button to 'up' self.ids.start_pause.state = 'normal' # Else the button is down else: self.ids.start_pause.state = 'down' # Method to toggle the state of presets (up or down) def toggle_preset_state(self): # If countdown is at 0, the presets are in the 'up' state if self.countdown == 0: self.ids.set20.state = 'normal' self.ids.set40.state = 'normal' self.ids.set60.state = 'normal' self.ids.set90.state = 'normal' self.ids.set120.state = 'normal' # Method to toggle the state cmd buttons (up or down) def toggle_cmd_state(self): self.ids.shutdown.state = 'normal' self.ids.restart.state = 'normal' self.ids.hibernate.state = 'normal' self.ids.logoff.state = 'normal' # Method to toggle the Start/Pause text label of the button def toggle_start_pause_text(self): # If the Start/Pause button is down and the countdown is above 0, then if self.ids.start_pause.state == 'down' and self.countdown > 0: # The countdown is active, so set the button text to 'Pause' self.start_pause = 'Pause' # Else, set the text to 'Start' else: self.start_pause = 'Start' def start_stop_timer(self): # Cancel any current animation in progress Animation.cancel_all(self) # Define the rules for Animation; i.e., (where we are going, where # we're coming from) self.anim = Animation( countdown=0, duration=self.countdown, ) # on_release of Start/Pause button, if it's down and there is still # time on the clock, then if self.ids.start_pause.state == 'down' and self.countdown > 0: # Set current cmd value self.get_cmd() # On completion of the countdown, call method to initiate the # shutdown process self.anim.bind(on_complete=self.initiate_shutdown) # Start the animation self.anim.start(self) # Method to add time to the current countdown (as opposed to resetting) # v1.1 introduced the ability to edit a live countdown, so Start/Pause # state is no longer checked def add_time(self, button_time): # The timer must be stopped in order to add time self.start_stop_timer() # Add to the current countdown time the time of the button that # initiated the call self.countdown += button_time # The timer is then restarted again self.start_stop_timer() # Method to subtract time from the current countdown (as opposed to # resetting) def sub_time(self, button_time): # If subtracting time would set the countdown to 0 or less, then if button_time >= self.countdown: # If the Start/Pause button is 'down', then if self.ids.start_pause.state == 'down': # Instantiate ImminentPopup self.imminent_popup = ImminentPopup(self.cmd) # Toggle '+ 15 min' button status self.toggle_add_time_status() # Disable '- 15 min button' self.sub_time_disabled = True # Stop the countdown, then Animation.cancel_all(self) # Call the pop-up to notify User of imminent shutdown, # restart, etc. self.imminent_popup.open() self.popup_active = True # Else, Start/Pause is 'normal' and the countdown isn't active, so else: # Set the countdown to 0 self.countdown = 0 # Disable '- 15 min' button self.sub_time_disabled = True # Else, the timer will remain above 0, so else: # The timer must be stopped in order to add time, then self.start_stop_timer() # Subtract from the current countdown time the time of the button # that initiated the call self.countdown -= button_time # The timer is then restarted again self.start_stop_timer() # Method to initiate shutdown on 'Yes' response to pop-up def popup_yes(self): self.countdown = 0 self.initiate_shutdown() self.popup_active = False # Method to dismiss the pop-up and restart the countdown def popup_no(self): self.anim.start(self) self.toggle_add_time_status() self.sub_time_disabled = False self.popup_active = False # Method to reset the entire app def reset(self): Animation.cancel_all(self), self.clear_timer(), self.toggle_start_pause_status(), self.toggle_start_pause_text(), self.toggle_start_pause_state(), self.toggle_preset_status(), self.toggle_cmd_status(), self.toggle_sub_time_status(), self.toggle_abort_status(), self.toggle_keybinding_allowed(), self.apply_defaults()
class SysTrayPlugin(plugins.SimplePlugin): ''' CherryPy plugin that creates a system tray icon for Windows. Because SysTrayIcon always fires off on_quit, we can't have on_quit execute cherrypy.engine.exit() if the exit command is what triggered SysTrayIcon to close. So conditions are set to only fire on_quit when the quit_method == 'menu'. This way, when the menu option is called, it destroys SysTrayIcon then closes cherrypy. Cherrypy will try to close SysTrayIcon by calling stop(), so stop() gets reassigned to None. If the app is closed by cherrypy (whether catching a kb interrupt or the GUI shutdown button), cherrypy stops the plugin by calling stop(). Stop() reassigns SysTrayIcon._on_quit to None and calls SysTrayIcon.shutdown(). SysTrayIcon is then destroyed (twice for reasons I can't figure out), then cherrypy finishes up the engine.stop() and engine.exit(). The chain is as such: Trigger == systray menu 'Quit': SysTrayIcon._destroy() > SysTrayIcon._on_quit() > set SysTrayPlugin.quit_method = 'menu' cherrypy.engine.exit() > SysTrayPlugin.stop() > does nothing sys.exit() Trigger == KBInterrupt or GUI Shutdown: cherrypy.engine.stop() > SysTrayPlugin.stop() > disable SysTrayIcon._on_quit() SysTrayIcon.shutdown() > SysTrayIcon._destroy() > SysTrayIcon._destroy() > cherrypy.engine.exit() > sys.exit() ''' def __init__(self, bus): plugins.SimplePlugin.__init__(self, bus) menu_options = (('Open Browser', None, self.open),) self.systray = SysTrayIcon('core/favicon.ico', 'Watcher', menu_options, on_quit=self.on_quit) self.quit_method = None return def start(self): self.systray.start() return def stop(self): if self.quit_method == 'menu': return else: self.systray._on_quit = None self.systray.shutdown() return def on_quit(self, systray): self.quit_method = 'menu' cherrypy.engine.exit() sys.exit(0) # sys tray functions: def open(self, systray): webbrowser.open(u'http://{}:{}{}'.format( core.SERVER_ADDRESS, core.SERVER_PORT, core.URL_BASE)) return
actualWindow.resizeRel(0, int((a - b) * 1.60)) #print('2188?\t',c) #printuj_staty_kurwo() #print(c-a) else: actualWindow = gw.getWindowsWithTitle( win32gui.GetWindowText(win32gui.GetForegroundWindow()))[0] win32gui.SetWindowPos(win32gui.GetForegroundWindow(), win32con.HWND_TOP, argv[1][0], int(y), argv[1][1], int(h1), 0) actualWindow.moveRel(-10, 0) monitory = [] menu_options = (("Status", None, say_hello), ) systray = SysTrayIcon( 'reSize.ico', 'Ready to reSize!', menu_options) #Icon made by Freepik from www.flaticon.com systray.shutdown( ) #https://www.flaticon.com/free-icon/maximize_1141984#term=resize&page=1&position=64 systray.start() platform = pyglet.window.get_platform() display = platform.get_default_display() for screen in display.get_screens(): monitory.append(screen) zakresy_var = zakresy(monitory) keyboard.add_hotkey('ctrl+f1', callback, (zakresy_var))
class SysTrayPlugin(plugins.SimplePlugin): ''' CherryPy plugin that creates a system tray icon for Windows. Because SysTrayIcon always fires off on_quit, we can't have on_quit execute cherrypy.engine.exit() if cherrypy.engine.exit() is what triggered SysTrayIcon to close. So conditions are set to only fire on_quit when the quit_method == 'men'. This way, when the menu option is called, it destroys SysTrayIcon then closes cherrypy. Cherrypy will try to close SysTrayIcon by calling stop(), so _on_quit() gets reassigned to None. If the app is closed by cherrypy (whether catching a kb interrupt or the GUI shutdown button), cherrypy stops the plugin by calling stop(). Stop() reassigns SysTrayIcon._on_quit to None and calls SysTrayIcon.shutdown(). SysTrayIcon is then destroyed (twice for reasons I can't figure out), then cherrypy finishes up the engine.stop() and engine.exit(). The chain is as such: Trigger == systray menu 'Quit': SysTrayIcon._destroy() > SysTrayIcon._on_quit() > set SysTrayPlugin.quit_method = 'men' cherrypy.engine.exit() > SysTrayPlugin.stop() > does nothing sys.exit() Trigger == KBInterrupt or GUI Shutdown: cherrypy.engine.stop() > SysTrayPlugin.stop() > disable SysTrayIcon._on_quit() SysTrayIcon.shutdown() > SysTrayIcon._destroy() > SysTrayIcon._destroy() > cherrypy.engine.exit() > sys.exit() ''' def __init__(self, bus, icon, name, menu_options, on_quit=lambda: None): if not callable(on_quit): raise TypeError('on_quit not a callable object.') self.on_quit = on_quit plugins.SimplePlugin.__init__(self, bus) self.systray = SysTrayIcon(icon, name, menu_options, on_quit=self._on_quit) self.quit_method = None return def start(self): self.systray.start() return def stop(self): if self.quit_method == 'men': return else: self.systray._on_quit = None self.systray.shutdown() return def _on_quit(self, systray): self.on_quit() self.quit_method = 'men' cherrypy.engine.exit() sys.exit(0)
class TrayIcon: def __init__(self): menuOptions = (('Audio', None, (('Select Alert Sound', None, self._onSelectAlertSound), ('Play', None, self._onPlayAlert), ('Stop', None, self._onStopAlert))), ('Open Maps File', None, self._onOpenMapsFile), ('Select Path of Exile folder', None, self._onSelectPathOfExileDirectory)) self._icon = SysTrayIcon(iconPath, 'Map Alert', menu_options=menuOptions, on_quit=self._onQuit) self._callbackQueue = queue.Queue() async def show(self): self._icon.start() while True: try: callback = self._callbackQueue.get(False) callback() except queue.Empty: await asyncio.sleep(0.1) def close(self): self._icon.shutdown() def onSelectAlertSound(self): pass def onPlayAlert(self): pass def onStopAlert(self): pass def onOpenMapsFile(self): pass def onSelectPathOfExileDirectory(self): pass def onQuit(self): pass def _onSelectAlertSound(self, sysTray): self._callbackQueue.put(self.onSelectAlertSound) def _onPlayAlert(self, sysTray): self._callbackQueue.put(self.onPlayAlert) def _onStopAlert(self, sysTray): self._callbackQueue.put(self.onStopAlert) def _onOpenMapsFile(self, sysTray): self._callbackQueue.put(self.onOpenMapsFile) def _onSelectPathOfExileDirectory(self, sysTray): self._callbackQueue.put(self.onSelectPathOfExileDirectory) def _onQuit(self, sysTray): self._callbackQueue.put(sysTray.shutdown) self._callbackQueue.put(self.onQuit)