def on_drop(self, event: 'tkinter.Event[tkinter.Misc]') -> None: self._window.withdraw() if not (_is_on_window(event) or isinstance(self._dragged_state, SpecialState)): log.info("popping off a tab") tab, state = self._dragged_state required_size = (tab.winfo_reqwidth(), tab.winfo_reqheight()) message = (type(tab), state, required_size, event.x_root, event.y_root) with tempfile.NamedTemporaryFile(delete=False) as file: log.debug("writing dumped state to '%s'", file) pickle.dump(message, file) settings.save() # let the new process use up-to-date settings # Empty string (aka "load from current working directory") becomes # the first item of sys.path when using -m, which isn't great if # your current working directory contains e.g. queue.py (issue 31). # # However, if the currently running porcupine imports from current # working directory (e.g. python3 -m porcupine), then the subprocess # should do that too. if os.getcwd() in sys.path: args = [sys.executable, '-m', 'porcupine'] else: python_code = ''' import sys if sys.path[0] == '': del sys.path[0] from porcupine.__main__ import main main() ''' args = [sys.executable, '-c', python_code] args.append('--without-plugins') args.append(','.join({ info.name for info in pluginloader.plugin_infos if info.status == pluginloader.Status.DISABLED_ON_COMMAND_LINE } | { # these plugins are not suitable for popups # TODO: geometry and restart stuff don't get saved 'restart', 'geometry', })) if get_parsed_args().verbose: args.append('--verbose') process = subprocess.Popen( args, env={ **os.environ, 'PORCUPINE_POPPINGTABS_STATE_FILE': file.name }) log.debug("started subprocess with PID %d", process.pid) get_tab_manager().close_tab(tab) # don't exit python until the subprocess exits, also log stuff threading.Thread(target=self._waiter_thread, args=[process]).start() self._dragged_state = NOT_DRAGGING
def setup() -> None: get_tab_manager().add_tab_callback(on_new_tab)
def main() -> None: # Arguments are parsed in two steps: # 1. only the arguments needed for importing plugins # 2. everything else # # Between those steps, plugins get a chance to add more command-line options. parser = argparse.ArgumentParser( epilog=_EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter, add_help=False, # help in step 1 wouldn't show options added by plugins ) parser.add_argument( '--version', action='version', version=("Porcupine %s" % porcupine_version), help="display the Porcupine version number and exit") parser.add_argument( '-v', '--verbose', action='store_true', help=("print all logging messages to stderr, only warnings and errors " "are printed by default (but all messages always go to a log " "file in %s as well)" % _logs.LOG_DIR)) parser.add_argument( '--print-plugindir', action=_PrintPlugindirAction, help="find out where to install custom plugins") plugingroup = parser.add_argument_group("plugin loading options") plugingroup.add_argument( '--no-plugins', action='store_false', dest='use_plugins', help=("don't load any plugins, this is useful for " "understanding how much can be done with plugins")) plugingroup.add_argument( '--without-plugins', metavar='PLUGINS', default='', help=("don't load PLUGINS (see --print-plugindir), " "e.g. --without-plugins=highlight disables syntax highlighting, " "multiple plugin names can be given comma-separated")) args_parsed_in_first_step, junk = parser.parse_known_args() dirs.makedirs() _logs.setup(args_parsed_in_first_step.verbose) settings.init_enough_for_using_disabled_plugins_list() if args_parsed_in_first_step.use_plugins: if args_parsed_in_first_step.without_plugins: disable_list = args_parsed_in_first_step.without_plugins.split(',') else: disable_list = [] pluginloader.import_plugins(disable_list) bad_disables = set(disable_list) - {info.name for info in pluginloader.plugin_infos} if bad_disables: one_of_them, *the_rest = bad_disables parser.error(f"--without-plugins: no plugin named {one_of_them!r}") parser.add_argument('--help', action='help', help="show this message") pluginloader.run_setup_argument_parser_functions(parser) parser.add_argument( 'files', metavar='FILES', nargs=argparse.ZERO_OR_MORE, help="open these files when Porcupine starts, - means stdin") plugingroup.add_argument( '--shuffle-plugins', action='store_true', help=("respect setup_before and setup_after, but otherwise setup the " "plugins in a random order instead of sorting by name " "alphabetically, useful for making sure that your plugin's " "setup_before and setup_after define everything needed; usually " "plugins are not shuffled in order to make the UI consistent")) args = parser.parse_args() _state.init(args) settings.init_the_rest_after_initing_enough_for_using_disabled_plugins_list() menubar._init() pluginloader.run_setup_functions(args.shuffle_plugins) get_main_window().event_generate('<<PluginsLoaded>>') tabmanager = get_tab_manager() for path_string in args.files: if path_string == '-': # don't close stdin so it's possible to do this: # # $ porcu - - # bla bla bla # ^D # bla bla # ^D tab = tabs.FileTab(tabmanager, content=sys.stdin.read()) else: tab = tabs.FileTab.open_file(tabmanager, pathlib.Path(path_string)) tabmanager.add_tab(tab) try: get_main_window().mainloop() finally: settings.save() log.info("exiting Porcupine successfully")
def _on_tab_closed(self, event): if not get_tab_manager().tabs: self._message.place(relx=0.5, rely=0.5, anchor='center')
def hide_all_message_labels(event: tkinter.Event[tkinter.Misc]) -> None: if event.widget is get_main_window( ): # Tk and Toplevel events need this check for tab in get_tab_manager().tabs(): if isinstance(tab, tabs.FileTab): tab.textwidget.event_generate("<<UnderlinerHidePopup>>")
def enable_or_disable(junk_event=None): tab = porcupine.get_tab_manager().select() action.enabled = isinstance(tab, tabtypes)
def _on_tab_closed(self, event=None): if not get_tab_manager().tabs: self._frame.pack(fill='both', expand=True)
def edit_it(): path = os.path.join(dirs.configdir, 'filetypes.ini') manager = porcupine.get_tab_manager() manager.add_tab(tabs.FileTab.open_file(manager, path)) _dialog.withdraw()
def _on_tab_closed(self, junk: object = None) -> None: if not get_tab_manager().tabs(): self._frame.pack(fill="both", expand=True)
def run_stuff(self) -> None: if self._run_stuff_once(): get_tab_manager().after(50, self.run_stuff)
def main() -> None: # Arguments are parsed in two steps: # 1. only the arguments needed for importing plugins # 2. everything else # # Between those steps, plugins get a chance to add more command-line options. parser = argparse.ArgumentParser( epilog=_EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter, add_help=False, # help in step 1 wouldn't show options added by plugins ) parser.add_argument( "--version", action="version", version=f"Porcupine {porcupine_version}", help="display the Porcupine version number and exit", ) parser.add_argument( "--print-plugindir", action=_PrintPlugindirAction, help="find out where to install custom plugins", ) verbose_group = parser.add_mutually_exclusive_group() verbose_group.add_argument( "-v", "--verbose", dest="verbose_logger", action="store_const", const="", help=("print all logging messages to stderr, only warnings and errors " "are printed by default (but all messages always go to a log " "file as well)"), ) verbose_group.add_argument( "--verbose-logger", help=("increase verbosity for just one logger only, e.g. " "--verbose-logger=porcupine.plugins.highlight " "to see messages from highlight plugin"), ) plugingroup = parser.add_argument_group("plugin loading options") plugingroup.add_argument( "--no-plugins", action="store_false", dest="use_plugins", help=("don't load any plugins, this is useful for " "understanding how much can be done with plugins"), ) plugingroup.add_argument( "--without-plugins", metavar="PLUGINS", default="", help=("don't load PLUGINS (see --print-plugindir), " "e.g. --without-plugins=highlight disables syntax highlighting, " "multiple plugin names can be given comma-separated"), ) args_parsed_in_first_step, junk = parser.parse_known_args() pathlib.Path(dirs.user_cache_dir).mkdir(parents=True, exist_ok=True) (pathlib.Path(dirs.user_config_dir) / "plugins").mkdir(parents=True, exist_ok=True) pathlib.Path(dirs.user_log_dir).mkdir(parents=True, exist_ok=True) _logs.setup(args_parsed_in_first_step.verbose_logger) settings.init_enough_for_using_disabled_plugins_list() if args_parsed_in_first_step.use_plugins: if args_parsed_in_first_step.without_plugins: disable_list = args_parsed_in_first_step.without_plugins.split(",") else: disable_list = [] pluginloader.import_plugins(disable_list) bad_disables = set(disable_list) - { info.name for info in pluginloader.plugin_infos } if bad_disables: one_of_them, *the_rest = bad_disables parser.error(f"--without-plugins: no plugin named {one_of_them!r}") parser.add_argument("--help", action="help", help="show this message") pluginloader.run_setup_argument_parser_functions(parser) parser.add_argument( "files", metavar="FILES", nargs=argparse.ZERO_OR_MORE, help="open these files when Porcupine starts, - means stdin", ) plugingroup.add_argument( "--shuffle-plugins", action="store_true", help=("respect setup_before and setup_after, but otherwise setup the " "plugins in a random order instead of sorting by name " "alphabetically, useful for making sure that your plugin's " "setup_before and setup_after define everything needed; usually " "plugins are not shuffled in order to make the UI consistent"), ) args = parser.parse_args() _state.init(args) # Prevent showing up a not-ready-yet root window to user get_main_window().withdraw() settings.init_the_rest_after_initing_enough_for_using_disabled_plugins_list( ) menubar._init() pluginloader.run_setup_functions(args.shuffle_plugins) tabmanager = get_tab_manager() for path_string in args.files: if path_string == "-": # don't close stdin so it's possible to do this: # # $ porcu - - # bla bla bla # ^D # bla bla # ^D tab = tabs.FileTab(tabmanager, content=sys.stdin.read()) else: tab = tabs.FileTab.open_file(tabmanager, pathlib.Path(path_string)) tabmanager.add_tab(tab) get_main_window().deiconify() try: get_main_window().mainloop() finally: settings.save() log.info("exiting Porcupine successfully")
def main(): if os.path.basename(sys.argv[0]) == '__main__.py': prog = '%s -m porcupine' % utils.short_python_command else: prog = os.path.basename(sys.argv[0]) # argparse default parser = argparse.ArgumentParser( prog=prog, epilog=_EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('-v', '--version', action='version', version=("Porcupine %s" % porcupine.__version__), help="display the Porcupine version number and exit") parser.add_argument('--print-plugindir', action=_PrintPlugindirAction, help="find out where to install custom plugins") parser.add_argument( 'files', metavar='FILES', action=_ExtendAction, # FIXME: this uses the system-default encoding :/ nargs=argparse.ZERO_OR_MORE, type=argparse.FileType("r"), help="open these files when Porcupine starts, - means stdin") parser.add_argument( '-n', '--new-file', dest='files', action='append_const', const=None, help='create a "New File" tab, may be given multiple times') plugingroup = parser.add_argument_group("plugin loading options") plugingroup.add_argument( '--no-plugins', action='store_false', dest='yes_plugins', help=("don't load any plugins, this is useful for " "understanding how much can be done with plugins")) plugingroup.add_argument( '--without-plugin', metavar='PLUGIN', action='append', default=[], help=("don't load PLUGIN, e.g. --without-plugin=highlight " "runs Porcupine without syntax highlighting")) plugingroup.add_argument( '--shuffle-plugins', action='store_true', help=("respect setup_before and setup_after, but otherwise setup the " "plugins in a random order instead of sorting by name " "alphabetically")) parser.add_argument( '--verbose', action='store_true', help=("print all logging messages to stderr, only warnings and errors " "are printed by default")) args = parser.parse_args() filelist = [] for file in args.files: if file is sys.stdin: # don't close stdin so it's possible to do this: # # $ porcupine - - # bla bla bla # ^D # bla bla # ^D filelist.append((None, file.read())) elif file is None: # -n or --new-file was used filelist.append((None, '')) else: with file: filelist.append((os.path.abspath(file.name), file.read())) porcupine.init(verbose_logging=args.verbose) if args.yes_plugins: plugin_names = pluginloader.find_plugins() log.info("found %d plugins", len(plugin_names)) for name in args.without_plugin: if name in plugin_names: plugin_names.remove(name) else: log.warning("no plugin named %r, cannot load without it", name) pluginloader.load(plugin_names, shuffle=args.shuffle_plugins) tabmanager = get_tab_manager() for path, content in filelist: tabmanager.add_tab(tabs.FileTab(tabmanager, content, path)) porcupine.run() log.info("exiting Porcupine successfully")
def tabs_from_state(): for tab_class, state in states: tab = tab_class.from_state(get_tab_manager(), state) get_tab_manager().add_tab(tab)
def on_menu_toggled(wrap_var: tkinter.BooleanVar, *junk: object) -> None: tab = get_tab_manager().select() assert isinstance(tab, tabs.FileTab) tab.textwidget.config(wrap=("word" if wrap_var.get() else "none"))
def setup(): actions.add_command( "Games/Tetris", lambda: get_tab_manager().add_tab(TetrisTab(get_tab_manager())))
def setup() -> None: displayer = WelcomeMessageDisplayer() get_tab_manager().bind("<Configure>", displayer.update_wraplen, add=True) get_tab_manager().add_tab_callback(displayer.on_new_tab)
def _add_any_action(path, kind, callback_or_choices, binding, var, *, filetype_names=None, tabtypes=None): if path.startswith('/') or path.endswith('/'): raise ValueError("action paths must not start or end with /") if filetype_names is not None and tabtypes is not None: # python raises TypeError when it comes to invalid arguments raise TypeError("only one of filetype_names and tabtypes can be used") if path in _actions: raise RuntimeError("there's already an action with path %r" % path) # event_generate must be before setting action.enabled, this way # plugins get a chance to do something to the new action before it's # disabled action = _Action(path, kind, callback_or_choices, binding, var) _actions[path] = action porcupine.get_main_window().event_generate('<<NewAction>>', data=path) if tabtypes is not None or filetype_names is not None: if tabtypes is not None: tabtypes = tuple( # None is the only type(None) object type(None) if cls is None else cls for cls in tabtypes) def enable_or_disable(junk_event=None): tab = porcupine.get_tab_manager().select() action.enabled = isinstance(tab, tabtypes) if filetype_names is not None: # the noqa comment is needed because flake8 thinks this is # a "redefinition of unused 'enable_or_disable'" def enable_or_disable(junk_event=None): # noqa tab = porcupine.get_tab_manager().select() if isinstance(tab, tabs.FileTab): action.enabled = tab.filetype.name in filetype_names else: action.enabled = False def on_new_tab(event): tab = event.data_widget if isinstance(tab, tabs.FileTab): tab.bind('<<FiletypeChanged>>', enable_or_disable, add=True) utils.bind_with_data(porcupine.get_tab_manager(), '<<NewTab>>', on_new_tab, add=True) enable_or_disable() porcupine.get_tab_manager().bind('<<NotebookTabChanged>>', enable_or_disable, add=True) # TODO: custom keyboard bindings with a config file or something if binding is not None: assert kind in {'command', 'yesno'}, repr(kind) def bind_callback(event): if action.enabled: if kind == 'command': action.callback() if kind == 'yesno': action.var.set(not action.var.get()) # try to allow binding keys that are used for other # things by default return 'break' # TODO: display a warning if it's already bound porcupine.get_main_window().bind(binding, bind_callback, add=True) return action
def select_current_file(tree: DirectoryTree, event: object) -> None: tab = get_tab_manager().select() if isinstance(tab, tabs.FileTab) and tab.path is not None: tree.select_file(tab.path)
def enable_or_disable(junk_event=None): # noqa tab = porcupine.get_tab_manager().select() if isinstance(tab, tabs.FileTab): action.enabled = tab.filetype.name in filetype_names else: action.enabled = False
def play_tetris(): tab = TetrisTab(get_tab_manager()) tab.new_game() get_tab_manager().add_tab(tab)
def __init__(self): self._message = ttk.Frame(get_tab_manager()) self._message.place(relx=0.5, rely=0.5, anchor='center') ttk.Label(self._message, text="Welcome to Porcupine!\n", font=('', 16, '')).pack() ttk.Label(self._message, text=MESSAGE, font=('', 14, '')).pack()
def close_if_not_closed(junk): if tab in get_tab_manager().tabs: get_tab_manager().close_tab(tab)
def setup(): displayer = WelcomeMessageDisplayer() get_tab_manager().bind('<Configure>', displayer.update_wraplen, add=True) utils.bind_with_data(get_tab_manager(), '<<NewTab>>', displayer.on_new_tab, add=True)
def _add_any_action(path, kind, callback_or_choices, binding, var, *, filetype_names=None, tabtypes=None): if path.startswith('/') or path.endswith('/'): raise ValueError("action paths must not start or end with /") if filetype_names is not None and tabtypes is not None: # python raises TypeError when it comes to invalid arguments raise TypeError("only one of filetype_names and tabtypes can be used") if path in _actions: raise RuntimeError("there's already an action with path %r" % path) # event_generate must be before setting action.enabled, this way # plugins get a chance to do something to the new action before it's # disabled action = _Action(path, kind, callback_or_choices, binding, var) _actions[path] = action porcupine.get_main_window().event_generate('<<NewAction>>', data=path) if tabtypes is not None or filetype_names is not None: if tabtypes is not None: tabtypes = tuple( # None is the only type(None) object type(None) if cls is None else cls for cls in tabtypes) def enable_or_disable(junk_event=None): tab = porcupine.get_tab_manager().select() action.enabled = isinstance(tab, tabtypes) if filetype_names is not None: # the noqa comment is needed because flake8 thinks this is # a "redefinition of unused 'enable_or_disable'" def enable_or_disable(junk_event=None): # noqa tab = porcupine.get_tab_manager().select() if isinstance(tab, tabs.FileTab): action.enabled = tab.filetype.name in filetype_names else: action.enabled = False def on_new_tab(event): tab = event.data_widget if isinstance(tab, tabs.FileTab): tab.bind('<<FiletypeChanged>>', enable_or_disable, add=True) utils.bind_with_data(porcupine.get_tab_manager(), '<<NewTab>>', on_new_tab, add=True) enable_or_disable() porcupine.get_tab_manager().bind('<<NotebookTabChanged>>', enable_or_disable, add=True) # TODO: custom keyboard bindings with a config file or something if binding is not None: assert kind in {'command', 'yesno'}, repr(kind) def bind_callback(event): if action.enabled: if kind == 'command': action.callback() if kind == 'yesno': action.var.set(not action.var.get()) # try to allow binding keys that are used for other # things by default return 'break' # TODO: display a warning if it's already bound? widget = porcupine.get_main_window() # any widget would do widget.bind_all(binding, bind_callback, add=True) # text widgets are tricky, by default they insert a newline on ctrl+o, # and i discovered how it works in a Tcl session: # # wish8.6 [~]bind Text <Control-o> # # if {!$tk_strictMotif} { # %W insert insert \n # %W mark set insert insert-1c # } # # preventing that is simple as binding it to nothing, and then they'll # do the bind_all thing as usual: # # wish8.6 [~]bind Text <Control-o> # wish8.6 [~]bind Text <Control-o> ;# empty string is returned # wish8.6 [~] # # this is done to all action bindings instead of just <Control-o> to # avoid any issues with other bindings widget.tk.call('bind', 'Text', binding, '') return action
def play_tetris(): manager = porcupine.get_tab_manager() manager.add_tab(TetrisTab(manager))
def open_irc(): irc_core = connectdialog.run(get_main_window()) if irc_core is not None: # not cancelled get_tab_manager().add_tab(IrcTab(get_tab_manager(), irc_core))
def tabmanager(porcusession): assert not get_tab_manager().tabs(), "something hasn't cleaned up its tabs" yield get_tab_manager() for tab in get_tab_manager().tabs(): get_tab_manager().close_tab(tab)
def find() -> None: tab = get_tab_manager().select() assert isinstance(tab, tabs.FileTab) if tab not in finders: finders[tab] = Finder(tab.bottom_frame, tab.textwidget) finders[tab].show()
def setup(): utils.bind_with_data(get_tab_manager(), '<<NewTab>>', on_new_tab, add=True)
def open_file(path, content): if (path, content) != (None, None): tabmanager = porcupine.get_tab_manager() tabmanager.add_tab(tabs.FileTab(tabmanager, content, path))