1 /*
2   FXiTe - The Free eXtensIble Text Editor
3   Copyright (c) 2009-2012 Jeffrey Pohlmeyer <yetanothergeek@gmail.com>
4 
5   This program is free software; you can redistribute it and/or modify it
6   under the terms of the GNU General Public License version 3 as
7   published by the Free Software Foundation.
8 
9   This software is distributed in the hope that it will be useful,
10   but WITHOUT ANY WARRANTY; without even the implied warranty of
11   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   GNU General Public License for more details.
13 
14   You should have received a copy of the GNU General Public License
15   along with this program; if not, write to the Free Software
16   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17 */
18 
19 
20 #include <cctype>
21 #include <fxkeys.h>
22 #include <fx.h>
23 #include "sl.h"
24 
25 #include "menuspec.h"
26 #include "compat.h"
27 
28 #include "shmenu.h"
29 
30 #define MAX_DEPTH 16         // Maximum recursion level
31 #define MAX_ENTRIES_PER 32   // Maximum entries per menu pane
32 #define MAX_ENTRIES_ALL 512  // Maximum total entries per UserMenu object
33 
34 FXDEFMAP(UserMenu) UserMenuMap[] = { };
35 
36 FXIMPLEMENT(UserMenu,FXObject,UserMenuMap,ARRAYNUMBER(UserMenuMap));
37 
38 
39 
40 class UsrMnuCmd: public FXMenuCommand {
41   FXDECLARE(UsrMnuCmd);
UsrMnuCmd()42   UsrMnuCmd(){}
43 public:
UsrMnuCmd(FXComposite * p,const FXString & text,FXObject * tgt=NULL,FXSelector sel=0)44   UsrMnuCmd( FXComposite *p,
45              const FXString &text, FXObject *tgt=NULL, FXSelector sel=0):FXMenuCommand(p,text,NULL,tgt,sel){}
46 
onButtonRelease(FXObject * o,FXSelector sel,void * p)47   long onButtonRelease(FXObject*o,FXSelector sel,void*p) {
48     FXbool active=isActive();
49     if(!isEnabled()) { return 0; }
50     getParent()->handle(this,FXSEL(SEL_COMMAND,ID_UNPOST),NULL);
51     if(active && target){ target->tryHandle(this,FXSEL(SEL_COMMAND,message),(void*)(FXuval)2); }
52     return 1;
53   }
onKeyRelease(FXObject * o,FXSelector sel,void * p)54   long onKeyRelease(FXObject*o,FXSelector sel,void* p){
55     FXEvent* ev=(FXEvent*)p;
56     if(isEnabled() && (flags&FLAG_PRESSED)){
57       if(ev->code==KEY_space || ev->code==KEY_KP_Space || ev->code==KEY_Return || ev->code==KEY_KP_Enter) {
58         flags&=~FLAG_PRESSED;
59         getParent()->handle(this,FXSEL(SEL_COMMAND,ID_UNPOST),NULL);
60         if(target) target->tryHandle(this,FXSEL(SEL_COMMAND,message),(void*)(FXuval)((ev->state&CONTROLMASK)?2:1));
61         return 1;
62         }
63       }
64     return 0;
65   }
ValidateTBarCmd()66   void ValidateTBarCmd() { MenuMgr::ValidateUsrTBarCmd(this); }
~UsrMnuCmd()67   ~UsrMnuCmd() {
68     MenuMgr::InvalidateUsrTBarCmd(this);
69     free(getUserData());
70   }
71 };
72 
73 
74 
75 FXDEFMAP(UsrMnuCmd) MyCmdMap[] = {
76   FXMAPFUNC(SEL_KEYRELEASE,0,UsrMnuCmd::onKeyRelease),
77   FXMAPFUNC(SEL_RIGHTBUTTONRELEASE,0,UsrMnuCmd::onButtonRelease)
78 };
79 
FXIMPLEMENT(UsrMnuCmd,FXMenuCommand,MyCmdMap,ARRAYNUMBER (MyCmdMap))80 FXIMPLEMENT(UsrMnuCmd,FXMenuCommand,MyCmdMap,ARRAYNUMBER(MyCmdMap))
81 
82 
83 
84 UserMenu::UserMenu(FXMenuPane *owner,
85   const FXString &label, const FXString &dirname, char tag, FXObject *tgt, FXSelector sel, const char**flags)
86 {
87   target=(FXWindow*)tgt;
88   selector=sel;
89   _flags=flags;
90   _tag=tag;
91   topcasc=NULL;
92   toplabel=label.text();
93   topdir=dirname.text();
94   topowner=owner;
95   rescan();
96 }
97 
98 
99 
DeletePanes()100 void UserMenu::DeletePanes()
101 {
102   for (FXint i=0; i<panes.no(); i++) {
103     FXMenuPane*pane=panes[i];
104     if (pane->getUserData()) { free(pane->getUserData()); }
105     delete pane;
106   }
107   panes.clear();
108 }
109 
110 
111 
~UserMenu()112 UserMenu::~UserMenu()
113 {
114   DeletePanes();
115   if (topcasc) {
116     delete(topcasc);
117     topcasc=NULL;
118   }
119 }
120 
121 
122 
create()123 void UserMenu::create()
124 {
125   for (FXint i=0; i<panes.no(); i++) {
126     panes[i]->create();
127   }
128   topcasc->create();
129 }
130 
131 
132 
rescan()133 void UserMenu::rescan()
134 {
135   bool created=false;
136   level=1;
137   count=0;
138   hasitems=false;
139   DeletePanes();
140   if (topcasc) {
141     created=true;
142     delete(topcasc);
143     topcasc=NULL;
144   }
145   ScanDir(topowner,topdir.text());
146   if (created) { create(); }
147   if (topcasc) {
148     if (hasitems) {
149       if (panes[0]->numChildren()==1) {
150         FXMenuCascade*mc=(FXMenuCascade*)panes[0]->getFirst();
151         if (mc && (strcmp(mc->getClassName(),"FXMenuCascade")==0)) {
152           if (mc->getText()=="Hidden") {
153             topcasc->hide();
154           }
155         }
156       }
157     } else { topcasc->hide(); }
158   }
159 }
160 
161 
162 
enable()163 void UserMenu::enable()
164 {
165   if (topcasc) { topcasc->enable(); }
166 }
167 
168 
169 
disable()170 void UserMenu::disable()
171 {
172   if (topcasc) { topcasc->disable(); }
173 }
174 
175 
176 
getText()177 FXString UserMenu::getText()
178 {
179   if (topcasc) {return topcasc->getText(); } else {
180     FXString rv=FXPath::name(topdir);
181     rv[0]=toupper(rv[0]);
182     return rv;
183   }
184 }
185 
186 
187 
setText(const FXString & s)188 void UserMenu::setText(const FXString &s)
189 {
190   if (topcasc) { topcasc->setText(s); }
191 }
192 
193 
194 
MakeLabelFromPath(const char * path,FXString & label)195 bool UserMenu::MakeLabelFromPath(const char*path, FXString &label)
196 {
197   label=FXPath::name(path);
198 
199   if ( (label.length()>=3) && isdigit(label[0]) && isdigit(label[0]) && (label[2]=='.')) {
200     label.erase(0,3);
201   } else {
202     return false;
203   }
204 
205   label=FXPath::stripExtension(label);
206 
207   if (!(FXPath::extension(label)).empty()) {
208     label=FXPath::stripExtension(label);
209   }
210   FXString accel="";
211   FXint at=label.find('@');
212   if (at>=0) {
213     accel=label.text();
214     accel.erase(0,at+1);
215     label.trunc(at);
216   }
217   if (isdigit(label[0])&&isdigit(label[1]&&(label[2]=='.'))) { label.erase(0,3); }
218   label.lower();
219   label.substitute('_', '&', false);
220   label.substitute('-', ' ', true);
221   label.substitute('\t', ' ', true);
222   label.trim();
223   label.simplify();
224   if (!accel.empty()) {
225     label.append("\t");
226     label.append(accel);
227   }
228   if (label[0]=='&') {
229     label[1]=toupper(label[1]);
230   } else {
231     label[0]=toupper(label[0]);
232   }
233   return true;
234 }
235 
236 
237 
238 typedef struct _StrNode {
239   struct _StrNode*next;
240   char* data;
241 } StrNode;
242 
243 
244 
NodeCmp(StrNode * n1,StrNode * n2)245 static int NodeCmp(StrNode*n1, StrNode*n2)
246 {
247   return strcasecmp(n1->data, n2->data);
248 }
249 
250 
251 typedef int (*SlFunc)(void*,void*);
252 
NodeFree(void * p)253 static void NodeFree(void*p)
254 {
255   StrNode*n=(StrNode*)p;
256   free(n);
257 }
258 
259 
260 
NodeNew(const char * s)261 static StrNode*NodeNew(const char*s)
262 {
263   StrNode*n=(StrNode*)malloc(sizeof(StrNode));
264   n->next=NULL;
265   n->data=strdup(s);
266   return n;
267 }
268 
269 
270 
271 class UserMenuHelper {
272 public:
IncCount(UserMenu * um)273   static void IncCount(UserMenu*um) { um->count++; um->hasitems=true; }
ScanDir(UserMenu * um,FXMenuPane * parent,const char * directory)274   static void ScanDir(UserMenu*um, FXMenuPane*parent, const char *directory) { um->ScanDir(parent, directory); }
275 };
276 
277 
278 
NodeCB(StrNode * n,FXMenuPane * mp)279 int NodeCB(StrNode*n, FXMenuPane*mp)
280 {
281   UserMenu*um=(UserMenu*)mp->getUserData();
282   if ( (mp->numChildren()<MAX_ENTRIES_PER) && (um->getCount()<MAX_ENTRIES_ALL)) {
283     if ( FXStat::isDirectory(n->data) ) {
284      UserMenuHelper::ScanDir(um, mp, n->data);
285      free(n->data);
286     } else {
287       FXString label;
288       if ( UserMenu::MakeLabelFromPath(n->data, label) ) {
289         UserMenuHelper::IncCount(um);
290         UsrMnuCmd*cmd=new UsrMnuCmd(mp,label,um->getTarget(),um->getSelector());
291         cmd->setUserData(n->data);
292         cmd->ValidateTBarCmd();
293       }
294     }
295   }
296   return 0;
297 }
298 
299 
300 
ScanDir(FXMenuPane * parent,const char * directory)301 void UserMenu::ScanDir(FXMenuPane*parent, const char *directory)
302 {
303   FXint matchmode=FILEMATCH_FILE_NAME|FILEMATCH_NOESCAPE;
304   FXString fn;
305   FXString pathname;
306   FXString pattern="*";
307   FXDir dir;
308   FXStat info;
309   level++;
310   if (( level<=MAX_DEPTH ) && dir.open(directory)) {
311     void*list=NULL;
312 
313 #ifdef FOX_1_6
314     while (dir.next()) {
315       fn=dir.name();
316 #else
317     FXString name;
318     while (dir.next(name)) {
319       fn=name.text();
320 #endif
321       if ( fn[0]=='.') { continue; }
322       pathname=directory;
323       if(!ISPATHSEP(pathname[pathname.length()-1])) { pathname.append(PATHSEPSTRING); }
324       pathname.append(fn);
325       if (FXStat::statFile(pathname,info)) {
326         if ((info.isFile() && PathMatch(pattern,fn,matchmode))||info.isDirectory()) {
327           list=sl_push(list,NodeNew(pathname.text()));
328         }
329       }
330     }
331     dir.close();
332     FXString label;
333     if (MakeLabelFromPath(directory, label)||(!topcasc)) {
334       count++;
335       if (list) { list=sl_mergesort(list,(SlFunc)NodeCmp); }
336       FXMenuPane*mp=new FXMenuPane(target);
337       panes.append(mp);
338       FXMenuCascade* mc;
339       if (topcasc) {
340         mc=new FXMenuCascade(parent,label,NULL,mp);
341         if (label=="Hidden") { mc->hide(); }
342       } else {
343         mc=new FXMenuCascade(parent,toplabel,NULL,mp);
344         topcasc=mc;
345       }
346       mp->setUserData(this);
347       if (list) { sl_map(list,(SlFunc)NodeCB, mp); } else { mc->hide(); }
348       mp->setUserData(strdup(directory));
349     }
350     if (list) { sl_free(list,NodeFree); }
351   }
352   level--;
353 }
354 
355 
356 
357 class HistMnuCmd: public FXMenuCommand {
358 public:
359   HistMnuCmd( FXComposite *p,
360               const FXString &text, FXObject *tgt=NULL, FXSelector sel=0):FXMenuCommand(p,text,NULL,tgt,sel){}
361   ~HistMnuCmd() { free(getUserData()); }
362 };
363 
364 
365 
366 FXDEFMAP(HistMenu) HistMenuMap[] = {
367   FXMAPFUNC(SEL_COMMAND,HistMenu::ID_ITEM_CLICK,HistMenu::onItemClick),
368 };
369 
370 FXIMPLEMENT(HistMenu,FXMenuPane,HistMenuMap,ARRAYNUMBER(HistMenuMap))
371 
372 
373 
374 long HistMenu::onItemClick(FXObject*o,FXSelector sel,void*p)
375 {
376   HistMnuCmd*mc = (HistMnuCmd*)o;
377   if (target&&message) { target->handle(this,FXSEL(SEL_COMMAND, message), mc->getUserData()); }
378   return 1;
379 }
380 
381 
382 
383 void HistMenu::add_item(const FXString &txt, bool prepended)
384 {
385   if ((!prepended) && (numChildren()>=26)) { return; }
386   if (!txt.empty()) {
387     if (prepended) {
388       remove(txt);
389     } else {
390       if (find(txt)) { return; }
391     }
392     FXStat info;
393     if ( FXStat::statFile(txt,info) && info.isFile() && info.isReadable() ) {
394       HistMnuCmd*mc=new HistMnuCmd(this,txt,this,ID_ITEM_CLICK);
395       mc->setUserData((void*)(strdup(  FXPath::simplify(FXPath::absolute(txt)).text()  )));
396       if (created) { mc->create(); }
397       if (prepended) {
398         mc->reparent(this, this->getFirst());
399         while (numChildren()>26) { delete getLast(); }
400       }
401     }
402   }
403 }
404 
405 
406 
407 void HistMenu::prepend(const FXString &txt)
408 {
409   add_item(txt, true);
410 }
411 
412 
413 
414 void HistMenu::append(const FXString &txt)
415 {
416   add_item(txt, false);
417 }
418 
419 
420 
421 void HistMenu::remove(const FXString &txt)
422 {
423   if (!txt.empty()) {
424     FXWindow*w;
425     FXString s=FXPath::simplify(FXPath::absolute(txt)).text();
426     for (w=getFirst(); w; ) {
427       if (strcmp(s.text(), (char*)w->getUserData())==0) {
428         FXWindow*w2=w->getNext();
429         delete (HistMnuCmd*) w;
430         w=w2;
431       } else {
432         w=w->getNext();
433       }
434     }
435   }
436 }
437 
438 
439 
440 FXMenuCommand*HistMenu::find(const FXString &txt)
441 {
442   if (!txt.empty()) {
443     FXWindow*w;
444     FXString s=FXPath::simplify(FXPath::absolute(txt)).text();
445     for (w=getFirst(); w; w=w->getNext()) {
446       if (strcmp(s.text(), (char*)w->getUserData())==0) {
447         return (FXMenuCommand*)w;
448       }
449     }
450   }
451   return NULL;
452 }
453 
454 
455 
456 HistMenu::HistMenu(FXWindow *p,
457   const FXString &caption, const FXString &groupname, FXObject *tgt, FXSelector sel):FXMenuPane(p)
458 {
459   target=tgt;
460   message=sel;
461   created=false;
462   FXchar key[2]="\0";
463   casc=new FXMenuCascade((FXComposite*)p,caption, NULL, this);
464   group=groupname.text();
465   app=p->getApp();
466   for (key[0]='a'; key[0]<='z'; key[0]++) { prepend(app->reg().readStringEntry(group.text(),key, "")); }
467 }
468 
469 
470 
471 HistMenu::~HistMenu()
472 {
473   FXchar key[2]="\0";
474   FXWindow*w;
475   for (key[0]='a'; key[0]<='z'; key[0]++) { append(app->reg().readStringEntry(group.text(),key, "")); }
476   app->reg().deleteSection(group.text());
477   for (w=getFirst(), key[0]='z' ; w && (key[0]>='a'); w=w->getNext(), key[0]--) {
478     app->reg().writeStringEntry(group.text(),key,(char*)w->getUserData());
479   }
480 }
481 
482 
483 
484 void HistMenu::create()
485 {
486   if (created) return;
487   created=true;
488   FXMenuPane::create();
489   casc->create();
490 }
491 
492