DEADSOFTWARE

gui: redesign menu
[d2df-editor.git] / src / editor / f_main.pas
index ca14ebe46ef777a30433bdacd9c2f93eaf09b5fb..790a646a334b9ff69700d3237260483e6baf427b 100644 (file)
@@ -18,18 +18,27 @@ type
     lLoad: TLabel;
   // Главное меню:
     MainMenu: TMainMenu;
+  // Apple menu:
+    miApple: TMenuItem;
+    miAppleAbout: TMenuItem;
+    miAppleLine0: TMenuItem;
+    miApplePref: TMenuItem;
+    miAppleLine1: TMenuItem;
   // "Файл":
     miMenuFile: TMenuItem;
     miNewMap: TMenuItem;
     miOpenMap: TMenuItem;
     miSaveMap: TMenuItem;
     miSaveMapAs: TMenuItem;
+    miMacRecentSubMenu: TMenuItem;
+    miMacRecentClear: TMenuItem;
     miOpenWadMap: TMenuItem;
     miLine1: TMenuItem;
     miReopenMap: TMenuItem;
     miSaveMiniMap: TMenuItem;
     miDeleteMap: TMenuItem;
     miPackMap: TMenuItem;
+    miWinRecent: TMenuItem;
     miLine2: TMenuItem;
     miExit: TMenuItem;
   // "Правка":
@@ -44,11 +53,8 @@ type
     miLine5: TMenuItem;
     miToFore: TMenuItem;
     miToBack: TMenuItem;
-  // "Инструменты":
-    miMenuTools: TMenuItem;
-    miSnapToGrid: TMenuItem;
-    miMiniMap: TMenuItem;
-    miSwitchGrid: TMenuItem;
+  // View menu:
+    miMenuView: TMenuItem;
     miShowEdges: TMenuItem;
     miLayers: TMenuItem;
     miLayer1: TMenuItem;
@@ -60,24 +66,29 @@ type
     miLayer7: TMenuItem;
     miLayer8: TMenuItem;
     miLayer9: TMenuItem;
+    miViewLine1: TMenuItem;
+    miMiniMap: TMenuItem;
+    miViewLine2: TMenuItem;
+    miMapPreview: TMenuItem;
+    miSnapToGrid: TMenuItem;
+    miSwitchGrid: TMenuItem;
+    miLine6: TMenuItem;
+    miOptions: TMenuItem;
+    miMapOptions: TMenuItem;
   // "Сервис":
     miMenuService: TMenuItem;
     miCheckMap: TMenuItem;
     miOptimmization: TMenuItem;
-    miMapPreview: TMenuItem;
     miTestMap: TMenuItem;
-  // "Настройка":
-    miMenuSettings: TMenuItem;
-    miMapOptions: TMenuItem;
-    miLine6: TMenuItem;
-    miOptions: TMenuItem;
-    miLine7: TMenuItem;
-    miMapTestSettings: TMenuItem;
+  // Window menu:
+    miMenuWindow: TMenuItem;
+    miMacMinimize: TMenuItem;
+    miMacZoom: TMenuItem;
   // "Справка":
     miMenuHelp: TMenuItem;
     miAbout: TMenuItem;
   // Скрытый пункт меню для Ctrl+Tab:
-    miHidden1: TMenuItem;
+    miMenuHidden: TMenuItem;
     minexttab: TMenuItem;
 
   // Панель инструментов:
@@ -85,6 +96,10 @@ type
     pbLoad: TProgressBar;
     pLoadProgress: TPanel;
     RenderPanel: TOpenGLControl;
+    Separator1: TMenuItem;
+    miMacRecentEnd: TMenuItem;
+    miWinRecentStart: TMenuItem;
+    Separator2: TMenuItem;
     tbNewMap: TToolButton;
     tbOpenMap: TToolButton;
     tbSaveMap: TToolButton;
@@ -109,9 +124,6 @@ type
     miLayerP7: TMenuItem;
     miLayerP8: TMenuItem;
     miLayerP9: TMenuItem;
-  // Всплывающее меню для кнопки теста карты:
-    pmMapTest: TPopupMenu;
-    miMapTestPMSet: TMenuItem;
 
   // Панель карты:
     PanelMap: TPanel;
@@ -124,6 +136,7 @@ type
   // Панель применения свойств:
     PanelPropApply: TPanel;
     bApplyProperty: TButton;
+    MapTestTimer: TTimer;
   // Редактор свойств объектов:
     vleObjectProperty: TValueListEditor;
 
@@ -208,9 +221,13 @@ type
     procedure FormDropFiles(Sender: TObject; const FileNames: array of String);
     procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
     procedure FormResize(Sender: TObject);
+    procedure FormWindowStateChange(Sender: TObject);
+    procedure miMacRecentClearClick(Sender: TObject);
+    procedure miMacZoomClick(Sender: TObject);
     procedure lbTextureListClick(Sender: TObject);
     procedure lbTextureListDrawItem(Control: TWinControl; Index: Integer;
       ARect: TRect; State: TOwnerDrawState);
+    procedure miMacMinimizeClick(Sender: TObject);
     procedure miReopenMapClick(Sender: TObject);
     procedure RenderPanelMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
     procedure RenderPanelMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
@@ -218,6 +235,7 @@ type
     procedure RenderPanelPaint(Sender: TObject);
     procedure RenderPanelResize(Sender: TObject);
     procedure Splitter1Moved(Sender: TObject);
+    procedure MapTestCheck(Sender: TObject);
     procedure vleObjectPropertyEditButtonClick(Sender: TObject);
     procedure vleObjectPropertyApply(Sender: TObject);
     procedure vleObjectPropertyGetPickList(Sender: TObject; const KeyName: String; Values: TStrings);
@@ -243,8 +261,6 @@ type
     procedure miSaveMiniMapClick(Sender: TObject);
     procedure bClearTextureClick(Sender: TObject);
     procedure miPackMapClick(Sender: TObject);
-    procedure aRecentFileExecute(Sender: TObject);
-    procedure miMapTestSettingsClick(Sender: TObject);
     procedure miTestMapClick(Sender: TObject);
     procedure sbVerticalScroll(Sender: TObject; ScrollCode: TScrollCode;
       var ScrollPos: Integer);
@@ -263,6 +279,7 @@ type
   private
     procedure Draw();
     procedure OnIdle(Sender: TObject; var Done: Boolean);
+    procedure RefillRecentMenu (menu: TMenuItem; start: Integer; fmt: AnsiString);
   public
     procedure RefreshRecentMenu();
     procedure OpenMapFile(FileName: String);
@@ -286,7 +303,7 @@ const
 
 var
   MainForm: TMainForm;
-  EditorDir: String;
+  StartMap: String;
   OpenedMap: String;
   OpenedWAD: String;
 
@@ -337,11 +354,11 @@ uses
   f_options, e_graphics, e_log, GL, Math,
   f_mapoptions, g_basic, f_about, f_mapoptimization,
   f_mapcheck, f_addresource_texture, g_textures,
-  f_activationtype, f_keys, wadreader,
+  f_activationtype, f_keys, wadreader, fileutil,
   MAPREADER, f_selectmap, f_savemap, WADEDITOR, MAPDEF,
   g_map, f_saveminimap, f_addresource, CONFIG, f_packmap,
-  f_addresource_sound, f_maptest, f_choosetype,
-  g_language, f_selectlang, ClipBrd, g_resources;
+  f_addresource_sound, f_choosetype,
+  g_language, f_selectlang, ClipBrd, g_resources, g_options;
 
 const
   UNDO_DELETE_PANEL   = 1;
@@ -442,8 +459,10 @@ var
   LastMovePoint: Types.TPoint;
   MouseLDown: Boolean;
   MouseRDown: Boolean;
+  MouseMDown: Boolean;
   MouseLDownPos: Types.TPoint;
   MouseRDownPos: Types.TPoint;
+  MouseMDownPos: Types.TPoint;
 
   SelectFlag: Byte = SELECTFLAG_NONE;
   MouseAction: Byte = MOUSEACTION_NONE;
@@ -455,6 +474,8 @@ var
 
   UndoBuffer: Array of Array of TUndoRec = nil;
 
+  MapTestProcess: TProcessUTF8;
+  MapTestFile: String;
 
 {$R *.lfm}
 
@@ -706,14 +727,8 @@ begin
     MapOffset.X := MapOffset.X - rx;
     MapOffset.Y := MapOffset.Y - ry;
   // Выход за границы:
-    if MapOffset.X < MainForm.sbHorizontal.Min then
-      MapOffset.X := MainForm.sbHorizontal.Min;
-    if MapOffset.Y < MainForm.sbVertical.Min then
-      MapOffset.Y := MainForm.sbVertical.Min;
-    if MapOffset.X > MainForm.sbHorizontal.Max then
-      MapOffset.X := MainForm.sbHorizontal.Max;
-    if MapOffset.Y > MainForm.sbVertical.Max then
-      MapOffset.Y := MainForm.sbVertical.Max;
+    MapOffset.X := EnsureRange(MapOffset.X, MainForm.sbHorizontal.Min, MainForm.sbHorizontal.Max);
+    MapOffset.Y := EnsureRange(MapOffset.Y, MainForm.sbVertical.Min, MainForm.sbVertical.Max);
   // Кратно 16:
   //  MapOffset.X := Normalize16(MapOffset.X);
   //  MapOffset.Y := Normalize16(MapOffset.Y);
@@ -1843,7 +1858,7 @@ end;
 
 procedure ErrorMessageBox(str: String);
 begin
-  MessageBox(0, PChar(str), PChar(_lc[I_MSG_ERROR]),
+  Application.MessageBox(PChar(str), PChar(_lc[I_MSG_ERROR]),
              MB_ICONINFORMATION or MB_OK or MB_DEFBUTTON1);
 end;
 
@@ -1934,7 +1949,6 @@ begin
   if aWAD = _lc[I_WAD_SPECIAL_MAP] then
     begin // Файл карты
       g_ProcessResourceStr(OpenedMap, @fn, nil, nil);
-    //FileName := EditorDir+'maps\'+ExtractFileName(fn);
       FileName := fn;
       ResourceName := ':'+SectionName+'\'+aTex;
     end
@@ -1946,7 +1960,7 @@ begin
       end
     else
       begin // Внешний WAD
-        FileName := EditorDir+'wads/'+aWAD;
+        FileName := WadsDir + DirectorySeparator + aWAD;
         ResourceName := aWAD+':'+SectionName+'\'+aTex;
       end;
 
@@ -2245,6 +2259,7 @@ procedure SwitchMap();
 begin
   ShowMap := not ShowMap;
   MainForm.tbShowMap.Down := ShowMap;
+  MainForm.miMiniMap.Checked := ShowMap;
 end;
 
 procedure ShowEdges();
@@ -2253,6 +2268,7 @@ begin
     drEdge[3] := 255
   else
     drEdge[3] := gAlphaEdge;
+  MainForm.miShowEdges.Checked := drEdge[3] <> 255;
 end;
 
 function SelectedTexture(): String;
@@ -2550,60 +2566,105 @@ end;
 //Закончились вспомогательные процедуры
 //----------------------------------------
 
-procedure TMainForm.RefreshRecentMenu();
-var
-  i: Integer;
-  MI: TMenuItem;
+type
+  TRecentHandler = class
+    private
+      FForm: TMainForm;
+      FPath: String;
+    public
+      constructor Create (form: TMainForm; path: String);
+      procedure Execute (Sender: TObject);
+  end;
+
+constructor TRecentHandler.Create (form: TMainForm; path: String);
 begin
-// Лишние запомненные карты:
-  while RecentFiles.Count > RecentCount do
-    RecentFiles.Delete(RecentFiles.Count-1);
+  Assert(form <> nil);
+  FForm := form;
+  FPath := path;
+end;
+
+procedure TRecentHandler.Execute (Sender: TObject);
+  var fn: AnsiString;
+begin
+  fn := g_ExtractWadName(FPath);
+  if FileExists(fn) then
+    OpenMap(fn, g_ExtractFilePathName(FPath))
+  else
+    Application.MessageBox('', 'File not available anymore', MB_OK);
+//  if Application.MessageBox(PChar(_lc[I_MSG_DEL_RECENT_PROMT]), PChar(_lc[I_MSG_DEL_RECENT]), MB_ICONQUESTION or MB_YESNO) = idYes then
+//  begin
+//    RecentFiles.Delete(n);
+//    RefreshRecentMenu();
+//  end;
+end;
 
-// Лишние строки меню:
-  while MainMenu.Items[0].Count > RECENT_FILES_MENU_START do
-    MainMenu.Items[0].Delete(MainMenu.Items[0].Count-1);
+procedure TMainForm.RefillRecentMenu (menu: TMenuItem; start: Integer; fmt: AnsiString);
+  var i: Integer; MI: TMenuItem; cb: TMethod; h: TRecentHandler; s: AnsiString;
+begin
+  Assert(menu <> nil);
+  Assert(start >= 0);
+  Assert(start <= menu.Count);
 
-// Отделение списка карт от строки "Выход":
-  if RecentFiles.Count > 0 then
+  // clear all recent entries from menu
+  i := start;
+  while i < menu.Count do
   begin
-    MI := TMenuItem.Create(MainMenu.Items[0]);
-    MI.Caption := '-';
-    MainMenu.Items[0].Add(MI);
+    MI := menu.Items[i];
+    cb := TMethod(MI.OnClick);
+    if cb.Code = @TRecentHandler.Execute then
+    begin
+      // this is recent menu entry
+      // remove it and free callback handler
+      h := TRecentHandler(cb.Data);
+      menu.Delete(i);
+      MI.Free();
+      h.Free();
+    end
+    else
+      Inc(i);
   end;
 
-// Добавление в меню списка запомненных карт:
-  for i := 0 to RecentFiles.Count-1 do
+  // fill with a new ones
+  for i := 0 to RecentFiles.Count - 1 do
   begin
-    MI := TMenuItem.Create(MainMenu.Items[0]);
-    MI.Caption := IntToStr(i+1) + '  ' + RecentFiles[i];
-    MI.OnClick := aRecentFileExecute;
-    MainMenu.Items[0].Add(MI);
+    s := RecentFiles[i];
+    h := TRecentHandler.Create(self, s);
+    MI := TMenuItem.Create(menu);
+    MI.Caption := Format(fmt, [i + 1, g_ExtractWadNameNoPath(s), g_ExtractFilePathName(s)]);
+    MI.OnClick := h.Execute;
+    menu.Insert(start + i, MI);
   end;
 end;
 
-procedure TMainForm.aRecentFileExecute(Sender: TObject);
-var
-  n: Integer;
-  fn, s: String;
-begin
-  s := LowerCase((Sender as TMenuItem).Caption);
-  Delete(s, Pos('&', s), 1);
-  s := Trim(Copy(s, 1, 2));
-  n := StrToIntDef(s, 0) - 1;
-  if (n >= 0) and (n <= RecentFiles.Count) then
+procedure TMainForm.RefreshRecentMenu();
+  var start: Integer;
+begin
+  while RecentFiles.Count > RecentCount do
+    RecentFiles.Delete(RecentFiles.Count - 1);
+
+  if miMacRecentSubMenu.Visible then
   begin
-    fn := g_ExtractWadName(RecentFiles[n]);
-    if FileExists(fn) then
-    begin
-      s := g_ExtractFilePathName(RecentFiles[n]);
-      OpenMap(fn, s)
-    end
-    else if MessageBox(0, PChar(_lc[I_MSG_DEL_RECENT_PROMT]), PChar(_lc[I_MSG_DEL_RECENT]), MB_ICONQUESTION or MB_YESNO) = idYes then
-    begin
-      RecentFiles.Delete(n);
-      RefreshRecentMenu();
-    end
-  end
+    // Reconstruct OSX-like recent list
+    RefillRecentMenu(miMacRecentSubMenu, 0, '%1:s - %2:s');
+    miMacRecentEnd.Enabled := RecentFiles.Count <> 0;
+    miMacRecentEnd.Visible := RecentFiles.Count <> 0;
+  end;
+
+  if miWinRecentStart.Visible then
+  begin
+    // Reconstruct Windows-like recent list
+    start := miMenuFile.IndexOf(miWinRecent);
+    if start < 0 then start := miMenuFile.Count else start := start + 1;
+    RefillRecentMenu(miMenuFile, start, '%0:d %1:s:%2:s');
+    miWinRecent.Enabled := False;
+    miWinRecent.Visible := RecentFiles.Count = 0;
+  end;
+end;
+
+procedure TMainForm.miMacRecentClearClick(Sender: TObject);
+begin
+  RecentFiles.Clear();
+  RefreshRecentMenu();
 end;
 
 procedure TMainForm.aEditorOptionsExecute(Sender: TObject);
@@ -2621,10 +2682,10 @@ var
   config: TConfig;
 begin
   ID := 0;
-  g_ReadResource(EditorDir + 'data/game.wad', 'FONTS', cfgres, cfgdata, cfglen);
+  g_ReadResource(GameWad, 'FONTS', cfgres, cfgdata, cfglen);
   if cfgdata <> nil then
   begin
-    if not g_CreateTextureWAD('FONT_STD', EditorDir + 'data/game.wad:FONTS\' + texture) then
+    if not g_CreateTextureWAD('FONT_STD', GameWad + ':FONTS\' + texture) then
       e_WriteLog('ERROR ERROR ERROR', MSG_WARNING);
 
     config := TConfig.CreateMem(cfgdata, cfglen);
@@ -2652,9 +2713,69 @@ var
 begin
   Randomize();
 
-  EditorDir := ExtractFilePath(Application.ExeName);
-
-  e_InitLog(EditorDir+'Editor.log', WM_NEWFILE);
+  {$IFDEF DARWIN}
+    miApple.Enabled := True;
+    miApple.Visible := True;
+    miMacRecentSubMenu.Enabled := True;
+    miMacRecentSubMenu.Visible := True;
+    miWinRecentStart.Enabled := False;
+    miWinRecentStart.Visible := False;
+    miWinRecent.Enabled := False;
+    miWinRecent.Visible := False;
+    miLine2.Enabled := False;
+    miLine2.Visible := False;
+    miExit.Enabled := False;
+    miExit.Visible := False;
+    miOptions.Enabled := False;
+    miOptions.Visible := False;
+    miMenuWindow.Enabled := True;
+    miMenuWindow.Visible := True;
+    miAbout.Enabled := False;
+    miAbout.Visible := False;
+  {$ELSE}
+    miApple.Enabled := False;
+    miApple.Visible := False;
+    miMacRecentSubMenu.Enabled := False;
+    miMacRecentSubMenu.Visible := False;
+    miWinRecentStart.Enabled := True;
+    miWinRecentStart.Visible := True;
+    miWinRecent.Enabled := True;
+    miWinRecent.Visible := True;
+    miLine2.Enabled := True;
+    miLine2.Visible := True;
+    miExit.Enabled := True;
+    miExit.Visible := True;
+    miOptions.Enabled := True;
+    miOptions.Visible := True;
+    miMenuWindow.Enabled := False;
+    miMenuWindow.Visible := False;
+    miAbout.Enabled := True;
+    miAbout.Visible := True;
+  {$ENDIF}
+
+  miNewMap.ShortCut := ShortCut(VK_N, [ssModifier]);
+  miOpenMap.ShortCut := ShortCut(VK_O, [ssModifier]);
+  miSaveMap.ShortCut := ShortCut(VK_S, [ssModifier]);
+  {$IFDEF DARWIN}
+    miSaveMapAs.ShortCut := ShortCut(VK_S, [ssModifier, ssShift]);
+    miReopenMap.ShortCut := ShortCut(VK_F5, [ssModifier]);
+  {$ENDIF}
+  miUndo.ShortCut := ShortCut(VK_Z, [ssModifier]);
+  miCopy.ShortCut := ShortCut(VK_C, [ssModifier]);
+  miCut.ShortCut := ShortCut(VK_X, [ssModifier]);
+  miPaste.ShortCut := ShortCut(VK_V, [ssModifier]);
+  miSelectAll.ShortCut := ShortCut(VK_A, [ssModifier]);
+  miToFore.ShortCut := ShortCut(VK_LCL_CLOSE_BRACKET, [ssModifier]);
+  miToBack.ShortCut := ShortCut(VK_LCL_OPEN_BRACKET, [ssModifier]);
+  {$IFDEF DARWIN}
+    miMapOptions.Shortcut := ShortCut(VK_P, [ssModifier, ssAlt]);
+    selectall1.Shortcut := ShortCut(VK_A, [ssModifier, ssAlt]);
+  {$ENDIF}
+
+  e_WriteLog('Doom 2D: Forever Editor version ' + EDITOR_VERSION, MSG_NOTIFY);
+  e_WriteLog('Build date: ' + EDITOR_BUILDDATE + ' ' + EDITOR_BUILDTIME, MSG_NOTIFY);
+  e_WriteLog('Build hash: ' + g_GetBuildHash(), MSG_NOTIFY);
+  e_WriteLog('Build by: ' + g_GetBuilderName(), MSG_NOTIFY);
 
   slInvalidTextures := TStringList.Create;
 
@@ -2674,7 +2795,7 @@ begin
   OpenedMap := '';
   OpenedWAD := '';
 
-  config := TConfig.CreateFile(EditorDir+'Editor.cfg');
+  config := TConfig.CreateFile(CfgFileName);
 
   if config.ReadInt('Editor', 'XPos', -1) = -1 then
     Position := poDesktopCenter
@@ -2729,8 +2850,8 @@ begin
     DotSize := 2
   else
     DotSize := 1;
-  OpenDialog.InitialDir := config.ReadStr('Editor', 'LastOpenDir', EditorDir);
-  SaveDialog.InitialDir := config.ReadStr('Editor', 'LastSaveDir', EditorDir);
+  OpenDialog.InitialDir := config.ReadStr('Editor', 'LastOpenDir', MapsDir);
+  SaveDialog.InitialDir := config.ReadStr('Editor', 'LastSaveDir', MapsDir);
 
   s := config.ReadStr('Editor', 'Language', '');
   gLanguage := s;
@@ -2738,6 +2859,24 @@ begin
   Compress := config.ReadBool('Editor', 'Compress', True);
   Backup := config.ReadBool('Editor', 'Backup', True);
 
+  TestGameMode := config.ReadStr('TestRun', 'GameMode', 'DM');
+  TestLimTime := config.ReadStr('TestRun', 'LimTime', '0');
+  TestLimScore := config.ReadStr('TestRun', 'LimScore', '0');
+  TestOptionsTwoPlayers := config.ReadBool('TestRun', 'TwoPlayers', False);
+  TestOptionsTeamDamage := config.ReadBool('TestRun', 'TeamDamage', False);
+  TestOptionsAllowExit := config.ReadBool('TestRun', 'AllowExit', True);
+  TestOptionsWeaponStay := config.ReadBool('TestRun', 'WeaponStay', False);
+  TestOptionsMonstersDM := config.ReadBool('TestRun', 'MonstersDM', False);
+  TestMapOnce := config.ReadBool('TestRun', 'MapOnce', False);
+  {$IF DEFINED(DARWIN)}
+    TestD2dExe := config.ReadStr('TestRun', 'ExeDrawin', GameExeFile);
+  {$ELSEIF DEFINED(WINDOWS)}
+    TestD2dExe := config.ReadStr('TestRun', 'ExeWindows', GameExeFile);
+  {$ELSE}
+    TestD2dExe := config.ReadStr('TestRun', 'ExeUnix', GameExeFile);
+  {$ENDIF}
+  TestD2DArgs := config.ReadStr('TestRun', 'Args', '');
+
   RecentCount := config.ReadInt('Editor', 'RecentCount', 5);
   if RecentCount > 10 then
     RecentCount := 10;
@@ -2747,7 +2886,11 @@ begin
   RecentFiles := TStringList.Create();
   for i := 0 to RecentCount-1 do
   begin
-    s := config.ReadStr('RecentFiles', IntToStr(i+1), '');
+    {$IFDEF WINDOWS}
+      s := config.ReadStr('RecentFilesWin', IntToStr(i), '');
+    {$ELSE}
+      s := config.ReadStr('RecentFilesUnix', IntToStr(i), '');
+    {$ENDIF}
     if s <> '' then
       RecentFiles.Add(s);
   end;
@@ -3110,6 +3253,24 @@ begin
   MapOffset.Y := -sbVertical.Position;
 end;
 
+procedure TMainForm.FormWindowStateChange(Sender: TObject);
+  {$IFDEF DARWIN}
+    var e: Boolean;
+  {$ENDIF}
+begin
+  {$IFDEF DARWIN}
+    // deactivate all menus when main window minimized
+    e := self.WindowState <> wsMinimized;
+    miMenuFile.Enabled := e;
+    miMenuEdit.Enabled := e;
+    miMenuView.Enabled := e;
+    miMenuService.Enabled := e;
+    miMenuWindow.Enabled := e;
+    miMenuHelp.Enabled := e;
+    miMenuHidden.Enabled := e;
+  {$ENDIF}
+end;
+
 procedure SelectNextObject(X, Y: Integer; ObjectType: Byte; ID: DWORD);
 var
   j, j_max: Integer;
@@ -3598,6 +3759,16 @@ begin
         end;
   end; // if Button = mbRight
 
+  if Button = mbMiddle then // Middle Mouse Button
+  begin
+    SetCapture(RenderPanel.Handle);
+    RenderPanel.Cursor := crSize;
+  end;
+
+  MouseMDown := Button = mbMiddle;
+  if MouseMDown then
+    MouseMDownPos := Mouse.CursorPos;
+
   MouseRDown := Button = mbRight;
   if MouseRDown then
     MouseRDownPos := MousePos;
@@ -3635,6 +3806,8 @@ begin
     MouseLDown := False;
   if Button = mbRight then
     MouseRDown := False;
+  if Button = mbMiddle then
+    MouseMDown := False;
 
   DrawRect := nil;
   ResizeType := RESIZETYPE_NONE;
@@ -3944,7 +4117,7 @@ begin
         MouseAction := MOUSEACTION_NONE;
       end;
     end // if Button = mbLeft...
-  else // Right Mouse Button:
+  else if Button = mbRight then // Right Mouse Button:
     begin
       if MouseAction = MOUSEACTION_NOACTION then
       begin
@@ -3955,6 +4128,7 @@ begin
     // Объект передвинут или изменен в размере:
       if MouseAction in [MOUSEACTION_MOVEOBJ, MOUSEACTION_RESIZE] then
       begin
+        RenderPanel.Cursor := crDefault;
         MouseAction := MOUSEACTION_NONE;
         FillProperty();
         Exit;
@@ -4007,6 +4181,12 @@ begin
         SelectObjects(pcObjects.ActivePageIndex+1);
 
       FillProperty();
+    end
+
+  else // Middle Mouse Button
+    begin
+      RenderPanel.Cursor := crDefault;
+      ReleaseCapture();
     end;
 end;
 
@@ -4091,12 +4271,8 @@ begin
         MousePos.Y := Round((-MapOffset.Y + Y) / sY) * sY + MapOffset.Y;
       end;
 
-// Изменение размера закончилось - ставим обычный курсор:
-  if ResizeType = RESIZETYPE_NONE then
-    RenderPanel.Cursor := crDefault;
-
 // Зажата только правая кнопка мыши:
-  if (not MouseLDown) and (MouseRDown) then
+  if (not MouseLDown) and (MouseRDown) and (not MouseMDown) then
   begin
   // Рисуем прямоугольник выделения:
     if MouseAction = MOUSEACTION_NONE then
@@ -4145,7 +4321,7 @@ begin
   end;
 
 // Зажата только левая кнопка мыши:
-  if (not MouseRDown) and (MouseLDown) then
+  if (not MouseRDown) and (MouseLDown) and (not MouseMDown) then
   begin
   // Рисуем прямоугольник планирования панели:
     if MouseAction in [MOUSEACTION_DRAWPANEL,
@@ -4185,6 +4361,18 @@ begin
       end;
   end;
 
+// Only Middle Mouse Button is pressed
+  if (not MouseLDown) and (not MouseRDown) and (MouseMDown) then
+  begin
+    MapOffset.X := -EnsureRange(-MapOffset.X + MouseMDownPos.X - Mouse.CursorPos.X,
+                                sbHorizontal.Min, sbHorizontal.Max);
+    sbHorizontal.Position := -MapOffset.X;
+    MapOffset.Y := -EnsureRange(-MapOffset.Y + MouseMDownPos.Y - Mouse.CursorPos.Y,
+                                sbVertical.Min, sbVertical.Max);
+    sbVertical.Position := -MapOffset.Y;
+    MouseMDownPos := Mouse.CursorPos;
+  end;
+
 // Клавиши мыши не зажаты:
   if (not MouseRDown) and (not MouseLDown) then
     DrawRect := nil;
@@ -4196,7 +4384,7 @@ end;
 
 procedure TMainForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
 begin
-  CanClose := MessageBox(0, PChar(_lc[I_MSG_EXIT_PROMT]),
+  CanClose := Application.MessageBox(PChar(_lc[I_MSG_EXIT_PROMT]),
                          PChar(_lc[I_MSG_EXIT]),
                          MB_ICONQUESTION or MB_YESNO or
                          MB_DEFBUTTON1) = idYes;
@@ -4210,9 +4398,10 @@ end;
 procedure TMainForm.FormDestroy(Sender: TObject);
 var
   config: TConfig;
+  s: AnsiString;
   i: Integer;
 begin
-  config := TConfig.CreateFile(EditorDir+'Editor.cfg');
+  config := TConfig.CreateFile(CfgFileName);
 
   if WindowState <> wsMaximized then
   begin
@@ -4244,14 +4433,18 @@ begin
   config.WriteInt('Editor', 'MonsterRectAlpha', gAlphaMonsterRect);
   config.WriteInt('Editor', 'AreaRectAlpha', gAlphaAreaRect);
 
-  for i := 0 to RecentCount-1 do
-    if i < RecentFiles.Count then
-      config.WriteStr('RecentFiles', IntToStr(i+1), RecentFiles[i])
-    else
-      config.WriteStr('RecentFiles', IntToStr(i+1), '');
+  for i := 0 to RecentCount - 1 do
+  begin
+    if i < RecentFiles.Count then s := RecentFiles[i] else s := '';
+    {$IFDEF WINDOWS}
+      config.WriteStr('RecentFilesWin', IntToStr(i), s);
+    {$ELSE}
+      config.WriteStr('RecentFilesUnix', IntToStr(i), s);
+    {$ENDIF}
+  end;
   RecentFiles.Free();
 
-  config.SaveFile(EditorDir+'Editor.cfg');
+  config.SaveFile(CfgFileName);
   config.Free();
 
   slInvalidTextures.Free;
@@ -4277,6 +4470,22 @@ begin
   FormResize(Sender);
 end;
 
+procedure TMainForm.MapTestCheck(Sender: TObject);
+begin
+  if MapTestProcess <> nil then
+  begin
+    if MapTestProcess.Running = false then
+    begin
+      if MapTestProcess.ExitCode <> 0 then
+        Application.MessageBox(PChar(_lc[I_MSG_EXEC_ERROR]), 'FIXME', MB_OK or MB_ICONERROR);
+      SysUtils.DeleteFile(MapTestFile);
+      MapTestFile := '';
+      FreeAndNil(MapTestProcess);
+      tbTestMap.Enabled := True;
+    end;
+  end;
+end;
+
 procedure TMainForm.aMapOptionsExecute(Sender: TObject);
 var
   ResName: String;
@@ -4691,6 +4900,21 @@ begin
   end;
 end;
 
+procedure TMainForm.miMacMinimizeClick(Sender: TObject);
+begin
+  self.WindowState := wsMinimized;
+  self.FormWindowStateChange(Sender);
+end;
+
+procedure TMainForm.miMacZoomClick(Sender: TObject);
+begin
+  if self.WindowState = wsMaximized then
+    self.WindowState := wsNormal
+  else
+    self.WindowState := wsMaximized;
+  self.FormWindowStateChange(Sender);
+end;
+
 procedure TMainForm.miReopenMapClick(Sender: TObject);
 var
   FileName, Resource: String;
@@ -4698,7 +4922,7 @@ begin
   if OpenedMap = '' then
     Exit;
 
-  if MessageBox(0, PChar(_lc[I_MSG_REOPEN_MAP_PROMT]),
+  if Application.MessageBox(PChar(_lc[I_MSG_REOPEN_MAP_PROMT]),
   PChar(_lc[I_MENU_FILE_REOPEN]), MB_ICONQUESTION or MB_YESNO) <> idYes then
     Exit;
 
@@ -5128,7 +5352,7 @@ begin
                 if vleObjectProperty.Values[_lc[I_PROP_TR_MUSIC_ACT]] = _lc[I_PROP_TR_MUSIC_ON] then
                   Data.MusicAction := 1
                 else
-                  Data.MusicAction := 2;
+                  Data.MusicAction := 0;
               end;
 
             TRIGGER_PUSH:
@@ -5328,7 +5552,7 @@ begin
   if i = -1 then
     Exit;
 
-  if MessageBox(0, PChar(Format(_lc[I_MSG_DEL_TEXTURE_PROMT],
+  if Application.MessageBox(PChar(Format(_lc[I_MSG_DEL_TEXTURE_PROMT],
                                 [SelectedTexture()])),
                 PChar(_lc[I_MSG_DEL_TEXTURE]),
                 MB_ICONQUESTION or MB_YESNO or
@@ -5354,10 +5578,7 @@ end;
 
 procedure TMainForm.aNewMapExecute(Sender: TObject);
 begin
-  if (MessageBox(0, PChar(_lc[I_MSG_CLEAR_MAP_PROMT]),
-                 PChar(_lc[I_MSG_CLEAR_MAP]),
-                 MB_ICONQUESTION or MB_YESNO or
-                 MB_DEFBUTTON1) = mrYes) then
+  if Application.MessageBox(PChar(_lc[I_MSG_CLEAR_MAP_PROMT]), PChar(_lc[I_MSG_CLEAR_MAP]), MB_ICONQUESTION or MB_YESNO or MB_DEFBUTTON1) = mrYes then
     FullClear();
 end;
 
@@ -6201,17 +6422,18 @@ begin
   MainForm.ActiveControl := RenderPanel;
 
 // Язык:
-  if gLanguage = '' then
+  if (gLanguage = '') and not (fsModal in SelectLanguageForm.FormState) then
   begin
     lang := SelectLanguageForm.ShowModal();
     case lang of
       1:   gLanguage := LANGUAGE_ENGLISH;
-      else gLanguage := LANGUAGE_RUSSIAN;
+      2:   gLanguage := LANGUAGE_RUSSIAN;
+      else gLanguage := LANGUAGE_ENGLISH;
     end;
 
-    config := TConfig.CreateFile(EditorDir+'Editor.cfg');
+    config := TConfig.CreateFile(CfgFileName);
     config.WriteStr('Editor', 'Language', gLanguage);
-    config.SaveFile(EditorDir+'Editor.cfg');
+    config.SaveFile(CfgFileName);
     config.Free();
   end;
 
@@ -6240,18 +6462,17 @@ begin
     Exit;
 
   MapName := SelectMapForm.lbMapList.Items[SelectMapForm.lbMapList.ItemIndex];
-  if MessageBox(0, PChar(Format(_lc[I_MSG_DELETE_MAP_PROMT], [MapName, OpenDialog.FileName])), PChar(_lc[I_MSG_DELETE_MAP]), MB_ICONQUESTION or MB_YESNO or MB_DEFBUTTON2) <> mrYes then
+  if Application.MessageBox(PChar(Format(_lc[I_MSG_DELETE_MAP_PROMT], [MapName, OpenDialog.FileName])), PChar(_lc[I_MSG_DELETE_MAP]), MB_ICONQUESTION or MB_YESNO or MB_DEFBUTTON2) <> mrYes then
     Exit;
 
   g_DeleteResource(FileName, '', MapName, res);
   if res <> 0 then
   begin
-    MessageBox(0, PChar('Cant delete map res=' + IntToStr(res)), PChar('Map not deleted!'), MB_ICONINFORMATION or MB_OK or MB_DEFBUTTON1);
+    Application.MessageBox(PChar('Cant delete map res=' + IntToStr(res)), PChar('Map not deleted!'), MB_ICONINFORMATION or MB_OK or MB_DEFBUTTON1);
     Exit
   end;
 
-  MessageBox(
-    0,
+  Application.MessageBox(
     PChar(Format(_lc[I_MSG_MAP_DELETED_PROMT], [MapName])),
     PChar(_lc[I_MSG_MAP_DELETED]),
     MB_ICONINFORMATION or MB_OK or MB_DEFBUTTON1
@@ -6489,6 +6710,7 @@ begin
 end;
 
 procedure TMainForm.OnIdle(Sender: TObject; var Done: Boolean);
+  var f: AnsiString;
 begin
   // FIXME: this is a shitty hack
   if not gDataLoaded then
@@ -6504,6 +6726,12 @@ begin
     MainForm.FormResize(nil);
   end;
   Draw();
+  if StartMap <> '' then
+  begin
+    f := StartMap;
+    StartMap := '';
+    OpenMap(f, '');
+  end;
 end;
 
 procedure TMainForm.miMapPreviewClick(Sender: TObject);
@@ -6663,11 +6891,6 @@ begin
   PackMapForm.ShowModal();
 end;
 
-procedure TMainForm.miMapTestSettingsClick(Sender: TObject);
-begin
-  MapTestForm.ShowModal();
-end;
-
 type SSArray = array of String;
 
 function ParseString (Str: AnsiString): SSArray;
@@ -6705,34 +6928,36 @@ end;
 
 procedure TMainForm.miTestMapClick(Sender: TObject);
 var
-  mapWAD, mapToRun, tempWAD: String;
+  newWAD, oldWAD, tempMap, ext: String;
   args: SSArray;
   opt: LongWord;
   time, i: Integer;
   proc: TProcessUTF8;
   res: Boolean;
 begin
-  mapToRun := '';
-  if OpenedMap <> '' then
-  begin
-    // Указываем текущую карту для теста:
-    g_ProcessResourceStr(OpenedMap, @mapWAD, nil, @mapToRun);
-    mapToRun := mapWAD + ':\' + mapToRun;
-    mapToRun := ExtractRelativePath(ExtractFilePath(TestD2dExe) + 'maps/', mapToRun);
-  end;
+  // Ignore while map testing in progress
+  if MapTestProcess <> nil then
+    Exit;
+
   // Сохраняем временную карту:
   time := 0;
   repeat
-    mapWAD := ExtractFilePath(TestD2dExe) + Format('maps/temp%.4d.wad', [time]);
+    newWAD := Format('%s/temp%.4d', [MapsDir, time]);
     Inc(time);
-  until not FileExists(mapWAD);
-  tempWAD := mapWAD + ':\' + TEST_MAP_NAME;
-  SaveMap(tempWAD);
-
-  tempWAD := ExtractRelativePath(ExtractFilePath(TestD2dExe) + 'maps/', tempWAD);
-// Если карта не была открыта, указываем временную в качестве текущей:
-  if mapToRun = '' then
-    mapToRun := tempWAD;
+  until not FileExists(newWAD);
+  if OpenedMap <> '' then
+  begin
+    oldWad := g_ExtractWadName(OpenedMap);
+    newWad := newWad + ExtractFileExt(oldWad);
+    if CopyFile(oldWad, newWad) = false then
+      e_WriteLog('MapTest: unable to copy [' + oldWad + '] to [' + newWad + ']', MSG_WARNING)
+  end
+  else
+  begin
+    newWad := newWad + '.wad'
+  end;
+  tempMap := newWAD + ':\' + TEST_MAP_NAME;
+  SaveMap(tempMap);
 
 // Опции игры:
   opt := 32 + 64;
@@ -6750,10 +6975,13 @@ begin
 // Запускаем:
   proc := TProcessUTF8.Create(nil);
   proc.Executable := TestD2dExe;
+  {$IFDEF DARWIN}
+    // TODO: get real executable name from Info.plist
+    if LowerCase(ExtractFileExt(TestD2dExe)) = '.app' then
+      proc.Executable := TestD2dExe + DirectorySeparator + 'Contents' + DirectorySeparator + 'MacOS' + DirectorySeparator + 'Doom2DF';
+  {$ENDIF}
   proc.Parameters.Add('-map');
-  proc.Parameters.Add(mapToRun);
-  proc.Parameters.Add('-testmap');
-  proc.Parameters.Add(tempWAD);
+  proc.Parameters.Add(tempMap);
   proc.Parameters.Add('-gm');
   proc.Parameters.Add(TestGameMode);
   proc.Parameters.Add('-limt');
@@ -6778,19 +7006,16 @@ begin
   end;
   if res then
   begin
-    Application.Minimize();
-    proc.WaitOnExit();
-  end;
-  if (not res) or (proc.ExitCode < 0) then
+    tbTestMap.Enabled := False;
+    MapTestFile := newWAD;
+    MapTestProcess := proc;
+  end
+  else
   begin
-    MessageBox(0, 'FIXME',
-               PChar(_lc[I_MSG_EXEC_ERROR]),
-               MB_OK or MB_ICONERROR);
+    Application.MessageBox(PChar(_lc[I_MSG_EXEC_ERROR]), 'FIXME', MB_OK or MB_ICONERROR);
+    SysUtils.DeleteFile(newWAD);
+    proc.Free();
   end;
-  proc.Free();
-
-  SysUtils.DeleteFile(mapWAD);
-  Application.Restore();
 end;
 
 procedure TMainForm.sbVerticalScroll(Sender: TObject;