Преглед на файлове

wxGUI: Fix key binding issues on mac (#818 #1160)

Fixes #785 (backport of 5fd8c29)

The main problem with stock id buttons is that they all come with a key
binding. E.g. Close, Clear and Copy have the same key binding ctrl+c and
causes problems at least for mac. To remedy this issue we are using
subclassed versions of Button: ClearButton, CancelButton, ApplyButton).

I also made some effort to implement ESC to close dialog (attribute
table manager, mapcalc).

Note: for module dialogs (forms.py) and Map calculator I have replaced
ctrl+c to copy selected text, not to copy the whole command.

This also addresses a subsequent issue (#1156) introduced by 5fd8c29,
with an added check for wxPython version (backport of 1ee82f0).
nilason преди 4 години
родител
ревизия
e7fded2202

+ 9 - 3
gui/wxpython/dbmgr/manager.py

@@ -42,7 +42,7 @@ from core.gcmd import GMessage
 from core.debug import Debug
 from dbmgr.base import DbMgrBase
 from gui_core.widgets import GNotebook
-from gui_core.wrap import Button
+from gui_core.wrap import Button, ClearButton, CloseButton
 
 
 class AttributeManager(wx.Frame, DbMgrBase):
@@ -136,15 +136,21 @@ class AttributeManager(wx.Frame, DbMgrBase):
             wx.CallAfter(self.notebook.SetSelection, 0)  # select browse tab
 
         # buttons
-        self.btnClose = Button(parent=self.panel, id=wx.ID_CLOSE)
+        self.btnClose = CloseButton(parent=self.panel)
         self.btnClose.SetToolTip(_("Close Attribute Table Manager"))
         self.btnReload = Button(parent=self.panel, id=wx.ID_REFRESH)
         self.btnReload.SetToolTip(
             _("Reload currently selected attribute data"))
-        self.btnReset = Button(parent=self.panel, id=wx.ID_CLEAR)
+        self.btnReset = ClearButton(parent=self.panel)
         self.btnReset.SetToolTip(
             _("Reload all attribute data (drop current selection)"))
 
+        # bind closing to ESC
+        self.Bind(wx.EVT_MENU, self.OnCloseWindow, id=wx.ID_CANCEL)
+        accelTableList = [(wx.ACCEL_NORMAL, wx.WXK_ESCAPE, wx.ID_CANCEL)]
+        accelTable = wx.AcceleratorTable(accelTableList)
+        self.SetAcceleratorTable(accelTable)
+
         # events
         self.btnClose.Bind(wx.EVT_BUTTON, self.OnCloseWindow)
         self.btnReload.Bind(wx.EVT_BUTTON, self.OnReloadData)

+ 5 - 4
gui/wxpython/dbmgr/sqlbuilder.py

@@ -37,7 +37,8 @@ from grass.pydispatch.signal import Signal
 
 from core.gcmd import RunCommand, GError, GMessage
 from dbmgr.vinfo import CreateDbInfoDesc, VectorDBInfo, GetUnicodeValue
-from gui_core.wrap import Button, TextCtrl, StaticText, StaticBox
+from gui_core.wrap import ApplyButton, Button, ClearButton, CloseButton, \
+    TextCtrl, StaticText, StaticBox
 
 import grass.script as grass
 
@@ -128,11 +129,11 @@ class SQLBuilder(wx.Frame):
         #
         # buttons
         #
-        self.btn_clear = Button(parent=self.panel, id=wx.ID_CLEAR)
+        self.btn_clear = ClearButton(parent=self.panel)
         self.btn_clear.SetToolTip(_("Set SQL statement to default"))
-        self.btn_apply = Button(parent=self.panel, id=wx.ID_APPLY)
+        self.btn_apply = ApplyButton(parent=self.panel)
         self.btn_apply.SetToolTip(_("Apply SQL statement"))
-        self.btn_close = Button(parent=self.panel, id=wx.ID_CLOSE)
+        self.btn_close = CloseButton(parent=self.panel)
         self.btn_close.SetToolTip(_("Close the dialog"))
 
         self.btn_logic = {'is': ['=', ],

+ 10 - 17
gui/wxpython/gui_core/forms.py

@@ -105,8 +105,8 @@ from core.settings import UserSettings
 from gui_core.widgets import FloatValidator, GNotebook, FormNotebook, FormListbook
 from core.giface import Notification
 from gui_core.widgets import LayersList
-from gui_core.wrap import BitmapFromImage, Button, StaticText, StaticBox, SpinCtrl, \
-    CheckBox, BitmapButton, TextCtrl, NewId
+from gui_core.wrap import BitmapFromImage, Button, CloseButton, StaticText, \
+    StaticBox, SpinCtrl, CheckBox, BitmapButton, TextCtrl, NewId
 from core.debug import Debug
 
 wxUpdateDialog, EVT_DIALOG_UPDATE = NewEvent()
@@ -572,11 +572,7 @@ class TaskFrame(wx.Frame):
         # buttons
         btnsizer = wx.BoxSizer(orient=wx.HORIZONTAL)
         # cancel
-        if sys.platform == 'darwin':
-            # stock id automatically adds ctrl-c shortcut to close dialog
-            self.btn_cancel = Button(parent=self.panel, label=_("Close"))
-        else:
-            self.btn_cancel = Button(parent=self.panel, id=wx.ID_CLOSE)
+        self.btn_cancel = CloseButton(parent=self.panel)
         self.btn_cancel.SetToolTip(
             _("Close this window without executing the command (Ctrl+Q)"))
         btnsizer.Add(
@@ -586,9 +582,9 @@ class TaskFrame(wx.Frame):
             border=10)
         self.btn_cancel.Bind(wx.EVT_BUTTON, self.OnCancel)
         # bind closing to ESC and CTRL+Q
-        self.Bind(wx.EVT_MENU, self.OnCancel, id=wx.ID_CLOSE)
-        accelTableList = [(wx.ACCEL_NORMAL, wx.WXK_ESCAPE, wx.ID_CLOSE)]
-        accelTableList.append((wx.ACCEL_CTRL, ord('Q'), wx.ID_CLOSE))
+        self.Bind(wx.EVT_MENU, self.OnCancel, id=wx.ID_CANCEL)
+        accelTableList = [(wx.ACCEL_NORMAL, wx.WXK_ESCAPE, wx.ID_CANCEL)]
+        accelTableList.append((wx.ACCEL_CTRL, ord('Q'), wx.ID_CANCEL))
         # TODO: bind Ctrl-t for tile windows here (trac #2004)
 
         if self.get_dcmd is not None:  # A callback has been set up
@@ -622,17 +618,14 @@ class TaskFrame(wx.Frame):
             accelTableList.append((wx.ACCEL_CTRL, ord('R'), wx.ID_OK))
 
         # copy
-        if sys.platform == 'darwin':
-            # stock id automatically adds ctrl-c shortcut to copy command
-            self.btn_clipboard = Button(parent=self.panel, label=_("Copy"))
-        else:
-            self.btn_clipboard = Button(parent=self.panel, id=wx.ID_COPY)
+        self.btn_clipboard = Button(
+            parent=self.panel, id=wx.ID_ANY, label=_("Copy"))
         self.btn_clipboard.SetToolTip(
             _("Copy the current command string to the clipboard"))
         btnsizer.Add(self.btn_clipboard, proportion=0,
                      flag=wx.ALL | wx.ALIGN_CENTER,
                      border=10)
-        self.btn_clipboard.Bind(wx.EVT_BUTTON, self.OnCopy)
+        self.btn_clipboard.Bind(wx.EVT_BUTTON, self.OnCopyCommand)
 
         # help
         self.btn_help = Button(parent=self.panel, id=wx.ID_HELP)
@@ -875,7 +868,7 @@ class TaskFrame(wx.Frame):
         event = wxCmdAbort(aborted=True)
         wx.PostEvent(self._gconsole, event)
 
-    def OnCopy(self, event):
+    def OnCopyCommand(self, event):
         """Copy the command"""
         cmddata = wx.TextDataObject()
         # list -> string

+ 3 - 4
gui/wxpython/gui_core/goutput.py

@@ -37,7 +37,7 @@ from core.gconsole   import GConsole, \
     EVT_CMD_OUTPUT, EVT_CMD_PROGRESS, EVT_CMD_RUN, EVT_CMD_DONE, \
     Notification
 from gui_core.prompt import GPromptSTC
-from gui_core.wrap import Button, ToggleButton, StaticText, \
+from gui_core.wrap import Button, ClearButton, ToggleButton, StaticText, \
     StaticBox
 from core.settings import UserSettings
 from gui_core.widgets import SearchModuleWidget
@@ -155,10 +155,9 @@ class GConsoleWindow(wx.SplitterWindow):
                                     label=" %s " % cmdLabel)
 
         # buttons
-        self.btnOutputClear = Button(
-            parent=self.panelOutput, id=wx.ID_CLEAR)
+        self.btnOutputClear = ClearButton(parent=self.panelOutput)
         self.btnOutputClear.SetToolTip(_("Clear output window content"))
-        self.btnCmdClear = Button(parent=self.panelOutput, id=wx.ID_CLEAR)
+        self.btnCmdClear = ClearButton(parent=self.panelOutput)
         self.btnCmdClear.SetToolTip(_("Clear command prompt content"))
         self.btnOutputSave = Button(parent=self.panelOutput, id=wx.ID_SAVE)
         self.btnOutputSave.SetToolTip(

+ 47 - 0
gui/wxpython/gui_core/wrap.py

@@ -15,6 +15,7 @@ This program is free software under the GNU General Public License
 @author Anna Petrasova <kratochanna gmail.com>
 """
 
+import sys
 import wx
 import wx.lib.agw.floatspin as fs
 import wx.lib.colourselect as csel
@@ -178,6 +179,52 @@ class Button(wx.Button):
             wx.Button.SetToolTipString(self, tip)
 
 
+class ClearButton(Button):
+    """Wrapper around a Button with stock id wx.ID_CLEAR,
+    to disable default key binding on certain platforms"""
+    def __init__(self, *args, **kwargs):
+        Button.__init__(self, *args, **kwargs)
+        self.SetId(wx.ID_CLEAR)
+        if sys.platform == "darwin":
+            self.SetLabel(_("Clear"))
+        else:
+            self.SetLabel(_("&Clear"))
+
+
+class CancelButton(Button):
+    """Wrapper around a Button with stock id wx.ID_CANCEL, to disable
+    default key binding on certain platforms/wxpython versions"""
+    def __init__(self, *args, **kwargs):
+        Button.__init__(self, *args, **kwargs)
+        self.SetId(wx.ID_CANCEL)
+        if sys.platform == "darwin" and not CheckWxVersion([4, 1, 0]):
+            self.SetLabel(_("Cancel"))
+        else:
+            self.SetLabel(_("&Cancel"))
+
+class CloseButton(Button):
+    """Wrapper around a Close labeled Button with stock id wx.ID_CANCEL
+    to disable default key binding on certain platforms/wxpython versions"""
+    def __init__(self, *args, **kwargs):
+        Button.__init__(self, *args, **kwargs)
+        self.SetId(wx.ID_CANCEL)
+        if sys.platform == "darwin" and not CheckWxVersion([4, 1, 0]):
+            self.SetLabel(_("Close"))
+        else:
+            self.SetLabel(_("&Close"))
+
+class ApplyButton(Button):
+    """Wrapper around a Button with stock id wx.ID_APPLY,
+    to disable default key binding on certain platforms"""
+    def __init__(self, *args, **kwargs):
+        Button.__init__(self, *args, **kwargs)
+        self.SetId(wx.ID_APPLY)
+        if sys.platform == "darwin":
+            self.SetLabel(_("Apply"))
+        else:
+            self.SetLabel(_("&Apply"))
+
+
 class RadioButton(wx.RadioButton):
     """Wrapper around wx.RadioButton to have more control
     over the widget on different platforms/wxpython versions"""

+ 0 - 14
gui/wxpython/lmgr/frame.py

@@ -246,13 +246,6 @@ class GMFrame(wx.Frame):
 
         show_menu_errors(menu_errors)
 
-        # Enable copying to clipboard with cmd+c from console and python shell on macOS
-        # (default key binding will clear the console), trac #3008
-        if sys.platform == "darwin":
-            self.Bind(wx.EVT_MENU, self.OnCopyToClipboard, id=wx.ID_COPY)
-            self.accel_tbl = wx.AcceleratorTable([(wx.ACCEL_CTRL, ord("C"), wx.ID_COPY)])
-            self.SetAcceleratorTable(self.accel_tbl)
-
         # start with layer manager on top
         if self.currentPage:
             self.GetMapDisplay().Raise()
@@ -709,13 +702,6 @@ class GMFrame(wx.Frame):
                     return False
         return True
 
-    def OnCopyToClipboard(self, event):
-        """Copy selected text in shell to the clipboard"""
-        try:
-            wx.Window.FindFocus().Copy()
-        except:
-            pass
-
     def _switchPageHandler(self, event, notification):
         self._switchPage(notification=notification)
         event.Skip()

+ 14 - 6
gui/wxpython/lmgr/pyshell.py

@@ -29,7 +29,8 @@ from wx.py.version import VERSION
 import grass.script as grass
 from grass.script.utils import try_remove
 
-from gui_core.wrap import Button
+from gui_core.wrap import Button, ClearButton
+from core.globalvar import CheckWxVersion
 
 
 class PyShellWindow(wx.Panel):
@@ -44,14 +45,21 @@ class PyShellWindow(wx.Panel):
         self.intro = _("Welcome to wxGUI Interactive Python Shell %s") % VERSION + "\n\n" + \
             _("Type %s for more GRASS scripting related information.") % "\"help(grass)\"" + "\n" + \
             _("Type %s to add raster or vector to the layer tree.") % "\"AddLayer()\"" + "\n\n"
-        self.shell = PyShell(parent=self, id=wx.ID_ANY,
-                             introText=self.intro,
-                             locals={'grass': grass,
-                                     'AddLayer': self.AddLayer})
+
+        shellargs = dict(
+            parent=self,
+            id=wx.ID_ANY,
+            introText=self.intro,
+            locals={"grass": grass, "AddLayer": self.AddLayer},
+        )
+        # useStockId (available since wxPython 4.0.2) should be False on macOS
+        if sys.platform == "darwin" and CheckWxVersion([4, 0, 2]):
+            shellargs["useStockId"] = False
+        self.shell = PyShell(**shellargs)
 
         sys.displayhook = self._displayhook
 
-        self.btnClear = Button(self, wx.ID_CLEAR)
+        self.btnClear = ClearButton(self)
         self.btnClear.Bind(wx.EVT_BUTTON, self.OnClear)
         self.btnClear.SetToolTip(_("Delete all text from the shell"))
 

+ 3 - 19
gui/wxpython/modules/import_export.py

@@ -41,7 +41,7 @@ from core.gcmd import RunCommand, GMessage, GWarning
 from gui_core.forms import CmdPanel
 from gui_core.gselect import OgrTypeSelect, GdalSelect, SubGroupSelect
 from gui_core.widgets import LayersList, GListCtrl, GNotebook
-from gui_core.wrap import Button, StaticText, StaticBox
+from gui_core.wrap import Button, CloseButton, StaticText, StaticBox
 from core.utils import GetValidLayerName
 from core.settings import UserSettings, GetDisplayVectSettings
 
@@ -114,14 +114,12 @@ class ImportDialog(wx.Dialog):
         # buttons
         #
         # cancel
-        self.btn_close = Button(parent=self.panel, id=wx.ID_CLOSE)
+        self.btn_close = CloseButton(parent=self.panel)
         self.btn_close.SetToolTip(_("Close dialog"))
         self.btn_close.Bind(wx.EVT_BUTTON, self.OnClose)
         # run
         self.btn_run = Button(
-            parent=self.panel,
-            id=wx.ID_OK,
-            label=_("&Import"))
+            parent=self.panel, id=wx.ID_OK, label=_("&Import"))
         self.btn_run.SetToolTip(_("Import selected layers"))
         self.btn_run.SetDefault()
         self.btn_run.Bind(wx.EVT_BUTTON, self.OnRun)
@@ -137,13 +135,6 @@ class ImportDialog(wx.Dialog):
 
         self.createSettingsPage()
 
-        # Enable copying to clipboard with cmd+c from dialog on macOS
-        # (default key binding will close the dialog), trac #3592
-        if sys.platform == "darwin":
-            self.Bind(wx.EVT_MENU, self.OnCopyToClipboard, id=wx.ID_COPY)
-            self.accel_tbl = wx.AcceleratorTable([(wx.ACCEL_CTRL, ord("C"), wx.ID_COPY)])
-            self.SetAcceleratorTable(self.accel_tbl)
-
     def createSettingsPage(self):
 
         self._blackList = {
@@ -317,13 +308,6 @@ class ImportDialog(wx.Dialog):
         """Do what has to be done after importing"""
         pass
 
-    def OnCopyToClipboard(self, event):
-        """Copy selected text in dialog to the clipboard"""
-        try:
-            wx.Window.FindFocus().Copy()
-        except:
-            pass
-
     def _getLayersToReprojetion(self, projMatch_idx, grassName_idx):
         """If there are layers with different projection from loation projection,
            show dialog to user to explicitly select layers which will be reprojected..."""

+ 14 - 8
gui/wxpython/modules/mcalc_builder.py

@@ -28,8 +28,8 @@ from core.giface import StandaloneGrassInterface
 from gui_core.gselect import Select
 from gui_core.forms import GUI
 from gui_core.widgets import IntegerValidator
-from gui_core.wrap import Button, TextCtrl, StaticText, \
-    StaticBox
+from gui_core.wrap import Button, ClearButton, CloseButton, TextCtrl, \
+    StaticText, StaticBox
 from core.settings import UserSettings
 
 
@@ -150,7 +150,7 @@ class MapCalcFrame(wx.Frame):
         #
         # Buttons
         #
-        self.btn_clear = Button(parent=self.panel, id=wx.ID_CLEAR)
+        self.btn_clear = ClearButton(parent=self.panel)
         self.btn_help = Button(parent=self.panel, id=wx.ID_HELP)
         self.btn_run = Button(
             parent=self.panel,
@@ -158,13 +158,14 @@ class MapCalcFrame(wx.Frame):
             label=_("&Run"))
         self.btn_run.SetForegroundColour(wx.Colour(35, 142, 35))
         self.btn_run.SetDefault()
-        self.btn_close = Button(parent=self.panel, id=wx.ID_CLOSE)
+        self.btn_close = CloseButton(parent=self.panel)
         self.btn_save = Button(parent=self.panel, id=wx.ID_SAVE)
         self.btn_save.SetToolTip(_('Save expression to file'))
         self.btn_load = Button(parent=self.panel, id=wx.ID_ANY,
                                label=_("&Load"))
         self.btn_load.SetToolTip(_('Load expression from file'))
-        self.btn_copy = Button(parent=self.panel, id=wx.ID_COPY)
+        self.btn_copy = Button(
+            parent=self.panel, id=wx.ID_ANY, label=_("Copy"))
         self.btn_copy.SetToolTip(
             _("Copy the current command string to the clipboard"))
 
@@ -324,9 +325,8 @@ class MapCalcFrame(wx.Frame):
         self.btn_help.Bind(wx.EVT_BUTTON, self.OnHelp)
         self.btn_save.Bind(wx.EVT_BUTTON, self.OnSaveExpression)
         self.btn_load.Bind(wx.EVT_BUTTON, self.OnLoadExpression)
-        self.btn_copy.Bind(wx.EVT_BUTTON, self.OnCopy)
+        self.btn_copy.Bind(wx.EVT_BUTTON, self.OnCopyCommand)
 
-        # self.mapselect.Bind(wx.EVT_TEXT, self.OnSelectTextEvt)
         self.mapselect.Bind(wx.EVT_TEXT, self.OnSelect)
         self.function.Bind(wx.EVT_COMBOBOX, self._return_funct)
         self.function.Bind(wx.EVT_TEXT_ENTER, self.OnSelect)
@@ -337,6 +337,12 @@ class MapCalcFrame(wx.Frame):
         self.randomSeed.Bind(wx.EVT_CHECKBOX, self.OnSeedFlag)
         self.randomSeedText.Bind(wx.EVT_TEXT, self.OnUpdateStatusBar)
 
+        # bind closing to ESC
+        self.Bind(wx.EVT_MENU, self.OnClose, id=wx.ID_CANCEL)
+        accelTableList = [(wx.ACCEL_NORMAL, wx.WXK_ESCAPE, wx.ID_CANCEL)]
+        accelTable = wx.AcceleratorTable(accelTableList)
+        self.SetAcceleratorTable(accelTable)
+
         self._layout()
 
         self.SetMinSize(self.panel.GetBestSize())
@@ -776,7 +782,7 @@ class MapCalcFrame(wx.Frame):
 
         dlg.Destroy()
 
-    def OnCopy(self, event):
+    def OnCopyCommand(self, event):
         command = self._getCommand()
         cmddata = wx.TextDataObject()
         cmddata.SetText(command)